@williamthorsen/kb 0.4.0 → 0.5.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 -10
- package/dist/esm/cli/commands/create.d.ts +2 -1
- package/dist/esm/cli/commands/create.js +32 -15
- package/dist/esm/cli/parse-flag-value.js +1 -1
- package/dist/esm/create/create.d.ts +2 -0
- package/dist/esm/create/create.js +3 -2
- package/dist/esm/discovery/register-store.js +23 -4
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -5,17 +5,15 @@ 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.5.0 (2026-08-08)
|
|
9
9
|
|
|
10
10
|
### 🎉 Features
|
|
11
11
|
|
|
12
|
-
-
|
|
12
|
+
- Register a new store with a description and keep the registry sorted (#1237)
|
|
13
13
|
|
|
14
|
-
|
|
14
|
+
Adds a `--description` flag to `kb create`, so that a new knowledge base can be given a description as it is created. Alphabetical ordering of keys in `kb.yaml` is now enforced on every write.
|
|
15
15
|
|
|
16
|
-
|
|
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.
|
|
16
|
+
Also fixes an issue where creating a knowledge base under an empty name could leave a stray registry entry. Across the CLI, a flag given an empty value is now refused.
|
|
19
17
|
<!-- /section:release-notes -->
|
|
20
18
|
|
|
21
19
|
## Exports
|
|
@@ -206,9 +204,10 @@ The package ships a `kb` bin with four subcommands: `check`, `create`, `set-defa
|
|
|
206
204
|
`kb create` scaffolds a new knowledge base in the current directory and registers it in the user-global `~/.agents/kb.yaml`.
|
|
207
205
|
|
|
208
206
|
```bash
|
|
209
|
-
kb create
|
|
210
|
-
kb create --name coding
|
|
211
|
-
kb create --
|
|
207
|
+
kb create # scaffold the current directory, register under its name
|
|
208
|
+
kb create --name coding # register under an explicit name
|
|
209
|
+
kb create --description "Coding notes" # describe the registry entry
|
|
210
|
+
kb create --no-register # scaffold without writing the registry
|
|
212
211
|
```
|
|
213
212
|
|
|
214
213
|
It creates these files and directories:
|
|
@@ -221,7 +220,7 @@ It creates these files and directories:
|
|
|
221
220
|
|
|
222
221
|
The config seed is serialized from the in-package `defaultKbConfig`, so a new store cannot drift from the bundled default.
|
|
223
222
|
|
|
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
|
|
223
|
+
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
224
|
|
|
226
225
|
`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
226
|
|
|
@@ -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,14 @@
|
|
|
1
1
|
import { homedir } from 'node:os';
|
|
2
2
|
import { join } from 'node:path';
|
|
3
3
|
import { create } from "../../create/create.js";
|
|
4
|
+
import { takeInlineValue, takeValue } from "../parse-flag-value.js";
|
|
4
5
|
import { runSetDefault } from "./set-default.js";
|
|
5
6
|
export const CREATE_HELP = `Usage: kb create [options]
|
|
6
7
|
|
|
7
8
|
Scaffold a new knowledge base in the current directory and register it in the user-global kb.yaml registry.
|
|
8
9
|
|
|
10
|
+
Registering leaves the registry's entries in alphabetical order, preserving its comments and formatting.
|
|
11
|
+
|
|
9
12
|
When the registry has no default knowledge base, the new store becomes the default.
|
|
10
13
|
If other knowledge bases are already registered, you are prompted to choose one (or set it later with "kb set-default").
|
|
11
14
|
|
|
@@ -15,9 +18,10 @@ Creates:
|
|
|
15
18
|
content/, content/events/
|
|
16
19
|
|
|
17
20
|
Options:
|
|
18
|
-
--
|
|
19
|
-
--
|
|
20
|
-
-
|
|
21
|
+
--description <text> Description for the registry entry; cannot be combined with --no-register.
|
|
22
|
+
--name <name> Registry name for the store. Defaults to the directory name.
|
|
23
|
+
--no-register Scaffold without writing the kb.yaml registry entry.
|
|
24
|
+
-h, --help Show this help.
|
|
21
25
|
|
|
22
26
|
Exit codes:
|
|
23
27
|
0 store created
|
|
@@ -38,7 +42,12 @@ export async function runCreate(input) {
|
|
|
38
42
|
const base = { targetDir: input.cwd, ...(options.name !== null && { name: options.name }) };
|
|
39
43
|
const outcome = options.noRegister
|
|
40
44
|
? await create({ ...base, register: false })
|
|
41
|
-
: await create({
|
|
45
|
+
: await create({
|
|
46
|
+
...base,
|
|
47
|
+
register: true,
|
|
48
|
+
registryPath,
|
|
49
|
+
...(options.description !== null && { description: options.description }),
|
|
50
|
+
});
|
|
42
51
|
if (!outcome.ok) {
|
|
43
52
|
return { exitCode: 2, stdout: '', stderr: `kb create: ${outcome.message}\n` };
|
|
44
53
|
}
|
|
@@ -57,6 +66,7 @@ export async function runCreate(input) {
|
|
|
57
66
|
return { exitCode: 0, stdout: summary + selection.stdout, stderr: selection.stderr };
|
|
58
67
|
}
|
|
59
68
|
export function parseCreateArgs(argv) {
|
|
69
|
+
let description = null;
|
|
60
70
|
let name = null;
|
|
61
71
|
let noRegister = false;
|
|
62
72
|
let help = false;
|
|
@@ -68,30 +78,34 @@ export function parseCreateArgs(argv) {
|
|
|
68
78
|
help = true;
|
|
69
79
|
continue;
|
|
70
80
|
}
|
|
81
|
+
if (arg === '--description') {
|
|
82
|
+
description = takeValue(argv, index, '--description');
|
|
83
|
+
index += 1;
|
|
84
|
+
continue;
|
|
85
|
+
}
|
|
86
|
+
if (arg.startsWith('--description=')) {
|
|
87
|
+
description = takeInlineValue(arg, '--description=');
|
|
88
|
+
continue;
|
|
89
|
+
}
|
|
71
90
|
if (arg === '--no-register') {
|
|
72
91
|
noRegister = true;
|
|
73
92
|
continue;
|
|
74
93
|
}
|
|
75
94
|
if (arg === '--name') {
|
|
76
|
-
|
|
77
|
-
if (next === null || next.startsWith('--')) {
|
|
78
|
-
throw new Error('--name requires a value');
|
|
79
|
-
}
|
|
80
|
-
name = next;
|
|
95
|
+
name = takeValue(argv, index, '--name');
|
|
81
96
|
index += 1;
|
|
82
97
|
continue;
|
|
83
98
|
}
|
|
84
99
|
if (arg.startsWith('--name=')) {
|
|
85
|
-
|
|
86
|
-
if (value === '') {
|
|
87
|
-
throw new Error('--name requires a value');
|
|
88
|
-
}
|
|
89
|
-
name = value;
|
|
100
|
+
name = takeInlineValue(arg, '--name=');
|
|
90
101
|
continue;
|
|
91
102
|
}
|
|
92
103
|
throw new Error(`unknown flag: ${arg}`);
|
|
93
104
|
}
|
|
94
|
-
|
|
105
|
+
if (noRegister && description !== null) {
|
|
106
|
+
throw new Error('--description cannot be combined with --no-register');
|
|
107
|
+
}
|
|
108
|
+
return { description, name, noRegister, help };
|
|
95
109
|
}
|
|
96
110
|
const UNSET_DEFAULT_HINT = 'Multiple knowledge bases are registered and no default is set. Run `kb set-default` to choose one.\n';
|
|
97
111
|
function buildUsageError(error) {
|
|
@@ -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
|
}
|
|
@@ -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;
|
|
@@ -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,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
|
+
}
|
package/package.json
CHANGED