@williamthorsen/kb 0.4.0 → 0.6.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.
- package/README.md +9 -12
- package/bin/kb.js +2 -0
- package/dist/esm/check/enumerate.js +3 -4
- package/dist/esm/cli/commands/check.js +2 -2
- package/dist/esm/cli/commands/create.d.ts +2 -1
- package/dist/esm/cli/commands/create.js +34 -17
- package/dist/esm/cli/commands/set-default.js +3 -3
- package/dist/esm/cli/commands/taxonomy.js +2 -2
- package/dist/esm/cli/index.js +2 -2
- package/dist/esm/cli/parse-flag-value.js +1 -1
- package/dist/esm/cli/run.js +2 -1
- package/dist/esm/cli/select-kb-prompt.js +1 -1
- package/dist/esm/cli/targeting/resolve-changed-paths.js +2 -1
- package/dist/esm/config/kb-loader-error.d.ts +1 -1
- package/dist/esm/config/kb-loader-error.js +2 -2
- package/dist/esm/config/load-config.js +2 -2
- package/dist/esm/create/create.d.ts +2 -0
- package/dist/esm/create/create.js +3 -2
- package/dist/esm/discovery/load-registry.js +7 -6
- package/dist/esm/discovery/register-store.js +23 -4
- package/dist/esm/frontmatter/parse-note.js +1 -1
- package/dist/esm/note-io/write-note.js +5 -1
- package/dist/esm/records/event.js +2 -1
- package/dist/esm/tags/load-aliases.js +4 -4
- package/dist/esm/taxonomy/load-taxonomy.js +2 -2
- package/dist/esm/taxonomy/write-taxonomy.js +7 -2
- package/dist/esm/vault-integrity/check-vault-integrity.js +1 -1
- package/package.json +4 -4
package/README.md
CHANGED
|
@@ -5,17 +5,13 @@ Provides knowledge-base discovery, registry loading, frontmatter parsing and wri
|
|
|
5
5
|
It underpins the knowledge-base skills — among them `kb-retrieve` (assertion recall) and `kb-retrieve-events` (event recall), `kb-add`, `kb-curate`, `capture-event`, and `kb-update-events` — and the planned `@williamthorsen/kb-mcp` server.
|
|
6
6
|
|
|
7
7
|
<!-- section:release-notes -->
|
|
8
|
-
## Release notes — v0.
|
|
8
|
+
## Release notes — v0.6.0 (2026-08-13)
|
|
9
9
|
|
|
10
10
|
### 🎉 Features
|
|
11
11
|
|
|
12
|
-
-
|
|
12
|
+
- Attach causes to kb's loader errors and retire its lint deferral (#1272)
|
|
13
13
|
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
- Guide kb-add note placement with the store's declared taxonomy (#1223)
|
|
17
|
-
|
|
18
|
-
Improves classification of captured knowledge-base notes by aligning with the domains declared by the KB's taxonomy rather than looking to the directory structure. If a note is filed in a folder not covered by a domain, that folder is now added to the base's taxonomy. A domain added without confirmation is recorded as awaiting review.
|
|
14
|
+
Errors `kb` raises on malformed input now carry the underlying parse error as their cause. Callers constructing one of these errors themselves can attach a cause. When `kb` reports a thrown error that carries no message, the diagnostic now names the error's class instead of trailing off empty.
|
|
19
15
|
<!-- /section:release-notes -->
|
|
20
16
|
|
|
21
17
|
## Exports
|
|
@@ -206,9 +202,10 @@ The package ships a `kb` bin with four subcommands: `check`, `create`, `set-defa
|
|
|
206
202
|
`kb create` scaffolds a new knowledge base in the current directory and registers it in the user-global `~/.agents/kb.yaml`.
|
|
207
203
|
|
|
208
204
|
```bash
|
|
209
|
-
kb create
|
|
210
|
-
kb create --name coding
|
|
211
|
-
kb create --
|
|
205
|
+
kb create # scaffold the current directory, register under its name
|
|
206
|
+
kb create --name coding # register under an explicit name
|
|
207
|
+
kb create --description "Coding notes" # describe the registry entry
|
|
208
|
+
kb create --no-register # scaffold without writing the registry
|
|
212
209
|
```
|
|
213
210
|
|
|
214
211
|
It creates these files and directories:
|
|
@@ -221,7 +218,7 @@ It creates these files and directories:
|
|
|
221
218
|
|
|
222
219
|
The config seed is serialized from the in-package `defaultKbConfig`, so a new store cannot drift from the bundled default.
|
|
223
220
|
|
|
224
|
-
The name defaults to the directory's base name; `--name` overrides it and `--no-register` scaffolds without writing the registry. The registry write preserves any existing comments in `kb.yaml
|
|
221
|
+
The name defaults to the directory's base name; `--name` overrides it and `--no-register` scaffolds without writing the registry. `--description` sets the new entry's description, and requires registration: combining it with `--no-register` is a usage error. The registry write preserves any existing comments in `kb.yaml` and leaves the `kbs:` entries alphabetically ordered, so a registry that has drifted out of order is tidied as stores are added. `kb create` refuses to clobber: it exits 2 if the directory already contains a `.kb/` store, or if the chosen name is already registered.
|
|
225
222
|
|
|
226
223
|
`kb create` also keeps a default knowledge base set. When the registry's top-level `default_kb` pointer is unset and the new store is the only registered KB, it becomes the default. When other KBs are already registered with no default, `kb create` prompts you to choose one on an interactive terminal — or, when stdin is not interactive, points you to `kb set-default`. An existing `default_kb` is never overwritten.
|
|
227
224
|
|
|
@@ -285,7 +282,7 @@ Without `--merge`, a store that already declares a taxonomy is left untouched an
|
|
|
285
282
|
|
|
286
283
|
## Error and exception model
|
|
287
284
|
|
|
288
|
-
The checks **return** findings; they never throw. Loaders (`loadKbConfig`, `loadAliases`, `loadTaxonomy`) **throw** a typed `KbLoaderError` on structural defects or malformed YAML, with the offending file path named in the message. `KbLoaderError` (exported from `@williamthorsen/kb/config`) carries a `kind: 'KbLoaderError'` discriminant — and an `isKbLoaderError` type guard — so a caller can distinguish a recoverable config or alias defect from any other throw. `loadKbRegistry` throws a plain `Error` on its own structural defects. I/O errors other than a missing optional file propagate.
|
|
285
|
+
The checks **return** findings; they never throw. Loaders (`loadKbConfig`, `loadAliases`, `loadTaxonomy`) **throw** a typed `KbLoaderError` on structural defects or malformed YAML, with the offending file path named in the message. `KbLoaderError` (exported from `@williamthorsen/kb/config`) carries a `kind: 'KbLoaderError'` discriminant — and an `isKbLoaderError` type guard — so a caller can distinguish a recoverable config or alias defect from any other throw. An underlying failure, such as a YAML parse error, is attached as the thrown error's `cause`. `loadKbRegistry` throws a plain `Error` on its own structural defects. I/O errors other than a missing optional file propagate.
|
|
289
286
|
|
|
290
287
|
## MCP wrappability
|
|
291
288
|
|
package/bin/kb.js
CHANGED
|
@@ -1,5 +1,7 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
2
|
|
|
3
|
+
// Imports only node builtins: a top-level import resolves before the gate below runs, so an
|
|
4
|
+
// unresolvable dependency would replace this file's build-first message with ERR_MODULE_NOT_FOUND.
|
|
3
5
|
import { existsSync } from 'node:fs';
|
|
4
6
|
|
|
5
7
|
// Thin wrapper so pnpm can symlink the bin at install time, before `dist/` exists.
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import { readdir, readFile } from 'node:fs/promises';
|
|
2
2
|
import { join, relative, sep } from 'node:path';
|
|
3
3
|
import process from 'node:process';
|
|
4
|
+
import { describeError } from '@williamthorsen/toolbelt.errors';
|
|
4
5
|
import { createNoteScopeMatcher } from "../config/note-scope.js";
|
|
5
6
|
import { readNoteContent } from "../note-io/read-note.js";
|
|
6
7
|
import { isGlobSegment } from "./glob-segments.js";
|
|
@@ -26,8 +27,7 @@ export async function enumerateNotes(input) {
|
|
|
26
27
|
});
|
|
27
28
|
}
|
|
28
29
|
catch (error) {
|
|
29
|
-
|
|
30
|
-
process.stderr.write(`kb: warning: could not read note ${path}; skipping: ${message}\n`);
|
|
30
|
+
process.stderr.write(`kb: warning: could not read note ${path}; skipping: ${describeError(error)}\n`);
|
|
31
31
|
}
|
|
32
32
|
}
|
|
33
33
|
return notes;
|
|
@@ -58,8 +58,7 @@ async function walk(input) {
|
|
|
58
58
|
entries = await readdir(dir, { withFileTypes: true });
|
|
59
59
|
}
|
|
60
60
|
catch (error) {
|
|
61
|
-
|
|
62
|
-
process.stderr.write(`kb: warning: could not read directory ${dir}; skipping: ${message}\n`);
|
|
61
|
+
process.stderr.write(`kb: warning: could not read directory ${dir}; skipping: ${describeError(error)}\n`);
|
|
63
62
|
return;
|
|
64
63
|
}
|
|
65
64
|
const atRoot = dir === root;
|
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { describeError } from '@williamthorsen/toolbelt.errors';
|
|
1
2
|
import { check } from "../../check/check.js";
|
|
2
3
|
import { isKbLoaderError } from "../../config/kb-loader-error.js";
|
|
3
4
|
import { formatHuman, formatJson, summarize } from "../format.js";
|
|
@@ -119,8 +120,7 @@ export function parseCheckArgs(argv) {
|
|
|
119
120
|
return { kb, json, help, patterns, vs };
|
|
120
121
|
}
|
|
121
122
|
function buildUsageError(error) {
|
|
122
|
-
|
|
123
|
-
return { exitCode: 2, stdout: '', stderr: `kb check: ${message}\n${CHECK_HELP}` };
|
|
123
|
+
return { exitCode: 2, stdout: '', stderr: `kb check: ${describeError(error)}\n${CHECK_HELP}` };
|
|
124
124
|
}
|
|
125
125
|
async function resolveSelection(input) {
|
|
126
126
|
const { options, store, result } = input;
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import type { SelectKbPrompt } from '../select-kb-prompt.js';
|
|
2
2
|
import type { CommandOutput } from './check.js';
|
|
3
|
-
export declare const CREATE_HELP = "Usage: kb create [options]\n\nScaffold a new knowledge base in the current directory and register it in the user-global kb.yaml registry.\n\nWhen the registry has no default knowledge base, the new store becomes the default.\nIf other knowledge bases are already registered, you are prompted to choose one (or set it later with \"kb set-default\").\n\nCreates:\n .kb/config.yaml check configuration (commented; defaults apply)\n .kb/tag-aliases.yaml tag-alias map (empty)\n content/, content/events/\n\nOptions:\n --name <name>
|
|
3
|
+
export declare const CREATE_HELP = "Usage: kb create [options]\n\nScaffold a new knowledge base in the current directory and register it in the user-global kb.yaml registry.\n\nRegistering leaves the registry's entries in alphabetical order, preserving its comments and formatting.\n\nWhen the registry has no default knowledge base, the new store becomes the default.\nIf other knowledge bases are already registered, you are prompted to choose one (or set it later with \"kb set-default\").\n\nCreates:\n .kb/config.yaml check configuration (commented; defaults apply)\n .kb/tag-aliases.yaml tag-alias map (empty)\n content/, content/events/\n\nOptions:\n --description <text> Description for the registry entry; cannot be combined with --no-register.\n --name <name> Registry name for the store. Defaults to the directory name.\n --no-register Scaffold without writing the kb.yaml registry entry.\n -h, --help Show this help.\n\nExit codes:\n 0 store created\n 2 usage error, an existing .kb/ in the directory, or an already-registered name\n";
|
|
4
4
|
export declare function runCreate(input: {
|
|
5
5
|
argv: readonly string[];
|
|
6
6
|
cwd: string;
|
|
@@ -8,6 +8,7 @@ export declare function runCreate(input: {
|
|
|
8
8
|
selectKb?: SelectKbPrompt;
|
|
9
9
|
}): Promise<CommandOutput>;
|
|
10
10
|
interface CreateOptions {
|
|
11
|
+
description: string | null;
|
|
11
12
|
name: string | null;
|
|
12
13
|
noRegister: boolean;
|
|
13
14
|
help: boolean;
|
|
@@ -1,11 +1,15 @@
|
|
|
1
1
|
import { homedir } from 'node:os';
|
|
2
2
|
import { join } from 'node:path';
|
|
3
|
+
import { describeError } from '@williamthorsen/toolbelt.errors';
|
|
3
4
|
import { create } from "../../create/create.js";
|
|
5
|
+
import { takeInlineValue, takeValue } from "../parse-flag-value.js";
|
|
4
6
|
import { runSetDefault } from "./set-default.js";
|
|
5
7
|
export const CREATE_HELP = `Usage: kb create [options]
|
|
6
8
|
|
|
7
9
|
Scaffold a new knowledge base in the current directory and register it in the user-global kb.yaml registry.
|
|
8
10
|
|
|
11
|
+
Registering leaves the registry's entries in alphabetical order, preserving its comments and formatting.
|
|
12
|
+
|
|
9
13
|
When the registry has no default knowledge base, the new store becomes the default.
|
|
10
14
|
If other knowledge bases are already registered, you are prompted to choose one (or set it later with "kb set-default").
|
|
11
15
|
|
|
@@ -15,9 +19,10 @@ Creates:
|
|
|
15
19
|
content/, content/events/
|
|
16
20
|
|
|
17
21
|
Options:
|
|
18
|
-
--
|
|
19
|
-
--
|
|
20
|
-
-
|
|
22
|
+
--description <text> Description for the registry entry; cannot be combined with --no-register.
|
|
23
|
+
--name <name> Registry name for the store. Defaults to the directory name.
|
|
24
|
+
--no-register Scaffold without writing the kb.yaml registry entry.
|
|
25
|
+
-h, --help Show this help.
|
|
21
26
|
|
|
22
27
|
Exit codes:
|
|
23
28
|
0 store created
|
|
@@ -38,7 +43,12 @@ export async function runCreate(input) {
|
|
|
38
43
|
const base = { targetDir: input.cwd, ...(options.name !== null && { name: options.name }) };
|
|
39
44
|
const outcome = options.noRegister
|
|
40
45
|
? await create({ ...base, register: false })
|
|
41
|
-
: await create({
|
|
46
|
+
: await create({
|
|
47
|
+
...base,
|
|
48
|
+
register: true,
|
|
49
|
+
registryPath,
|
|
50
|
+
...(options.description !== null && { description: options.description }),
|
|
51
|
+
});
|
|
42
52
|
if (!outcome.ok) {
|
|
43
53
|
return { exitCode: 2, stdout: '', stderr: `kb create: ${outcome.message}\n` };
|
|
44
54
|
}
|
|
@@ -57,6 +67,7 @@ export async function runCreate(input) {
|
|
|
57
67
|
return { exitCode: 0, stdout: summary + selection.stdout, stderr: selection.stderr };
|
|
58
68
|
}
|
|
59
69
|
export function parseCreateArgs(argv) {
|
|
70
|
+
let description = null;
|
|
60
71
|
let name = null;
|
|
61
72
|
let noRegister = false;
|
|
62
73
|
let help = false;
|
|
@@ -68,35 +79,38 @@ export function parseCreateArgs(argv) {
|
|
|
68
79
|
help = true;
|
|
69
80
|
continue;
|
|
70
81
|
}
|
|
82
|
+
if (arg === '--description') {
|
|
83
|
+
description = takeValue(argv, index, '--description');
|
|
84
|
+
index += 1;
|
|
85
|
+
continue;
|
|
86
|
+
}
|
|
87
|
+
if (arg.startsWith('--description=')) {
|
|
88
|
+
description = takeInlineValue(arg, '--description=');
|
|
89
|
+
continue;
|
|
90
|
+
}
|
|
71
91
|
if (arg === '--no-register') {
|
|
72
92
|
noRegister = true;
|
|
73
93
|
continue;
|
|
74
94
|
}
|
|
75
95
|
if (arg === '--name') {
|
|
76
|
-
|
|
77
|
-
if (next === null || next.startsWith('--')) {
|
|
78
|
-
throw new Error('--name requires a value');
|
|
79
|
-
}
|
|
80
|
-
name = next;
|
|
96
|
+
name = takeValue(argv, index, '--name');
|
|
81
97
|
index += 1;
|
|
82
98
|
continue;
|
|
83
99
|
}
|
|
84
100
|
if (arg.startsWith('--name=')) {
|
|
85
|
-
|
|
86
|
-
if (value === '') {
|
|
87
|
-
throw new Error('--name requires a value');
|
|
88
|
-
}
|
|
89
|
-
name = value;
|
|
101
|
+
name = takeInlineValue(arg, '--name=');
|
|
90
102
|
continue;
|
|
91
103
|
}
|
|
92
104
|
throw new Error(`unknown flag: ${arg}`);
|
|
93
105
|
}
|
|
94
|
-
|
|
106
|
+
if (noRegister && description !== null) {
|
|
107
|
+
throw new Error('--description cannot be combined with --no-register');
|
|
108
|
+
}
|
|
109
|
+
return { description, name, noRegister, help };
|
|
95
110
|
}
|
|
96
111
|
const UNSET_DEFAULT_HINT = 'Multiple knowledge bases are registered and no default is set. Run `kb set-default` to choose one.\n';
|
|
97
112
|
function buildUsageError(error) {
|
|
98
|
-
|
|
99
|
-
return { exitCode: 2, stdout: '', stderr: `kb create: ${message}\n${CREATE_HELP}` };
|
|
113
|
+
return { exitCode: 2, stdout: '', stderr: `kb create: ${describeError(error)}\n${CREATE_HELP}` };
|
|
100
114
|
}
|
|
101
115
|
function formatCreated(created, registryPath) {
|
|
102
116
|
const lines = [`Created knowledge base "${created.name}" at ${created.storePath}`];
|
|
@@ -104,6 +118,9 @@ function formatCreated(created, registryPath) {
|
|
|
104
118
|
lines.push(` ${path}`);
|
|
105
119
|
}
|
|
106
120
|
lines.push(created.registered ? `Registered in ${registryPath}` : 'Not registered (--no-register).');
|
|
121
|
+
if (created.description !== undefined) {
|
|
122
|
+
lines.push(`Description: ${created.description}`);
|
|
123
|
+
}
|
|
107
124
|
if (created.defaultKb === 'set') {
|
|
108
125
|
lines.push('Set as the default knowledge base.');
|
|
109
126
|
}
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import { homedir } from 'node:os';
|
|
2
2
|
import { join } from 'node:path';
|
|
3
|
+
import { describeError } from '@williamthorsen/toolbelt.errors';
|
|
3
4
|
import { tryLoadKbRegistry } from "../../discovery/load-registry.js";
|
|
4
5
|
import { clearDefaultKb, setDefaultKb } from "../../discovery/set-default-kb.js";
|
|
5
6
|
export const SET_DEFAULT_HELP = `Usage: kb set-default [name] [options]
|
|
@@ -38,11 +39,11 @@ export async function runSetDefault(input) {
|
|
|
38
39
|
if (error !== undefined) {
|
|
39
40
|
return { exitCode: 2, stdout: '', stderr: `kb set-default: ${error}\n` };
|
|
40
41
|
}
|
|
41
|
-
const entries = config.entries;
|
|
42
42
|
if (options.none) {
|
|
43
43
|
await clearDefaultKb({ registryPath });
|
|
44
44
|
return { exitCode: 0, stdout: 'The default knowledge base has been cleared.\n', stderr: '' };
|
|
45
45
|
}
|
|
46
|
+
const entries = config.entries;
|
|
46
47
|
if (options.name !== null) {
|
|
47
48
|
if (entries.length === 0) {
|
|
48
49
|
return { exitCode: 2, stdout: '', stderr: buildNoStoresMessage() };
|
|
@@ -118,6 +119,5 @@ function buildSetConfirmation(name) {
|
|
|
118
119
|
return `Default knowledge base has been set to "${name}".\n`;
|
|
119
120
|
}
|
|
120
121
|
function buildUsageError(error) {
|
|
121
|
-
|
|
122
|
-
return { exitCode: 2, stdout: '', stderr: `kb set-default: ${message}\n${SET_DEFAULT_HELP}` };
|
|
122
|
+
return { exitCode: 2, stdout: '', stderr: `kb set-default: ${describeError(error)}\n${SET_DEFAULT_HELP}` };
|
|
123
123
|
}
|
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { describeError } from '@williamthorsen/toolbelt.errors';
|
|
1
2
|
import { enumerateNotes } from "../../check/enumerate.js";
|
|
2
3
|
import { isKbLoaderError } from "../../config/kb-loader-error.js";
|
|
3
4
|
import { loadKbConfig } from "../../config/load-config.js";
|
|
@@ -34,8 +35,7 @@ export async function runTaxonomy(input) {
|
|
|
34
35
|
options = parseTaxonomyArgs(input.argv);
|
|
35
36
|
}
|
|
36
37
|
catch (error) {
|
|
37
|
-
|
|
38
|
-
return { exitCode: 2, stdout: '', stderr: `kb taxonomy: ${message}\n${TAXONOMY_HELP}` };
|
|
38
|
+
return { exitCode: 2, stdout: '', stderr: `kb taxonomy: ${describeError(error)}\n${TAXONOMY_HELP}` };
|
|
39
39
|
}
|
|
40
40
|
if (options.help) {
|
|
41
41
|
return { exitCode: 0, stdout: TAXONOMY_HELP, stderr: '' };
|
package/dist/esm/cli/index.js
CHANGED
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import process from 'node:process';
|
|
2
|
+
import { describeError } from '@williamthorsen/toolbelt.errors';
|
|
2
3
|
import { run } from "./run.js";
|
|
3
4
|
import { readlineSelectKbPrompt } from "./select-kb-prompt.js";
|
|
4
5
|
async function main() {
|
|
@@ -12,8 +13,7 @@ async function main() {
|
|
|
12
13
|
});
|
|
13
14
|
}
|
|
14
15
|
catch (error) {
|
|
15
|
-
|
|
16
|
-
process.stderr.write(`kb: unexpected error: ${message}\n`);
|
|
16
|
+
process.stderr.write(`kb: unexpected error: ${describeError(error)}\n`);
|
|
17
17
|
process.exit(2);
|
|
18
18
|
}
|
|
19
19
|
if (output.stdout !== '')
|
|
@@ -7,7 +7,7 @@ export function takeInlineValue(arg, prefix) {
|
|
|
7
7
|
}
|
|
8
8
|
export function takeValue(argv, index, flag) {
|
|
9
9
|
const next = argv[index + 1] ?? null;
|
|
10
|
-
if (next === null || next.startsWith('--')) {
|
|
10
|
+
if (next === null || next === '' || next.startsWith('--')) {
|
|
11
11
|
throw new Error(`${flag} requires a value`);
|
|
12
12
|
}
|
|
13
13
|
return next;
|
package/dist/esm/cli/run.js
CHANGED
|
@@ -12,9 +12,10 @@ Commands:
|
|
|
12
12
|
|
|
13
13
|
Run "kb <command> --help" for command options.
|
|
14
14
|
`;
|
|
15
|
+
const HELP_COMMANDS = new Set([undefined, '--help', '-h']);
|
|
15
16
|
export async function run(input) {
|
|
16
17
|
const [command, ...rest] = input.argv;
|
|
17
|
-
if (command
|
|
18
|
+
if (HELP_COMMANDS.has(command)) {
|
|
18
19
|
return { exitCode: 0, stdout: HELP, stderr: '' };
|
|
19
20
|
}
|
|
20
21
|
if (command === 'check') {
|
|
@@ -14,7 +14,7 @@ export function parseSelection(answer, kbCount) {
|
|
|
14
14
|
if (answer === '')
|
|
15
15
|
return { kind: 'cancel' };
|
|
16
16
|
const choice = Number(answer);
|
|
17
|
-
if (!Number.
|
|
17
|
+
if (!Number.isSafeInteger(choice))
|
|
18
18
|
return null;
|
|
19
19
|
if (choice >= 1 && choice <= kbCount)
|
|
20
20
|
return { kind: 'kb', index: choice - 1 };
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import { execFileSync } from 'node:child_process';
|
|
2
2
|
import { realpathSync } from 'node:fs';
|
|
3
3
|
import { join, relative, sep } from 'node:path';
|
|
4
|
+
import { describeError } from '@williamthorsen/toolbelt.errors';
|
|
4
5
|
import { isRecord } from "../../type-guards.js";
|
|
5
6
|
export function resolveChangedPaths(input) {
|
|
6
7
|
const { storeRoot, ref } = input;
|
|
@@ -37,7 +38,7 @@ function extractGitErrorMessage(error) {
|
|
|
37
38
|
if (isRecord(error) && typeof error.stderr === 'string' && error.stderr.trim() !== '') {
|
|
38
39
|
return error.stderr.trim();
|
|
39
40
|
}
|
|
40
|
-
return
|
|
41
|
+
return describeError(error);
|
|
41
42
|
}
|
|
42
43
|
function tryGit(storeRoot, args) {
|
|
43
44
|
try {
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import { readFile } from 'node:fs/promises';
|
|
2
2
|
import { join } from 'node:path';
|
|
3
|
+
import { describeError } from '@williamthorsen/toolbelt.errors';
|
|
3
4
|
import { parse } from 'yaml';
|
|
4
5
|
import { CONFIG_FILE } from "../layout/index.js";
|
|
5
6
|
import { isEnoent } from "../type-guards.js";
|
|
@@ -22,8 +23,7 @@ export async function loadKbConfig(input) {
|
|
|
22
23
|
parsed = parse(text);
|
|
23
24
|
}
|
|
24
25
|
catch (error) {
|
|
25
|
-
|
|
26
|
-
throw new KbLoaderError(`${path}: malformed YAML — ${message}`);
|
|
26
|
+
throw new KbLoaderError(`${path}: malformed YAML: ${describeError(error)}`, { cause: error });
|
|
27
27
|
}
|
|
28
28
|
const result = configFileShape.safeParse(parsed ?? {});
|
|
29
29
|
if (!result.success) {
|
|
@@ -2,6 +2,7 @@ export type DefaultKbOutcome = 'set' | 'unchanged' | 'needs-selection';
|
|
|
2
2
|
export interface CreatedStore {
|
|
3
3
|
name: string;
|
|
4
4
|
storePath: string;
|
|
5
|
+
description?: string;
|
|
5
6
|
registered: boolean;
|
|
6
7
|
created: readonly string[];
|
|
7
8
|
defaultKb?: DefaultKbOutcome;
|
|
@@ -14,6 +15,7 @@ export type CreateInput = {
|
|
|
14
15
|
} | {
|
|
15
16
|
register: true;
|
|
16
17
|
registryPath: string;
|
|
18
|
+
description?: string;
|
|
17
19
|
});
|
|
18
20
|
export type CreateOutcome = {
|
|
19
21
|
ok: true;
|
|
@@ -22,12 +22,13 @@ export async function create(input) {
|
|
|
22
22
|
return { ok: false, reason: 'name-registered', message: nameRegisteredMessage(name, registryPath) };
|
|
23
23
|
}
|
|
24
24
|
const created = await scaffold(storePath);
|
|
25
|
-
const
|
|
25
|
+
const described = input.description !== undefined && { description: input.description };
|
|
26
|
+
const result = await registerStore({ registryPath, name, storePath, ...described });
|
|
26
27
|
if (result.status === 'already-present') {
|
|
27
28
|
return { ok: false, reason: 'name-registered', message: nameRegisteredMessage(name, registryPath) };
|
|
28
29
|
}
|
|
29
30
|
const defaultKb = await ensureDefaultKb({ registryPath, name, before });
|
|
30
|
-
return { ok: true, created: { name, storePath, registered: true, created, defaultKb } };
|
|
31
|
+
return { ok: true, created: { name, storePath, ...described, registered: true, created, defaultKb } };
|
|
31
32
|
}
|
|
32
33
|
async function ensureDefaultKb(input) {
|
|
33
34
|
if (input.before.defaultKb !== undefined) {
|
|
@@ -1,6 +1,8 @@
|
|
|
1
1
|
import { readFile } from 'node:fs/promises';
|
|
2
2
|
import { homedir } from 'node:os';
|
|
3
3
|
import { dirname, isAbsolute, join, resolve } from 'node:path';
|
|
4
|
+
import { describeError } from '@williamthorsen/toolbelt.errors';
|
|
5
|
+
import { chainError } from '@williamthorsen/toolbelt.errors/candidate';
|
|
4
6
|
import { parse } from 'yaml';
|
|
5
7
|
import { isEnoent } from "../type-guards.js";
|
|
6
8
|
import { kbRegistryFileSchema } from "./kb-registry-schema.js";
|
|
@@ -36,8 +38,7 @@ export async function tryLoadKbRegistry(input = {}) {
|
|
|
36
38
|
return { config: await loadKbRegistry(input) };
|
|
37
39
|
}
|
|
38
40
|
catch (error) {
|
|
39
|
-
|
|
40
|
-
return { config: { entries: [], sources: {} }, error: message };
|
|
41
|
+
return { config: { entries: [], sources: {} }, error: describeError(error) };
|
|
41
42
|
}
|
|
42
43
|
}
|
|
43
44
|
function expandTilde(value, home) {
|
|
@@ -65,8 +66,7 @@ async function loadRegistryFile(path, source, home) {
|
|
|
65
66
|
parsed = parse(text);
|
|
66
67
|
}
|
|
67
68
|
catch (error) {
|
|
68
|
-
|
|
69
|
-
throw new Error(`${path}: malformed YAML — ${message}`);
|
|
69
|
+
throw chainError(`${path}: malformed YAML`, error);
|
|
70
70
|
}
|
|
71
71
|
if (parsed === null || parsed === undefined) {
|
|
72
72
|
return { entries: [] };
|
|
@@ -77,7 +77,8 @@ async function loadRegistryFile(path, source, home) {
|
|
|
77
77
|
}
|
|
78
78
|
const configDir = dirname(path);
|
|
79
79
|
const entries = [];
|
|
80
|
-
|
|
80
|
+
const fileEntries = Object.entries(result.data.kbs ?? {});
|
|
81
|
+
for (const [name, fileEntry] of fileEntries) {
|
|
81
82
|
entries.push({
|
|
82
83
|
name,
|
|
83
84
|
path: resolvePath(fileEntry.path, configDir, home),
|
|
@@ -96,7 +97,7 @@ function mergeEntries(userEntries, projectEntries) {
|
|
|
96
97
|
for (const entry of projectEntries) {
|
|
97
98
|
byName.set(entry.name, entry);
|
|
98
99
|
}
|
|
99
|
-
return
|
|
100
|
+
return byName.values().toArray();
|
|
100
101
|
}
|
|
101
102
|
function resolveDefaultKb(entries, name, sourcePath) {
|
|
102
103
|
if (name === undefined) {
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import { mkdir, writeFile } from 'node:fs/promises';
|
|
2
2
|
import { dirname } from 'node:path';
|
|
3
|
+
import { isMap, isScalar } from 'yaml';
|
|
3
4
|
import { kbRegistryFileSchema } from "./kb-registry-schema.js";
|
|
4
5
|
import { loadRegistryDocument } from "./registry-document.js";
|
|
5
6
|
export async function registerStore(input) {
|
|
@@ -11,11 +12,12 @@ export async function registerStore(input) {
|
|
|
11
12
|
if (doc.hasIn(['kbs', input.name])) {
|
|
12
13
|
return { status: 'already-present' };
|
|
13
14
|
}
|
|
14
|
-
const entry = {
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
}
|
|
15
|
+
const entry = {
|
|
16
|
+
...(input.description !== undefined && { description: input.description }),
|
|
17
|
+
path: input.storePath,
|
|
18
|
+
};
|
|
18
19
|
doc.setIn(['kbs', input.name], entry);
|
|
20
|
+
sortRegistryEntries(doc);
|
|
19
21
|
const result = kbRegistryFileSchema.safeParse(doc.toJS());
|
|
20
22
|
if (!result.success) {
|
|
21
23
|
throw new Error(`${input.registryPath}: cannot register "${input.name}" — ${result.error.issues[0]?.message ?? 'invalid entry'}`);
|
|
@@ -24,3 +26,20 @@ export async function registerStore(input) {
|
|
|
24
26
|
await writeFile(input.registryPath, doc.toString(), 'utf8');
|
|
25
27
|
return { status: 'added' };
|
|
26
28
|
}
|
|
29
|
+
function compareRegistryNames(left, right) {
|
|
30
|
+
const caseInsensitive = left.localeCompare(right, 'en', { sensitivity: 'base' });
|
|
31
|
+
return caseInsensitive === 0 ? left.localeCompare(right) : caseInsensitive;
|
|
32
|
+
}
|
|
33
|
+
function sortRegistryEntries(doc) {
|
|
34
|
+
const kbs = doc.getIn(['kbs'], true);
|
|
35
|
+
if (!isMap(kbs)) {
|
|
36
|
+
return;
|
|
37
|
+
}
|
|
38
|
+
kbs.items.sort((left, right) => compareRegistryNames(readName(left.key), readName(right.key)));
|
|
39
|
+
}
|
|
40
|
+
function readName(key) {
|
|
41
|
+
if (isScalar(key)) {
|
|
42
|
+
return String(key.value);
|
|
43
|
+
}
|
|
44
|
+
return typeof key === 'string' ? key : '';
|
|
45
|
+
}
|
|
@@ -91,7 +91,7 @@ function toFrontmatter(doc) {
|
|
|
91
91
|
tags = stringList(item.value);
|
|
92
92
|
break;
|
|
93
93
|
default:
|
|
94
|
-
extra[key] = key
|
|
94
|
+
extra[key] = Object.hasOwn(plainRecord, key) ? plainRecord[key] : null;
|
|
95
95
|
}
|
|
96
96
|
}
|
|
97
97
|
return { title, recordType, created, updated, tags, extra };
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import { asStringList, isValidDate } from "../note-io/field-validators.js";
|
|
2
2
|
export const EVENT_IMPACT_LEVELS = ['low', 'medium', 'high', 'critical'];
|
|
3
3
|
const IMPACT_LEVEL_SET = new Set(EVENT_IMPACT_LEVELS);
|
|
4
|
+
const NOT_SUPPLIED_VALUES = new Set([undefined, null, '']);
|
|
4
5
|
export function isEventImpact(value) {
|
|
5
6
|
return typeof value === 'string' && IMPACT_LEVEL_SET.has(value);
|
|
6
7
|
}
|
|
@@ -110,7 +111,7 @@ function readListField(value, field, errors) {
|
|
|
110
111
|
return list;
|
|
111
112
|
}
|
|
112
113
|
function readOptionalNonEmptyString(value, field, errors) {
|
|
113
|
-
if (value
|
|
114
|
+
if (NOT_SUPPLIED_VALUES.has(value)) {
|
|
114
115
|
return undefined;
|
|
115
116
|
}
|
|
116
117
|
if (typeof value === 'string') {
|
|
@@ -1,5 +1,7 @@
|
|
|
1
1
|
import { readFile } from 'node:fs/promises';
|
|
2
2
|
import { join } from 'node:path';
|
|
3
|
+
import { describeError } from '@williamthorsen/toolbelt.errors';
|
|
4
|
+
import { chainError } from '@williamthorsen/toolbelt.errors/candidate';
|
|
3
5
|
import { parse } from 'yaml';
|
|
4
6
|
import { KbLoaderError } from "../config/kb-loader-error.js";
|
|
5
7
|
import { ALIASES_FILE } from "../layout/index.js";
|
|
@@ -20,8 +22,7 @@ export async function loadAliases(input) {
|
|
|
20
22
|
return parseAliases(text, path);
|
|
21
23
|
}
|
|
22
24
|
catch (error) {
|
|
23
|
-
|
|
24
|
-
throw new KbLoaderError(message);
|
|
25
|
+
throw new KbLoaderError(describeError(error), { cause: error });
|
|
25
26
|
}
|
|
26
27
|
}
|
|
27
28
|
export function parseAliases(text, contextLabel = 'tag-aliases') {
|
|
@@ -30,8 +31,7 @@ export function parseAliases(text, contextLabel = 'tag-aliases') {
|
|
|
30
31
|
parsed = parse(text);
|
|
31
32
|
}
|
|
32
33
|
catch (error) {
|
|
33
|
-
|
|
34
|
-
throw new Error(`${contextLabel}: malformed YAML — ${message}`);
|
|
34
|
+
throw chainError(`${contextLabel}: malformed YAML`, error);
|
|
35
35
|
}
|
|
36
36
|
if (!isRecord(parsed)) {
|
|
37
37
|
throw new Error(`${contextLabel}: top-level must be a mapping`);
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import { readFile } from 'node:fs/promises';
|
|
2
2
|
import { join } from 'node:path';
|
|
3
|
+
import { describeError } from '@williamthorsen/toolbelt.errors';
|
|
3
4
|
import { parse } from 'yaml';
|
|
4
5
|
import { KbLoaderError } from "../config/kb-loader-error.js";
|
|
5
6
|
import { TAXONOMY_FILE } from "../layout/index.js";
|
|
@@ -22,8 +23,7 @@ export async function loadTaxonomy(input) {
|
|
|
22
23
|
parsed = parse(text);
|
|
23
24
|
}
|
|
24
25
|
catch (error) {
|
|
25
|
-
|
|
26
|
-
throw new KbLoaderError(`${path}: malformed YAML — ${message}`);
|
|
26
|
+
throw new KbLoaderError(`${path}: malformed YAML: ${describeError(error)}`, { cause: error });
|
|
27
27
|
}
|
|
28
28
|
const result = taxonomyFileShape.safeParse(parsed ?? {});
|
|
29
29
|
if (!result.success) {
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import { randomBytes } from 'node:crypto';
|
|
2
2
|
import { readFile, rename, unlink, writeFile } from 'node:fs/promises';
|
|
3
3
|
import { join } from 'node:path';
|
|
4
|
+
import { describeError } from '@williamthorsen/toolbelt.errors';
|
|
4
5
|
import { isMap, isPair, isScalar, parseDocument } from 'yaml';
|
|
5
6
|
import { KbLoaderError } from "../config/kb-loader-error.js";
|
|
6
7
|
import { TAXONOMY_FILE } from "../layout/index.js";
|
|
@@ -98,7 +99,7 @@ async function readDocument(path) {
|
|
|
98
99
|
const document = parseDocument(text);
|
|
99
100
|
const firstError = document.errors[0];
|
|
100
101
|
if (firstError !== undefined) {
|
|
101
|
-
throw new KbLoaderError(`${path}: malformed YAML
|
|
102
|
+
throw new KbLoaderError(`${path}: malformed YAML: ${describeError(firstError)}`, { cause: firstError });
|
|
102
103
|
}
|
|
103
104
|
if (document.contents !== null && !isMap(document.contents)) {
|
|
104
105
|
throw new KbLoaderError(`${path}: top-level must be a mapping`);
|
|
@@ -136,7 +137,11 @@ async function writeAtomic(path, content) {
|
|
|
136
137
|
await rename(tempPath, path);
|
|
137
138
|
}
|
|
138
139
|
catch (error) {
|
|
139
|
-
|
|
140
|
+
try {
|
|
141
|
+
await unlink(tempPath);
|
|
142
|
+
}
|
|
143
|
+
catch {
|
|
144
|
+
}
|
|
140
145
|
throw error;
|
|
141
146
|
}
|
|
142
147
|
}
|
|
@@ -6,7 +6,7 @@ export function checkVaultIntegrity(notes) {
|
|
|
6
6
|
}
|
|
7
7
|
function basenameFindings(vaultIndex) {
|
|
8
8
|
const findings = [];
|
|
9
|
-
for (const key of
|
|
9
|
+
for (const key of vaultIndex.keys().toArray().toSorted()) {
|
|
10
10
|
const paths = vaultIndex.get(key);
|
|
11
11
|
if (paths === undefined || paths.size < 2)
|
|
12
12
|
continue;
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@williamthorsen/kb",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.6.0",
|
|
4
4
|
"description": "Knowledge-base foundation: discovery, config, frontmatter parsing, records, tags, and vault-integrity checks",
|
|
5
5
|
"keywords": [
|
|
6
6
|
"frontmatter",
|
|
@@ -19,6 +19,7 @@
|
|
|
19
19
|
},
|
|
20
20
|
"license": "ISC",
|
|
21
21
|
"author": "William Thorsen <william@thorsen.dev> (https://github.com/williamthorsen)",
|
|
22
|
+
"sideEffects": false,
|
|
22
23
|
"type": "module",
|
|
23
24
|
"exports": {
|
|
24
25
|
".": {
|
|
@@ -96,6 +97,7 @@
|
|
|
96
97
|
"dist"
|
|
97
98
|
],
|
|
98
99
|
"dependencies": {
|
|
100
|
+
"@williamthorsen/toolbelt.errors": "0.3.0",
|
|
99
101
|
"picomatch": "4.0.5",
|
|
100
102
|
"yaml": "2.9.0",
|
|
101
103
|
"zod": "4.4.3"
|
|
@@ -109,7 +111,5 @@
|
|
|
109
111
|
"publishConfig": {
|
|
110
112
|
"access": "public"
|
|
111
113
|
},
|
|
112
|
-
"scripts": {
|
|
113
|
-
"build": "nmr compile"
|
|
114
|
-
}
|
|
114
|
+
"scripts": {}
|
|
115
115
|
}
|