@msareen/knowledge-hub-builder 0.1.3 → 0.1.4

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.md CHANGED
@@ -138,7 +138,7 @@ From outside, pass `--hub <dir>` or set `$KHB_HUB`.
138
138
  | `khb upgrade` | refresh this hub's package-owned contract docs |
139
139
  | `khb visualize` | regenerate `visualizer/graph.html` |
140
140
  | `khb new-bundle <name>` | scaffold + register a bundle |
141
- | `khb ingest [bundle]` | acquire + extract every source in `sources.yaml` → `raw/`; maintains `log.md`. No bundle named `default`, created if absent |
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
144
  There is no `khb catalog` command — cataloging is entirely a judgement pass.
@@ -147,10 +147,17 @@ There is no `khb catalog` command — cataloging is entirely a judgement pass.
147
147
 
148
148
  **Ingest** (`skills/ingest/SKILL.md`) is mechanical and flat: `khb ingest <bundle>` pulls
149
149
  every declared source into `raw/` as markdown with a provenance header, extracting
150
- everything it can locally — into the named bundle, or into `default` when none is named — text, PDF, DOCX, ODT, XLSX, PPTX, images by OCR, audio and
150
+ everything it can locally — text, PDF, DOCX, ODT, XLSX, PPTX, images by OCR, audio and
151
151
  video by whisper. Sources behind an authenticated API (Confluence, ADO, git hosts) you pull
152
152
  yourself via MCP/CLI into the same `raw/` shape. Ingest never interprets content.
153
153
 
154
+ When the user has not named a bundle, **ask** which existing bundle owns the material or
155
+ whether to start a new one, and for an existing bundle ask whether to re-ingest what its
156
+ `sources.yaml` already declares or to take a new path. Never choose a destination silently.
157
+ The single exception is a hub with nothing to choose between: with no bundles at all an
158
+ unnamed ingest lands in `default`, created on the spot, and where `default` is the only
159
+ bundle it lands there as it stands.
160
+
154
161
  **Catalog** (`skills/catalog/SKILL.md`) is the judgement half, one bundle at a time: read
155
162
  each `raw/` file, split it into concepts, give each OKF frontmatter, link them, register
156
163
  them in `index.md`. When the runtime supports parallel agents, fan them out over the raw
package/README.md CHANGED
@@ -180,9 +180,13 @@ Create one only when you intend to:
180
180
  khb new-bundle team-payments "Payments team roadmap, incidents, and vendor decisions"
181
181
  ```
182
182
 
183
- KHB never creates, splits, or merges named bundles based on their contents. When
184
- `khb ingest` is run without a bundle name, it uses a `default` landing bundle and creates it
185
- if needed.
183
+ KHB never creates, splits, or merges bundles based on their contents. Name the bundle when
184
+ you ingest: run `khb ingest` without one and it lists the hub's bundles and stops. Ask an
185
+ agent to ingest without naming a bundle and it asks you — existing bundle or new one, and for
186
+ an existing one whether to reuse the paths in its `sources.yaml` or take a new path. An
187
+ unnamed ingest lands somewhere by itself only when there is nothing to choose between: a hub
188
+ with no bundles gets a `default` bundle created on the spot, and a hub whose only bundle is
189
+ `default` uses it. Material moves out of `default` when you say who owns it.
186
190
 
187
191
  Cross-bundle relationships belong in `refs.md`; concept documents must not link directly
188
192
  into another bundle.
package/SPEC.md CHANGED
@@ -217,12 +217,20 @@ Two look-alikes are deliberately excluded, since both would rewire provenance on
217
217
  **copy** (the twin's path still exists — two real sources, both ingested) and an **ambiguous**
218
218
  match (several vanished rows share the hash). Each is reported and left to judgement.
219
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.
220
+ A bundle is a logical unit its owner defines, so the destination is a human decision. An
221
+ explicit name that doesn't resolve is an error, not a scaffold request; a bare `khb ingest`
222
+ in a hub that has real bundles prints them and stops, because the CLI cannot ask. The agent
223
+ asks instead (`skills/ingest/SKILL.md` §1–2): which existing bundle, or a new one; then, for
224
+ an existing bundle, whether to re-ingest its declared `sources.yaml` paths or take a new path.
225
+
226
+ `default` survives as the fallback for a hub with nothing to choose between: a bare
227
+ `khb ingest` scaffolds it and lands there when there are **no bundles at all**, and uses it
228
+ when it is the **only** bundle — a one-option question is not a choice, and the first ingest
229
+ anyone runs should not fail for want of a destination. It is not an option once a real bundle
230
+ exists — that was the earlier design, and it bought a pile of material whose ownership
231
+ nobody had decided, which is exactly the decision cataloging then has to make blind.
232
+ `default` is a holding area, not a tier: its contents are cataloged like any bundle's, and
233
+ they move only when a human says which bundle should own them.
226
234
 
227
235
  Every raw file carries its origin, so a lossy extraction is always recoverable:
228
236
 
package/document/faq.md CHANGED
@@ -58,8 +58,12 @@ source, extracts it into markdown under `raw/`, and updates `log.md`.
58
58
  It does not summarize, label, organize, or create concepts. Unchanged content hashes are
59
59
  skipped by default; `--force` reacquires everything.
60
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.
61
+ A named bundle must already exist. Run without a name and the command lists the hub's bundles
62
+ and stops unless there is nothing to choose between: a hub with no bundles gets a `default`
63
+ landing bundle created so a first ingest still works, and a hub whose only bundle is `default`
64
+ uses it. An agent asked to ingest without a named bundle asks you which
65
+ existing bundle owns the material or whether to create a new one, and for an existing bundle
66
+ whether to re-ingest the paths already in its `sources.yaml` or take a new path.
63
67
 
64
68
  ## Should I run ingestion manually or ask an agent?
65
69
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@msareen/knowledge-hub-builder",
3
- "version": "0.1.3",
3
+ "version": "0.1.4",
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
@@ -7,7 +7,7 @@ const COMMANDS: Record<string, { load: () => Promise<unknown>; help: string }> =
7
7
  init: { load: () => import("./init"), help: "khb init [dir] create a hub here (or in dir)" },
8
8
  upgrade: { load: () => import("./init"), help: "khb upgrade refresh this hub's contract docs" },
9
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')" },
10
+ ingest: { load: () => import("./ingest/index"), help: "khb ingest <bundle> [--force] acquire + extract declared sources → raw/ (name required once the hub has a bundle other than 'default')" },
11
11
  lint: { load: () => import("./lint"), help: "khb lint validate the hub against skills/lint/SKILL.md" },
12
12
  visualize: { load: () => import("./visualize"), help: "khb visualize [--port N] [--no-open] serve the live bundle graph in your browser; aliases: vis, viz" },
13
13
  export: { load: () => import("./export"), help: "khb export <bundle> [dest] standalone copy of one bundle" },
@@ -12,7 +12,7 @@
12
12
  import { parse } from "yaml";
13
13
  import { read, join, HUB } from "../lib/util";
14
14
  import { detail, section, totalElapsed } from "../lib/log";
15
- import { bundleForIngest, DEFAULT_BUNDLE } from "../lib/scaffold";
15
+ import { bundleForIngest, listBundles, DEFAULT_BUNDLE } from "../lib/scaffold";
16
16
  import { readLedger, writeLedger } from "../lib/ledger";
17
17
  import { takeFlag } from "../lib/args";
18
18
  import { ingestFolder } from "./folder";
@@ -43,10 +43,23 @@ if (positional.length > 1) {
43
43
  console.error("Usage: khb ingest [bundle] [--force] [--skip-ocr] [--skip-audio]");
44
44
  process.exit(1);
45
45
  }
46
- // No bundle named `default`, created on the spot if the hub has none. Bytes always have
47
- // somewhere to land; sorting them into real bundles is a later, cheaper decision (a concept
48
- // is one file, and moving it is a `git mv`). Naming a bundle explicitly stays the norm.
49
- const bundle = positional[0] ?? DEFAULT_BUNDLE;
46
+ // No bundle named: which one owns the material is a human decision (AGENTS.md) and a CLI
47
+ // cannot ask, so it stops and shows what the hub has, leaving the choice to whoever is
48
+ // driving. The exception is a hub with nothing to choose between no bundles at all, or
49
+ // only the landing bundle where bytes go to `default` rather than the ingest failing.
50
+ let bundle = positional[0];
51
+ if (!bundle) {
52
+ const have = listBundles();
53
+ const onlyLanding = have.length === 1 && have[0] === DEFAULT_BUNDLE;
54
+ if (have.length && !onlyLanding) {
55
+ console.error("Usage: khb ingest [bundle] [--force] [--skip-ocr] [--skip-audio]");
56
+ console.error(`\nBundles in this hub: ${have.join(", ")}`);
57
+ console.error(`Name the one that owns this material, or start a new one:`);
58
+ console.error(` khb new-bundle <name> "<scope>"`);
59
+ process.exit(1);
60
+ }
61
+ bundle = DEFAULT_BUNDLE;
62
+ }
50
63
 
51
64
  const dir = bundleForIngest(bundle);
52
65
  let cfg: unknown;
@@ -1,9 +1,12 @@
1
- // Bundle creation, shared by `khb new-bundle` and by ingest's default-bundle fallback.
2
- // One implementation so a bundle born from a bare `khb ingest` is indistinguishable from
3
- // one the user named: same template, same {{name}} substitution, same outer.index.md row.
1
+ // Bundle creation and lookup, shared by `khb new-bundle` and `khb ingest`.
2
+ // One implementation so every bundle is born the same way: same template, same {{name}}
3
+ // substitution, same outer.index.md row — and nothing ever conjures one implicitly.
4
4
  import { cpSync, readFileSync, writeFileSync, readdirSync, statSync, existsSync } from "node:fs";
5
5
  import { HUB, BUNDLES, TEMPLATE, join } from "./util";
6
6
 
7
+ // The landing bundle: where a bare `khb ingest` goes in a hub with nothing to choose between
8
+ // — conjured when the hub has no bundles at all, reused when it is the only one. With a real
9
+ // bundle present the destination is a choice and the user makes it, so nothing is auto-created.
7
10
  export const DEFAULT_BUNDLE = "default";
8
11
 
9
12
  export const VALID_NAME = /^[a-z0-9][a-z0-9-]*$/;
@@ -33,24 +36,34 @@ export function createBundle(name: string, scope: string): string {
33
36
  return dest;
34
37
  }
35
38
 
39
+ /** Names of the bundles that exist in this hub, alphabetically. */
40
+ export function listBundles(): string[] {
41
+ if (!existsSync(BUNDLES)) return [];
42
+ return readdirSync(BUNDLES)
43
+ .filter((n) => statSync(join(BUNDLES, n)).isDirectory())
44
+ .sort();
45
+ }
46
+
36
47
  /**
37
- * Resolve the bundle to ingest into, creating `default` if that is the target and it does
38
- * not exist yet. A hub with no bundles must still have somewhere for bytes to land — the
39
- * alternative is refusing the first ingest anyone ever runs. Only `default` is ever
40
- * conjured this way: a misspelled explicit name is a mistake, not a request to scaffold.
48
+ * Resolve the bundle to ingest into. A name the user gave must already exist which bundle
49
+ * owns material is their decision, so an unresolvable name is a typo, not a scaffold request.
50
+ * The one exception is `default` in a hub with no bundles: the first ingest anywhere must
51
+ * still have somewhere to land, and there is no choice to put to the user yet.
41
52
  */
42
53
  export function bundleForIngest(name: string): string {
43
54
  const dir = join(BUNDLES, name);
44
55
  if (existsSync(dir)) return dir;
45
- if (name !== DEFAULT_BUNDLE) {
46
- console.error(`No such bundle: ${name}`);
47
- console.error(`Create it: khb new-bundle ${name} "<scope>"`);
48
- process.exit(1);
56
+ const have = listBundles();
57
+ if (name === DEFAULT_BUNDLE && !have.length) {
58
+ // The scope line lands in outer.index.md, where every agent reads it so it must not
59
+ // read as an instruction to reorganize the hub. Splitting `default` into real bundles is
60
+ // the user's call, exactly like any other bundle decision.
61
+ createBundle(DEFAULT_BUNDLE, "Unsorted material — where a first ingest lands before any bundle exists; moves out when you say which bundle owns it");
62
+ console.log(`Created bundles/${DEFAULT_BUNDLE}/ — this hub had no bundles to land in.`);
63
+ return dir;
49
64
  }
50
- // The scope line lands in outer.index.md, where every agent reads it — so it must not
51
- // read as an instruction to reorganize the hub. Splitting `default` into real bundles is
52
- // the user's call, exactly like any other bundle decision.
53
- createBundle(DEFAULT_BUNDLE, "Unsorted material — where an ingest with no named bundle lands; moves out when you say which bundle owns it");
54
- console.log(`Created bundles/${DEFAULT_BUNDLE}/ — the landing bundle for unrouted material.`);
55
- return dir;
65
+ console.error(`No such bundle: ${name}`);
66
+ if (have.length) console.error(`This hub has: ${have.join(", ")}`);
67
+ console.error(`Create it: khb new-bundle ${name} "<scope>"`);
68
+ process.exit(1);
56
69
  }
@@ -11,14 +11,60 @@ text exists. Deciding what the text *means* — splitting it into concepts, titl
11
11
  tagging, linking, indexing — is the [catalog skill](../catalog/SKILL.md), a separate step
12
12
  you run afterwards.
13
13
 
14
- Do not curate here. Do not create, split, or merge bundles here routing material to a
15
- bundle that exists is fine, reshaping the hub is not. If you find yourself reading a document
16
- to understand it, you have left this skill.
17
-
18
- ## 1. Declare the sources
19
-
20
- Ingest is bundle-first: material lands in one bundle, and you say where it comes from. Edit
21
- `bundles/<bundle>/sources.yaml`:
14
+ Do not curate here. Do not split or merge bundles here, and create one only as the user's
15
+ answer to the question in step 1 never on your own initiative. If you find yourself reading
16
+ a document to understand it, you have left this skill.
17
+
18
+ ## 1. Settle the bundle — ask, never assume
19
+
20
+ Ingest is bundle-first: material lands in exactly one bundle, and which bundle owns it is the
21
+ user's decision, not yours. Whenever there is a choice to make, put it to them.
22
+
23
+ **Take the first of these that applies:**
24
+
25
+ 1. **The user named a bundle** — "re-ingest the real-estate bundle" — → use it, no question
26
+ asked. If the name does not resolve to a bundle in `bundles/`, say so and ask whether to
27
+ create it; never scaffold on a guess, and never silently fall back to a similar name.
28
+ 2. **No bundle named, and the hub has real bundles** → ask, always, offering the choice
29
+ explicitly: **an existing bundle** (list them, from `outer.index.md`, with their scope
30
+ lines so the user can tell them apart) **or a new bundle**. Do not pick for the user,
31
+ however plainly one bundle seems to own the material — say which you would pick if you
32
+ have a view, then wait for the answer. Do not offer `default` here and do not mention it;
33
+ with real bundles on the table it is not one of the options.
34
+ 3. **The user answered "a new bundle"** → they name it and give its scope; you run
35
+ `khb new-bundle <name> "<scope>"`. Creating a bundle is a human decision (`AGENTS.md`), so
36
+ this branch only ever runs on an explicit answer to the question above.
37
+ 4. **No bundle named and nothing to choose between** → `default`, without asking. Two shapes
38
+ of hub qualify:
39
+ - **no bundles at all** → `default` is created on the spot; a first ingest should not fail
40
+ for want of a destination.
41
+ - **`default` is the only bundle** → it is used as it stands. A one-option question is not
42
+ a choice, so do not put it to the user.
43
+
44
+ **The `default` bundle** is only that last case — a landing place in a hub that has no other,
45
+ not an option to fall back on once a real bundle exists. The moment one does, `default` stops
46
+ being a destination for unnamed ingests and case 2 applies. What lands there is ordinary
47
+ bundle content: catalog it like any other. Do **not** graduate it into new bundles on your
48
+ own; material leaves `default` when the user says which bundle owns it.
49
+
50
+ ## 2. Declare the sources — reuse or replace
51
+
52
+ Once the bundle is settled, settle where the material comes from. If the user already named
53
+ the files, folders or URLs, that is the answer — you still ask the add-or-replace question
54
+ below when the bundle has declarations of its own. **Otherwise, for an existing bundle, read
55
+ its `sources.yaml` first and ask which you are doing:**
56
+
57
+ - **use what's declared** — re-ingest the paths already in `sources.yaml` (this is what
58
+ "re-ingest the real-estate bundle" usually means), or
59
+ - **a new path** — the user gives files, folders or URLs; ask whether they are *added* to
60
+ the declarations or *replace* them before you edit the file.
61
+
62
+ Quote the current declarations in the question so the answer is informed. A bundle with an
63
+ empty `sources.yaml` has nothing to re-ingest, so there the only answer is a new path — ask
64
+ for it. Do not infer sources from nearby files, do not edit `sources.yaml`, and do not run
65
+ `khb ingest` until the user has answered.
66
+
67
+ Sources live in `bundles/<bundle>/sources.yaml`:
22
68
 
23
69
  ```yaml
24
70
  sources:
@@ -32,46 +78,17 @@ sources:
32
78
  urls:
33
79
  - https://example.com/design-doc
34
80
  # Types with no scripted ingester are still declared here, for the record —
35
- # you pull them yourself in step 3.
81
+ # you pull them yourself in step 4.
36
82
  - type: confluence
37
83
  space: PROJX
38
84
  ```
39
85
 
40
- If the user has not explicitly named the source locations, inspect the bundle's current
41
- `sources.yaml`, then ask which files, folders, URLs, or services to ingest. Include any
42
- existing declarations in the question so the user can confirm or replace them. Do not
43
- infer sources from nearby files, edit `sources.yaml`, or run `khb ingest` until the user
44
- answers.
45
-
46
86
  Nothing is copied by declaring a source.
47
87
 
48
- **Which bundle take the first of these that applies, and do not go further:**
49
-
50
- 1. **The user named a bundle** → use it. A named bundle that does not exist is an error,
51
- not an invitation to create one.
52
- 2. **The hub has bundles and exactly one plainly owns the material** → use it, and say
53
- which you picked. If several could own it, ask which — this is the only bundle question
54
- ingest ever asks.
55
- 3. **Anything else** — no bundle named, or the hub has no bundles at all → `default`,
56
- created on the spot, without asking.
57
-
58
- Never ask the user to name or create a bundle *for the ingest to land in*. `default` exists
59
- so that question never has to be asked at this stage: bytes always have somewhere to go, and
60
- which bundle owns them is a cheaper decision later, once the text exists and the user can see
61
- what they actually have.
62
-
63
- **The `default` bundle.** When no bundle is named, ingest targets `default` and creates it
64
- if the hub has none — a first `khb ingest` never fails for want of a destination. It is not a
65
- way around step 2: when a bundle in the hub plainly owns the material, that bundle wins. What
66
- lands there is ordinary bundle content: catalog it like any other. Do **not** graduate it into new
67
- bundles on your own — a bundle is a logical unit the user defines (a person, a team, a
68
- project), so material leaves `default` only when the user says which bundle owns it. An
69
- explicitly named bundle that doesn't exist is still an error — only `default` is conjured.
70
-
71
- ## 2. Run it
88
+ ## 3. Run it
72
89
 
73
90
  ```
74
- khb ingest # no bundle named the 'default' bundle
91
+ khb ingest # only where 'default' is the sole bundle, or none is
75
92
  khb ingest <bundle> # incremental: unchanged content hashes are skipped
76
93
  khb ingest <bundle> --force # re-acquire everything
77
94
  khb ingest <bundle> --skip-ocr # leave scans and images unread
@@ -135,7 +152,7 @@ pip install -U openai-whisper # transcription (faster-whisper als
135
152
  Install them where `khb` resolves modules from — for a global install that is the khb
136
153
  package directory, not your hub. khb prints the exact `cd … && bun add …` to use.
137
154
 
138
- ## 3. Sources khb cannot reach
155
+ ## 4. Sources khb cannot reach
139
156
 
140
157
  Anything behind an authenticated API has no scripted ingester, because maintaining API
141
158
  wrappers is not what this tool is for. Pull those yourself with the site's MCP server or
@@ -175,7 +192,7 @@ and the text reads thin, garbled, or contradictory, **open the `source:` file an
175
192
  directly** — a vision pass over a chart or a scanned table recovers what OCR drops. Rewrite
176
193
  the `raw/` file with `extract_tool: claude-vision` and `quality: high` when you do.
177
194
 
178
- ## 4. The ledger — `log.md`
195
+ ## 5. The ledger — `log.md`
179
196
 
180
197
  Every bundle keeps its ingest ledger in `log.md` (OKF-reserved, so it is never mistaken for
181
198
  a concept doc, and committed, so it survives `raw/` being deleted and re-derived).