@sdsrs/llm-wiki 0.6.2 → 0.6.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/CHANGELOG.md +46 -0
- package/README.md +6 -2
- package/bin/llm-wiki.mjs +4 -0
- package/package.json +1 -1
- package/src/convert.mjs +8 -4
- package/src/export.mjs +3 -0
- package/src/scanner.mjs +9 -2
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,51 @@
|
|
|
1
1
|
# Changelog
|
|
2
2
|
|
|
3
|
+
## 0.6.4 (2026-07-11)
|
|
4
|
+
|
|
5
|
+
Six fixes from a full-pipeline self-test pass (as a real user across every
|
|
6
|
+
command). No breaking changes, no KB migration, no config change. Suite 179 →
|
|
7
|
+
185.
|
|
8
|
+
|
|
9
|
+
- **fix(convert): PDF conversion was 100% broken** and is now restored. The code
|
|
10
|
+
imported `pdf-parse/lib/pdf-parse.js` — the v1 subpath — but the pinned
|
|
11
|
+
dependency is pdf-parse v2, whose export map has no such subpath, so *every*
|
|
12
|
+
PDF failed at import time before any parsing. Switched to the v2 API
|
|
13
|
+
(`new PDFParse({ data }).getText()`). It slipped through because there was no
|
|
14
|
+
PDF fixture and the only test fed garbage bytes, which the import failure also
|
|
15
|
+
satisfied; added a real `test/fixtures/hello.pdf` + a positive extraction test.
|
|
16
|
+
- fix(ask): `ask --retrieve-only` no longer prints nothing (exit 0) when
|
|
17
|
+
retrieval locates zero pages — the state right after `init`/`scan`, before the
|
|
18
|
+
wiki is built. It now emits a diagnostic to stderr (stdout stays pipe-clean),
|
|
19
|
+
matching the LLM path which already errored.
|
|
20
|
+
- fix(convert): a source file whose whole basename is emoji/symbols
|
|
21
|
+
(e.g. `😀.md`) no longer produces a hidden `raw/.md` that `status` skips
|
|
22
|
+
(silently dropping the source from the incremental workflow). `slugify` now
|
|
23
|
+
falls back to `untitled`, so the existing `-N` collision handler yields
|
|
24
|
+
visible, shell-safe names (`untitled.md`, `untitled-2.md`, …).
|
|
25
|
+
- fix(scan): a missing or non-directory source path now gives a human-readable
|
|
26
|
+
error ("source directory not found: X" / "source path is not a directory: X")
|
|
27
|
+
instead of a raw `ENOENT`/`ENOTDIR`; also improves `status --src`.
|
|
28
|
+
- fix(export): `export --out path/with/missing/dirs/graph.graphml` now creates
|
|
29
|
+
the parent directory (matching the markdown-export sibling) instead of leaking
|
|
30
|
+
a raw `ENOENT`.
|
|
31
|
+
- docs: the layout diagram placed `llms.txt` under `wiki/`, but `index` writes it
|
|
32
|
+
to the KB root (the llms.txt convention); corrected the diagram and command
|
|
33
|
+
table.
|
|
34
|
+
|
|
35
|
+
## 0.6.3 (2026-07-11)
|
|
36
|
+
|
|
37
|
+
Two cosmetic fixes from an independent review of 0.6.0–0.6.2; no behavior
|
|
38
|
+
change beyond the scan warning's rounding.
|
|
39
|
+
|
|
40
|
+
- fix(scan): the tag-dispersion warning now flags on the same rounded top-tag
|
|
41
|
+
share it displays, so it can no longer print "top tag covers 30%" while
|
|
42
|
+
firing a "below 30%" rule. Effective threshold is 29.5% (was a raw 30% with
|
|
43
|
+
an inconsistent display); negligible for an advisory heuristic. Suite 178 →
|
|
44
|
+
179.
|
|
45
|
+
- docs: note that the JSON Canvas map's `file` cards are vault-relative from the
|
|
46
|
+
KB root, so a canvas written outside the KB via `--out` won't resolve them in
|
|
47
|
+
Obsidian — open the canvas with the KB as its own vault.
|
|
48
|
+
|
|
3
49
|
## 0.6.2 (2026-07-11)
|
|
4
50
|
|
|
5
51
|
No breaking changes, no KB migration, no default-behavior change — one additive
|
package/README.md
CHANGED
|
@@ -84,7 +84,7 @@ npx @sdsrs/llm-wiki@0 ask "what did we decide about X?"
|
|
|
84
84
|
| `init [dir]` | scaffold a knowledge base (raw/, wiki/, AGENTS.md, wiki.config.json) |
|
|
85
85
|
| `scan <srcDir>` | inventory a source dir: dedup, batches, incremental diff, token estimate |
|
|
86
86
|
| `convert` | convert files from the scan plan into `raw/` markdown |
|
|
87
|
-
| `index` | rebuild `wiki/index.md`, `graph.json`, `llms.txt` from page frontmatter |
|
|
87
|
+
| `index` | rebuild `wiki/index.md`, `wiki/graph.json`, and root `llms.txt` from page frontmatter |
|
|
88
88
|
| `ask <question>` | answer from the KB using full pages (never chunks), with citations |
|
|
89
89
|
| `lint` | mechanical checks + a semantic worklist for the agent |
|
|
90
90
|
| `status` | incremental state: uncompiled raw files, source-dir diff, affected wiki pages |
|
|
@@ -147,6 +147,9 @@ human to resolve.
|
|
|
147
147
|
|
|
148
148
|
`export --format canvas` writes a JSON Canvas (`graph.canvas`) domain map —
|
|
149
149
|
`file` cards laid out by page type that open as live notes in Obsidian Canvas.
|
|
150
|
+
The card paths are vault-relative from the KB root, so open the canvas with the
|
|
151
|
+
KB as its own vault; a canvas written outside the KB via `--out` won't resolve
|
|
152
|
+
its cards.
|
|
150
153
|
|
|
151
154
|
### Skills (Claude Code / Codex)
|
|
152
155
|
|
|
@@ -213,7 +216,8 @@ my-kb/
|
|
|
213
216
|
raw/ immutable converted source documents (agents only read)
|
|
214
217
|
wiki/
|
|
215
218
|
sources/ entities/ concepts/ comparisons/ full pages, one file each
|
|
216
|
-
index.md graph.json
|
|
219
|
+
index.md graph.json generated by `index`
|
|
220
|
+
llms.txt flat page listing for agents, KB root (generated by `index`)
|
|
217
221
|
AGENTS.md the contract every maintaining agent follows
|
|
218
222
|
wiki.config.json thresholds, batch size, language
|
|
219
223
|
```
|
package/bin/llm-wiki.mjs
CHANGED
|
@@ -74,6 +74,10 @@ program.command('ask <question>')
|
|
|
74
74
|
if (!Number.isFinite(k) || k < 1) { console.error(`invalid -k value: ${opts.k} (expected a positive integer)`); process.exit(1) }
|
|
75
75
|
const r = await askKb(opts.kb, question, { k, retrieveOnly: opts.retrieveOnly })
|
|
76
76
|
if (opts.retrieveOnly) {
|
|
77
|
+
if (r.pages.length === 0) {
|
|
78
|
+
console.error('no pages located — the KB has no matching wiki pages (build them with the wiki-build skill, or check that wiki/ contains pages)')
|
|
79
|
+
return
|
|
80
|
+
}
|
|
77
81
|
for (const h of r.pages) console.log(`${h.score.toFixed(3)} ${h.relPath} [${(h.sources ?? ['bm25']).join('+')}]`)
|
|
78
82
|
} else {
|
|
79
83
|
console.log(r.answer)
|
package/package.json
CHANGED
package/src/convert.mjs
CHANGED
|
@@ -4,10 +4,14 @@ import path from 'node:path'
|
|
|
4
4
|
export const SUPPORTED_EXTS = ['.md', '.markdown', '.txt', '.html', '.htm', '.pdf', '.docx']
|
|
5
5
|
|
|
6
6
|
export function slugify(name) {
|
|
7
|
-
|
|
7
|
+
const slug = name.toLowerCase()
|
|
8
8
|
.replace(/\.[a-z0-9]+$/i, '')
|
|
9
9
|
.replace(/[^\p{L}\p{N}]+/gu, '-')
|
|
10
10
|
.replace(/^-+|-+$/g, '')
|
|
11
|
+
// A basename made entirely of emoji/symbols slugs to '' → a hidden `raw/.md`
|
|
12
|
+
// dotfile that `status` skips (source silently dropped). Never return empty;
|
|
13
|
+
// the caller's -N collision handler disambiguates untitled/untitled-2/…
|
|
14
|
+
return slug || 'untitled'
|
|
11
15
|
}
|
|
12
16
|
|
|
13
17
|
function titleFrom(markdown, fallback) {
|
|
@@ -44,9 +48,9 @@ export async function convertFile(srcPath) {
|
|
|
44
48
|
return { markdown: `# ${article.title || base}\n\n${md}`, title: article.title || base, warnings }
|
|
45
49
|
}
|
|
46
50
|
if (ext === '.pdf') {
|
|
47
|
-
const
|
|
48
|
-
const
|
|
49
|
-
return { markdown:
|
|
51
|
+
const { PDFParse } = await import('pdf-parse')
|
|
52
|
+
const { text } = await new PDFParse({ data: fs.readFileSync(srcPath) }).getText()
|
|
53
|
+
return { markdown: text, title: titleFrom(text, base), warnings }
|
|
50
54
|
}
|
|
51
55
|
if (ext === '.docx') {
|
|
52
56
|
const mammoth = await import('mammoth')
|
package/src/export.mjs
CHANGED
|
@@ -301,6 +301,9 @@ export function exportGraph(kbRoot, { format, out } = {}) {
|
|
|
301
301
|
if (!render) throw new Error(`unknown format: ${format} (expected ${Object.keys(RENDERERS).join(' | ')} | markdown)`)
|
|
302
302
|
const graph = loadGraph(kbRoot)
|
|
303
303
|
const outPath = out ?? path.join(kbRoot, `graph.${EXT[format]}`)
|
|
304
|
+
// Match the markdown-export sibling: create the parent of an explicit --out so
|
|
305
|
+
// `--out new/dir/graph.graphml` writes instead of leaking a raw ENOENT.
|
|
306
|
+
fs.mkdirSync(path.dirname(path.resolve(outPath)), { recursive: true })
|
|
304
307
|
fs.writeFileSync(outPath, render(graph))
|
|
305
308
|
return { out: outPath, nodeCount: graph.nodes.length, edgeCount: graph.edges.length }
|
|
306
309
|
}
|
package/src/scanner.mjs
CHANGED
|
@@ -33,8 +33,12 @@ function detectDomainMixture(files, kbRoot) {
|
|
|
33
33
|
if (pages.length >= TAG_MIN_PAGES) {
|
|
34
34
|
const counts = new Map()
|
|
35
35
|
for (const p of pages) for (const t of new Set(p.data.tags)) counts.set(t, (counts.get(t) ?? 0) + 1)
|
|
36
|
-
|
|
37
|
-
|
|
36
|
+
// Round once and flag on the rounded value so the stored/displayed share
|
|
37
|
+
// (CLI prints Math.round(topShare*100)) never reads "30%" while the strict
|
|
38
|
+
// `< TAG_TOP_SHARE_MIN` rule fired on a raw 0.29x. Effective threshold is
|
|
39
|
+
// 29.5%; negligible for an advisory heuristic, and flag/display now agree.
|
|
40
|
+
const topShare = Number((Math.max(...counts.values()) / pages.length).toFixed(2))
|
|
41
|
+
tags = { pages: pages.length, distinct: counts.size, topShare, flagged: topShare < TAG_TOP_SHARE_MIN }
|
|
38
42
|
}
|
|
39
43
|
return { language, tags, flagged: language.flagged || (tags?.flagged ?? false) }
|
|
40
44
|
}
|
|
@@ -67,6 +71,9 @@ function* walk(dir, base = dir, skippedDirs = [], exclude = []) {
|
|
|
67
71
|
// `persist: false` runs a read-only scan (e.g. for `status`) that does not
|
|
68
72
|
// overwrite the .scan-plan.json a previous explicit `scan` produced.
|
|
69
73
|
export async function scanSource(srcDir, kbRoot, { exclude = [], persist = true } = {}) {
|
|
74
|
+
const st = fs.statSync(srcDir, { throwIfNoEntry: false })
|
|
75
|
+
if (!st) throw new Error(`source directory not found: ${srcDir}`)
|
|
76
|
+
if (!st.isDirectory()) throw new Error(`source path is not a directory: ${srcDir}`)
|
|
70
77
|
const cfg = loadKbConfig(kbRoot)
|
|
71
78
|
const files = []
|
|
72
79
|
const skipped = []
|