@williamthorsen/kb 0.6.2 → 0.8.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.
Files changed (88) hide show
  1. package/README.md +161 -27
  2. package/dist/esm/check/check.d.ts +34 -0
  3. package/dist/esm/check/check.js +41 -1
  4. package/dist/esm/check/enumerate.d.ts +37 -0
  5. package/dist/esm/check/enumerate.js +12 -4
  6. package/dist/esm/check/glob-segments.d.ts +6 -0
  7. package/dist/esm/check/resolve-foreign-stores.d.ts +24 -0
  8. package/dist/esm/check/resolve-foreign-stores.js +50 -0
  9. package/dist/esm/cli/commands/check.d.ts +23 -1
  10. package/dist/esm/cli/commands/check.js +7 -2
  11. package/dist/esm/cli/commands/create.d.ts +19 -1
  12. package/dist/esm/cli/commands/create.js +2 -0
  13. package/dist/esm/cli/commands/scaffold.d.ts +30 -0
  14. package/dist/esm/cli/commands/scaffold.js +108 -0
  15. package/dist/esm/cli/commands/set-default.d.ts +15 -0
  16. package/dist/esm/cli/commands/taxonomy.d.ts +18 -0
  17. package/dist/esm/cli/format.d.ts +21 -0
  18. package/dist/esm/cli/parse-flag-value.d.ts +5 -0
  19. package/dist/esm/cli/resolve-store.d.ts +12 -0
  20. package/dist/esm/cli/run.d.ts +9 -1
  21. package/dist/esm/cli/run.js +5 -0
  22. package/dist/esm/cli/select-kb-prompt.d.ts +18 -0
  23. package/dist/esm/cli/targeting/resolve-changed-paths.d.ts +10 -0
  24. package/dist/esm/cli/targeting/resolve-changed-paths.js +7 -33
  25. package/dist/esm/cli/targeting/select-notes.d.ts +13 -0
  26. package/dist/esm/cli/targeting/select-notes.js +4 -3
  27. package/dist/esm/config/config-schema.d.ts +30 -1
  28. package/dist/esm/config/config-schema.js +9 -0
  29. package/dist/esm/config/index.d.ts +1 -1
  30. package/dist/esm/config/index.js +1 -1
  31. package/dist/esm/config/kb-loader-error.d.ts +8 -0
  32. package/dist/esm/config/load-config.d.ts +7 -0
  33. package/dist/esm/config/load-config.js +1 -0
  34. package/dist/esm/config/note-scope.d.ts +13 -0
  35. package/dist/esm/create/create.d.ts +25 -0
  36. package/dist/esm/create/create.js +8 -12
  37. package/dist/esm/create/index.d.ts +0 -1
  38. package/dist/esm/create/index.js +0 -1
  39. package/dist/esm/discovery/find-kb-root.d.ts +4 -0
  40. package/dist/esm/discovery/kb-registry-schema.d.ts +4 -0
  41. package/dist/esm/discovery/load-registry.d.ts +23 -0
  42. package/dist/esm/discovery/register-store.d.ts +11 -0
  43. package/dist/esm/discovery/registry-document.d.ts +5 -0
  44. package/dist/esm/discovery/set-default-kb.d.ts +12 -0
  45. package/dist/esm/filesystem/exists.d.ts +13 -0
  46. package/dist/esm/filesystem/write-atomic.d.ts +5 -0
  47. package/dist/esm/frontmatter/parse-note.d.ts +9 -0
  48. package/dist/esm/git/list-git-scope.d.ts +15 -0
  49. package/dist/esm/git/list-git-scope.js +22 -0
  50. package/dist/esm/git/run-git.d.ts +17 -0
  51. package/dist/esm/git/run-git.js +22 -0
  52. package/dist/esm/layout/index.d.ts +1 -1
  53. package/dist/esm/layout/index.js +1 -1
  54. package/dist/esm/layout/store-layout.d.ts +20 -0
  55. package/dist/esm/layout/store-layout.js +2 -0
  56. package/dist/esm/lints/paths.d.ts +7 -0
  57. package/dist/esm/lints/tag-alias.d.ts +6 -0
  58. package/dist/esm/lints/taxonomy.d.ts +15 -0
  59. package/dist/esm/note-io/field-validators.d.ts +9 -0
  60. package/dist/esm/note-io/read-note.d.ts +7 -0
  61. package/dist/esm/note-io/write-note.d.ts +9 -0
  62. package/dist/esm/note-io/yaml-fields.d.ts +5 -0
  63. package/dist/esm/records/assertion.d.ts +4 -0
  64. package/dist/esm/records/event.d.ts +12 -0
  65. package/dist/esm/scaffold/index.d.ts +2 -0
  66. package/dist/esm/scaffold/index.js +2 -0
  67. package/dist/esm/scaffold/render-seeds.d.ts +16 -0
  68. package/dist/esm/scaffold/render-seeds.js +65 -0
  69. package/dist/esm/scaffold/scaffold.d.ts +23 -0
  70. package/dist/esm/scaffold/scaffold.js +35 -0
  71. package/dist/esm/tags/canonicalize.d.ts +8 -0
  72. package/dist/esm/tags/load-aliases.d.ts +10 -0
  73. package/dist/esm/taxonomy/domain-paths.d.ts +10 -0
  74. package/dist/esm/taxonomy/load-taxonomy.d.ts +8 -0
  75. package/dist/esm/taxonomy/taxonomy-schema.d.ts +19 -0
  76. package/dist/esm/taxonomy/write-taxonomy.d.ts +20 -0
  77. package/dist/esm/type-guards.d.ts +13 -0
  78. package/dist/esm/types.d.ts +41 -0
  79. package/dist/esm/vault-integrity/build-vault-index.d.ts +11 -0
  80. package/dist/esm/vault-integrity/check-vault-integrity.d.ts +48 -1
  81. package/dist/esm/vault-integrity/check-vault-integrity.js +59 -20
  82. package/dist/esm/vault-integrity/index.d.ts +2 -2
  83. package/dist/esm/vault-integrity/index.js +2 -2
  84. package/dist/esm/vault-integrity/wikilink-parse.d.ts +47 -4
  85. package/dist/esm/vault-integrity/wikilink-parse.js +37 -9
  86. package/package.json +10 -4
  87. package/dist/esm/create/render-seeds.d.ts +0 -2
  88. package/dist/esm/create/render-seeds.js +0 -29
@@ -1,20 +1,42 @@
1
+ /** The outcome of a command run: the exit code plus the streams to write. */
1
2
  export interface CommandOutput {
3
+ /** Process exit code: 0 clean, 1 error-severity findings, 2 usage/config error. */
2
4
  exitCode: 0 | 1 | 2;
3
5
  stdout: string;
4
6
  stderr: string;
5
7
  }
6
- export declare const CHECK_HELP = "Usage: kb check [paths...] [options]\n\nValidate notes in a knowledge base against its tag aliases and cross-note\nlink and path rules. With no path arguments, every note is checked.\nCross-note rules always resolve against the whole vault.\n\nTargeting (mutually exclusive):\n [paths...] Check only the notes matching the given glob patterns, files,\n or directories. Quote globs so kb expands them itself. A\n directory checks every note beneath it. A path that matches no\n note is a usage error unless it names a real non-note.\n --vs <ref> Check only the notes changed between the working tree and the\n merge-base of <ref> and HEAD: follows renames, includes\n uncommitted edits, excludes deletions.\n\nOptions:\n --kb <name> Check the named store from the kb.yaml registry. Without it,\n the nearest ancestor .kb/ directory is used.\n --json Emit a JSON report instead of human-readable output.\n -h, --help Show this help.\n\nExit codes:\n 0 no error-severity findings in the checked notes (warnings allowed)\n 1 one or more error-severity findings in the checked notes\n 2 usage error, unresolvable store or --vs ref, a path matching no note, or\n malformed config or aliases\n";
8
+ /** Usage text for `kb check`. */
9
+ export declare const CHECK_HELP = "Usage: kb check [paths...] [options]\n\nValidate notes in a knowledge base against its tag aliases and cross-note\nlink and path rules. With no path arguments, every note is checked.\nCross-note rules always resolve against the whole store; a [[store:Target]]\nlink resolves against the store its prefix names in the kb.yaml registry.\n\nTargeting (mutually exclusive):\n [paths...] Check only the notes matching the given glob patterns, files,\n or directories. Quote globs so kb expands them itself. A\n directory checks every note beneath it. A path that matches no\n note is a usage error unless it names a real non-note.\n --vs <ref> Check only the notes changed between the working tree and the\n merge-base of <ref> and HEAD: follows renames, includes\n uncommitted edits, excludes deletions.\n\nOptions:\n --kb <name> Check the named store from the kb.yaml registry. Without it,\n the nearest ancestor .kb/ directory is used.\n --json Emit a JSON report instead of human-readable output.\n -h, --help Show this help.\n\nExit codes:\n 0 no error-severity findings in the checked notes (warnings allowed)\n 1 one or more error-severity findings in the checked notes\n 2 usage error, unresolvable store or --vs ref, a path matching no note, or\n malformed config or aliases\n";
10
+ /**
11
+ * Runs `kb check`: parses options, resolves the store, runs the shared `check`, and formats the report.
12
+ *
13
+ * Store resolution composes the package's own exports inline — `findKbRoot` for the default ancestor-walk and
14
+ * `tryLoadKbRegistry` for an explicit `--kb <name>`. The lookup is read-only, so a store's registry `readonly` flag
15
+ * is ignored. A malformed `.kb/config.yaml`/`tag-aliases.yaml` surfaces as a `KbLoaderError` from `check`, which maps
16
+ * to exit 2; any other error from `check` propagates to the caller as a real crash.
17
+ */
7
18
  export declare function runCheck(input: {
8
19
  argv: readonly string[];
9
20
  cwd: string;
10
21
  home?: string;
11
22
  }): Promise<CommandOutput>;
23
+ /** Parsed `kb check` options. */
12
24
  interface CheckOptions {
25
+ /** Explicit store name from `--kb`, or `null` for ancestor-walk discovery. */
13
26
  kb: string | null;
27
+ /** Whether `--json` was supplied. */
14
28
  json: boolean;
29
+ /** Whether `--help`/`-h` was supplied. */
15
30
  help: boolean;
31
+ /** Positional glob/path/directory arguments selecting which notes to check; empty for a whole-vault run. */
16
32
  patterns: string[];
33
+ /** The `--vs` ref to diff against, or `null` when not supplied. */
17
34
  vs: string | null;
18
35
  }
36
+ /**
37
+ * Parses `kb check` options. `--kb` and `--vs` each accept both the space (`--kb x`) and equals (`--kb=x`) forms;
38
+ * non-flag arguments are collected as selection patterns. Unknown flags, a missing `--kb`/`--vs` value, or combining
39
+ * patterns with `--vs` throw with a usage-style message.
40
+ */
19
41
  export declare function parseCheckArgs(argv: readonly string[]): CheckOptions;
20
42
  export {};
@@ -10,7 +10,8 @@ export const CHECK_HELP = `Usage: kb check [paths...] [options]
10
10
 
11
11
  Validate notes in a knowledge base against its tag aliases and cross-note
12
12
  link and path rules. With no path arguments, every note is checked.
13
- Cross-note rules always resolve against the whole vault.
13
+ Cross-note rules always resolve against the whole store; a [[store:Target]]
14
+ link resolves against the store its prefix names in the kb.yaml registry.
14
15
 
15
16
  Targeting (mutually exclusive):
16
17
  [paths...] Check only the notes matching the given glob patterns, files,
@@ -55,7 +56,11 @@ export async function runCheck(input) {
55
56
  const store = resolved.store;
56
57
  let result;
57
58
  try {
58
- result = await check({ kbRoot: store.path });
59
+ result = await check({
60
+ kbRoot: store.path,
61
+ cwd: input.cwd,
62
+ ...(input.home !== undefined && { home: input.home }),
63
+ });
59
64
  }
60
65
  catch (error) {
61
66
  if (isKbLoaderError(error)) {
@@ -1,17 +1,35 @@
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\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";
3
+ /** Usage text for `kb create`. */
4
+ 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 .editorconfig editor and formatter settings (width, indent, line endings)\n .kb/config.yaml check configuration (commented; defaults apply)\n .kb/tag-aliases.yaml tag-alias map (empty)\n .prettierrc.yaml formatting configuration (commented)\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";
5
+ /**
6
+ * Runs `kb create`: parses options, scaffolds a store in `cwd`, and (unless `--no-register`) registers it in the
7
+ * user-global `~/.agents/kb.yaml`. After registering, it ensures a default knowledge base: the new store becomes the
8
+ * default when none is set and it is the only KB; when other KBs already exist with no default, it delegates to
9
+ * `kb set-default`'s picker (or, with no `selectKb` on a non-interactive stdin, points the user there). A precondition
10
+ * failure from `create` — an existing `.kb/` or an already-registered name — maps to exit 2; a genuine I/O error
11
+ * propagates to the caller.
12
+ */
4
13
  export declare function runCreate(input: {
5
14
  argv: readonly string[];
6
15
  cwd: string;
7
16
  home?: string;
8
17
  selectKb?: SelectKbPrompt;
9
18
  }): Promise<CommandOutput>;
19
+ /** Parsed `kb create` options. */
10
20
  interface CreateOptions {
21
+ /** Description from `--description`, or `null` to write the entry without one. */
11
22
  description: string | null;
23
+ /** Explicit registry name from `--name`, or `null` to default to the directory name. */
12
24
  name: string | null;
25
+ /** Whether `--no-register` was supplied. */
13
26
  noRegister: boolean;
27
+ /** Whether `--help`/`-h` was supplied. */
14
28
  help: boolean;
15
29
  }
30
+ /**
31
+ * Parses `kb create` options. Each value-taking flag accepts both the space form and the equals form. Unknown flags, a
32
+ * missing value, or a description supplied alongside `--no-register` throw with a usage-style message.
33
+ */
16
34
  export declare function parseCreateArgs(argv: readonly string[]): CreateOptions;
17
35
  export {};
@@ -14,8 +14,10 @@ When the registry has no default knowledge base, the new store becomes the defau
14
14
  If other knowledge bases are already registered, you are prompted to choose one (or set it later with "kb set-default").
15
15
 
16
16
  Creates:
17
+ .editorconfig editor and formatter settings (width, indent, line endings)
17
18
  .kb/config.yaml check configuration (commented; defaults apply)
18
19
  .kb/tag-aliases.yaml tag-alias map (empty)
20
+ .prettierrc.yaml formatting configuration (commented)
19
21
  content/, content/events/
20
22
 
21
23
  Options:
@@ -0,0 +1,30 @@
1
+ import type { CommandOutput } from './check.js';
2
+ /** Usage text for `kb scaffold`. */
3
+ export declare const SCAFFOLD_HELP = "Usage: kb scaffold [options]\n\nWrite into an existing knowledge base any canonical file that it lacks, so a store\ncreated before a given file existed can acquire it. An existing file is left\nuntouched unless --force is given.\n\nWrites:\n .editorconfig editor and formatter settings (width, indent, line endings)\n .kb/config.yaml check configuration (commented; defaults apply)\n .kb/tag-aliases.yaml tag-alias map (empty)\n .prettierrc.yaml formatting configuration (commented)\n content/, content/events/\n\n.kb/taxonomy.yaml is not part of this set: \"kb taxonomy init\" derives it from\nthe notes that the store already holds. Use \"kb create\" to make a new store.\n\nOptions:\n --force Replace an existing canonical file with a fresh seed. A\n directory has no content to replace and is left as it is.\n --kb <name> Use the named store from the kb.yaml registry. Without it, the\n nearest ancestor .kb/ directory is used.\n -h, --help Show this help.\n\nExit codes:\n 0 every canonical file is present, whether it was written or already there\n 2 usage error, unresolvable store, a store marked readonly in kb.yaml, or a\n resolved path holding no .kb/\n";
4
+ /**
5
+ * Runs `kb scaffold`: parses options, resolves the store, and writes the canonical files it lacks.
6
+ *
7
+ * Store resolution matches `kb check`. A store the registry marks `readonly` is refused, as `kb taxonomy init` does.
8
+ * A resolved path holding no `.kb/` is refused too: the command back-fills a store rather than creating one, and a
9
+ * registry entry names a path without proving a store is there.
10
+ */
11
+ export declare function runScaffold(input: {
12
+ argv: readonly string[];
13
+ cwd: string;
14
+ home?: string;
15
+ }): Promise<CommandOutput>;
16
+ /** Parsed `kb scaffold` options. */
17
+ interface ScaffoldOptions {
18
+ /** Whether `--force` was supplied. */
19
+ force: boolean;
20
+ /** Whether `--help`/`-h` was supplied. */
21
+ help: boolean;
22
+ /** Explicit store name from `--kb`, or `null` for ancestor-walk discovery. */
23
+ kb: string | null;
24
+ }
25
+ /**
26
+ * Parses `kb scaffold` options. `--kb` accepts both the space (`--kb x`) and equals (`--kb=x`) forms. An unknown flag,
27
+ * a positional argument, or a missing `--kb` value throws with a usage-style message.
28
+ */
29
+ export declare function parseScaffoldArgs(argv: readonly string[]): ScaffoldOptions;
30
+ export {};
@@ -0,0 +1,108 @@
1
+ import { describeError } from '@williamthorsen/toolbelt.errors';
2
+ import { directoryExists } from "../../filesystem/exists.js";
3
+ import { KB_DIR, resolveKbDir } from "../../layout/index.js";
4
+ import { scaffold } from "../../scaffold/scaffold.js";
5
+ import { takeInlineValue, takeValue } from "../parse-flag-value.js";
6
+ import { resolveStore } from "../resolve-store.js";
7
+ export const SCAFFOLD_HELP = `Usage: kb scaffold [options]
8
+
9
+ Write into an existing knowledge base any canonical file that it lacks, so a store
10
+ created before a given file existed can acquire it. An existing file is left
11
+ untouched unless --force is given.
12
+
13
+ Writes:
14
+ .editorconfig editor and formatter settings (width, indent, line endings)
15
+ .kb/config.yaml check configuration (commented; defaults apply)
16
+ .kb/tag-aliases.yaml tag-alias map (empty)
17
+ .prettierrc.yaml formatting configuration (commented)
18
+ content/, content/events/
19
+
20
+ .kb/taxonomy.yaml is not part of this set: "kb taxonomy init" derives it from
21
+ the notes that the store already holds. Use "kb create" to make a new store.
22
+
23
+ Options:
24
+ --force Replace an existing canonical file with a fresh seed. A
25
+ directory has no content to replace and is left as it is.
26
+ --kb <name> Use the named store from the kb.yaml registry. Without it, the
27
+ nearest ancestor .kb/ directory is used.
28
+ -h, --help Show this help.
29
+
30
+ Exit codes:
31
+ 0 every canonical file is present, whether it was written or already there
32
+ 2 usage error, unresolvable store, a store marked readonly in kb.yaml, or a
33
+ resolved path holding no ${KB_DIR}/
34
+ `;
35
+ export async function runScaffold(input) {
36
+ let options;
37
+ try {
38
+ options = parseScaffoldArgs(input.argv);
39
+ }
40
+ catch (error) {
41
+ return { exitCode: 2, stdout: '', stderr: `kb scaffold: ${describeError(error)}\n${SCAFFOLD_HELP}` };
42
+ }
43
+ if (options.help) {
44
+ return { exitCode: 0, stdout: SCAFFOLD_HELP, stderr: '' };
45
+ }
46
+ const resolved = await resolveStore({
47
+ explicitKb: options.kb,
48
+ cwd: input.cwd,
49
+ ...(input.home !== undefined && { home: input.home }),
50
+ });
51
+ if (!resolved.ok) {
52
+ return { exitCode: 2, stdout: '', stderr: `kb scaffold: ${resolved.message}\n` };
53
+ }
54
+ const store = resolved.store;
55
+ const label = store.name ?? store.path;
56
+ if (resolved.readonly) {
57
+ return buildRefusal(`knowledge base "${label}" is marked readonly in kb.yaml; scaffold is refused`);
58
+ }
59
+ if (!(await directoryExists(resolveKbDir(store.path)))) {
60
+ return buildRefusal(`no ${KB_DIR}/ directory at ${store.path}; run "kb create" to make a store there`);
61
+ }
62
+ const entries = await scaffold({ storePath: store.path, force: options.force });
63
+ return { exitCode: 0, stdout: formatScaffolded(store, entries), stderr: '' };
64
+ }
65
+ export function parseScaffoldArgs(argv) {
66
+ let force = false;
67
+ let help = false;
68
+ let kb = null;
69
+ for (let index = 0; index < argv.length; index += 1) {
70
+ const arg = argv[index];
71
+ if (arg === undefined)
72
+ continue;
73
+ if (arg === '--help' || arg === '-h') {
74
+ help = true;
75
+ continue;
76
+ }
77
+ if (arg === '--force') {
78
+ force = true;
79
+ continue;
80
+ }
81
+ if (arg === '--kb') {
82
+ kb = takeValue(argv, index, '--kb');
83
+ index += 1;
84
+ continue;
85
+ }
86
+ if (arg.startsWith('--kb=')) {
87
+ kb = takeInlineValue(arg, '--kb=');
88
+ continue;
89
+ }
90
+ if (arg.startsWith('-')) {
91
+ throw new Error(`unknown flag: ${arg}`);
92
+ }
93
+ throw new Error(`unexpected argument: ${arg}`);
94
+ }
95
+ return { force, help, kb };
96
+ }
97
+ const ACTION_WIDTH = 8;
98
+ function buildRefusal(message) {
99
+ return { exitCode: 2, stdout: '', stderr: `kb scaffold: ${message}\n` };
100
+ }
101
+ function formatScaffolded(store, entries) {
102
+ const named = store.name === null ? '' : ` "${store.name}"`;
103
+ const lines = [`Scaffolded knowledge base${named} at ${store.path}`];
104
+ for (const entry of entries) {
105
+ lines.push(` ${entry.action.padEnd(ACTION_WIDTH)} ${entry.path}`);
106
+ }
107
+ return `${lines.join('\n')}\n`;
108
+ }
@@ -1,15 +1,30 @@
1
1
  import type { SelectKbPrompt } from '../select-kb-prompt.js';
2
2
  import type { CommandOutput } from './check.js';
3
+ /** Usage text for `kb set-default`. */
3
4
  export declare const SET_DEFAULT_HELP = "Usage: kb set-default [name] [options]\n\nSet, clear, or interactively choose the user-global default knowledge base \u2014\nthe top-level default_kb pointer in ~/.agents/kb.yaml.\n\n kb set-default <name> Set default_kb to the named registered KB.\n kb set-default --none Clear default_kb.\n kb set-default List the registered KBs and prompt for a choice\n (including a \"(none)\" option to clear).\n\nOptions:\n --none Clear the default knowledge base.\n -h, --help Show this help.\n\nExit codes:\n 0 default set or cleared, or the selection was cancelled\n 2 usage error, an unregistered name, no registered KBs, or no name supplied\n when stdin is not interactive\n";
5
+ /**
6
+ * Runs `kb set-default`: sets, clears, or interactively selects the user-global `default_kb`. Resolution reads the
7
+ * user-global registry only (no project overlay), so the chosen default resolves in every project context. The picker
8
+ * is injected via `selectKb`, keeping the dispatcher free of terminal I/O; its absence means stdin is non-interactive,
9
+ * making the no-argument form a usage error rather than a hang.
10
+ */
4
11
  export declare function runSetDefault(input: {
5
12
  argv: readonly string[];
6
13
  home?: string;
7
14
  selectKb?: SelectKbPrompt;
8
15
  }): Promise<CommandOutput>;
16
+ /** Parsed `kb set-default` options. */
9
17
  interface SetDefaultOptions {
18
+ /** The positional KB name, or `null` when none was supplied (interactive form). */
10
19
  name: string | null;
20
+ /** Whether `--none` was supplied. */
11
21
  none: boolean;
22
+ /** Whether `--help`/`-h` was supplied. */
12
23
  help: boolean;
13
24
  }
25
+ /**
26
+ * Parses `kb set-default` options. At most one positional name is accepted; `--none` clears. A name combined with
27
+ * `--none`, a second positional, or an unknown flag throws with a usage-style message.
28
+ */
14
29
  export declare function parseSetDefaultArgs(argv: readonly string[]): SetDefaultOptions;
15
30
  export {};
@@ -1,15 +1,33 @@
1
1
  import type { CommandOutput } from './check.js';
2
+ /** Usage text for `kb taxonomy`. */
2
3
  export declare const TAXONOMY_HELP = "Usage: kb taxonomy init [options]\n\nDerive a starting taxonomy from the notes a knowledge base already holds, so a\ntaxonomy can be introduced to a populated store without every folder reporting\nas undeclared. Every folder holding notes is declared, along with each of its\nancestors, under \"provisional:\" with no description: the command cannot invent\ndescriptions, and provisional already means \"declared, not yet reviewed\".\n\nOptions:\n --kb <name> Use the named store from the kb.yaml registry. Without it, the\n nearest ancestor .kb/ directory is used.\n --merge Add only the domains an existing taxonomy does not declare.\n Without it, a store that already has a taxonomy is left\n untouched.\n -h, --help Show this help.\n\nExit codes:\n 0 the taxonomy was written, or already declared every derived domain\n 2 usage error, unresolvable store, a store marked readonly in kb.yaml,\n malformed config or taxonomy, or an existing taxonomy without --merge\n";
4
+ /**
5
+ * Runs `kb taxonomy`: parses options, resolves the store, derives the domains its notes imply, and declares them.
6
+ *
7
+ * The derivation reads the same enumeration `kb check` does, so a store back-filled by this command reports no
8
+ * taxonomy drift. A store the registry marks `readonly` is refused, matching `kb-curate --apply`. A malformed
9
+ * `.kb/config.yaml` or `.kb/taxonomy.yaml` surfaces as a `KbLoaderError` and maps to exit 2; any other error
10
+ * propagates to the caller as a real crash.
11
+ */
3
12
  export declare function runTaxonomy(input: {
4
13
  argv: readonly string[];
5
14
  cwd: string;
6
15
  home?: string;
7
16
  }): Promise<CommandOutput>;
17
+ /** Parsed `kb taxonomy` options. */
8
18
  interface TaxonomyOptions {
19
+ /** The subcommand to run, or `null` when none was given. */
9
20
  subcommand: 'init' | null;
21
+ /** Explicit store name from `--kb`, or `null` for ancestor-walk discovery. */
10
22
  kb: string | null;
23
+ /** Whether `--merge` was supplied. */
11
24
  merge: boolean;
25
+ /** Whether `--help`/`-h` was supplied. */
12
26
  help: boolean;
13
27
  }
28
+ /**
29
+ * Parses `kb taxonomy` options. `--kb` accepts both the space (`--kb x`) and equals (`--kb=x`) forms. An unknown flag,
30
+ * an unknown subcommand, a missing `--kb` value, or a second subcommand throws with a usage-style message.
31
+ */
14
32
  export declare function parseTaxonomyArgs(argv: readonly string[]): TaxonomyOptions;
15
33
  export {};
@@ -1,24 +1,45 @@
1
1
  import type { Finding } from '../types.js';
2
+ /** A resolved store's identity, surfaced in both human and JSON output. */
2
3
  export interface StoreRef {
4
+ /** The store's display name, or `null` for a `.kb/`-discovered store with no registry entry. */
3
5
  name: string | null;
6
+ /** Absolute path to the store root. */
4
7
  path: string;
5
8
  }
9
+ /** Severity-partitioned counts over a finding set, plus the note total checked. */
6
10
  export interface CheckSummary {
11
+ /** Notes enumerated and checked. */
7
12
  notes: number;
13
+ /** Total findings. */
8
14
  total: number;
15
+ /** Findings with `severity: 'error'`. */
9
16
  errors: number;
17
+ /** Findings with `severity: 'warning'`. */
10
18
  warnings: number;
11
19
  }
20
+ /** Which selection produced a report, controlling the wording of the zero-match line. */
12
21
  export type CheckScope = 'vault' | 'patterns' | 'vs';
22
+ /**
23
+ * Renders the default human output. Findings are grouped by file in path order, each line reading
24
+ * `<severity> <rule> (line N): message`. A clean run (notes checked, no findings) prints `✓ no findings (N notes
25
+ * checked)`; a run that checked nothing prints a zero-match line worded for its `scope` (naming the config targets for
26
+ * a whole-vault run, and a scope-appropriate line for a targeted one) without the `✓`, since no check ran.
27
+ *
28
+ * A run can check no notes and still carry vault-scoped findings, which describe the store rather than any note. The
29
+ * zero-match line then heads the report instead of replacing it: it explains why no note was checked, and the findings
30
+ * follow.
31
+ */
13
32
  export declare function formatHuman(input: {
14
33
  summary: CheckSummary;
15
34
  findings: readonly Finding[];
16
35
  targets: readonly string[];
17
36
  scope: CheckScope;
18
37
  }): string;
38
+ /** Renders the `--json` payload: store identity, summary counts, and the raw findings. */
19
39
  export declare function formatJson(input: {
20
40
  store: StoreRef;
21
41
  summary: CheckSummary;
22
42
  findings: readonly Finding[];
23
43
  }): string;
44
+ /** Partitions a finding set into total, error, and warning counts over `noteCount` notes. */
24
45
  export declare function summarize(findings: readonly Finding[], noteCount: number): CheckSummary;
@@ -1,2 +1,7 @@
1
+ /** Reads the value from an inline flag (`--kb=x`), throwing when it is empty. */
1
2
  export declare function takeInlineValue(arg: string, prefix: string): string;
3
+ /**
4
+ * Reads the value after a space-form flag (`--kb x`), throwing when it is missing, empty, or looks like another flag.
5
+ * An empty value is rejected on both forms, so the two spellings of a flag accept the same set of values.
6
+ */
2
7
  export declare function takeValue(argv: readonly string[], index: number, flag: string): string;
@@ -1,4 +1,5 @@
1
1
  import type { StoreRef } from './format.js';
2
+ /** The store-resolution outcome: a resolved store, or a categorical failure message for exit 2. */
2
3
  export type ResolveStoreOutcome = {
3
4
  ok: true;
4
5
  store: StoreRef;
@@ -7,6 +8,17 @@ export type ResolveStoreOutcome = {
7
8
  ok: false;
8
9
  message: string;
9
10
  };
11
+ /**
12
+ * Resolves the store a command runs against. An explicit `--kb <name>` is looked up in the merged registry
13
+ * (`tryLoadKbRegistry` with `projectDir: cwd`, so project-local `.agents/kb.yaml` entries join the user-global
14
+ * registry); without a flag, the nearest ancestor `.kb/` directory is used. An unknown `--kb` name or a missing `.kb/`
15
+ * fails for exit 2.
16
+ *
17
+ * The registry's `readonly` flag is reported rather than enforced: a command that writes into the store refuses on it,
18
+ * and a read-only command ignores it. It is kept off {@link StoreRef}, which carries the identity a report renders. A
19
+ * discovered store is cross-referenced against the registry by path, so a vault marked readonly is reported as such
20
+ * however it was named; one with no registry entry has no metadata to consult and is reported writable.
21
+ */
10
22
  export declare function resolveStore(input: {
11
23
  explicitKb: string | null;
12
24
  cwd: string;
@@ -1,6 +1,14 @@
1
1
  import { type CommandOutput } from './commands/check.js';
2
2
  import type { SelectKbPrompt } from './select-kb-prompt.js';
3
- export declare const HELP = "Usage: kb <command> [options]\n\nCommands:\n check Validate a knowledge base, optionally scoped to selected notes.\n create Scaffold a new knowledge base and register it in the kb.yaml registry.\n set-default Set, clear, or choose the default knowledge base.\n taxonomy Derive a knowledge base's taxonomy from the notes it already holds.\n\nRun \"kb <command> --help\" for command options.\n";
3
+ /** Top-level usage text for the `kb` bin. */
4
+ export declare const HELP = "Usage: kb <command> [options]\n\nCommands:\n check Validate a knowledge base, optionally scoped to selected notes.\n create Scaffold a new knowledge base and register it in the kb.yaml registry.\n scaffold Write into an existing knowledge base any canonical file that it lacks.\n set-default Set, clear, or choose the default knowledge base.\n taxonomy Derive a knowledge base's taxonomy from the notes it already holds.\n\nRun \"kb <command> --help\" for command options.\n";
5
+ /**
6
+ * Dispatches a `kb` subcommand and returns its {@link CommandOutput} without touching `process`, so tests drive the
7
+ * command directly. `check`, `create`, `scaffold`, `set-default`, and `taxonomy` are the subcommands; a bare
8
+ * invocation or `--help`/`-h` prints top-level usage (exit 0), and an unknown command prints usage to stderr (exit 2).
9
+ * The optional `selectKb` picker is forwarded to `set-default`'s interactive form and to `create`'s ambiguous
10
+ * default-KB prompt; `cli/index.ts` supplies it only when stdin is a TTY.
11
+ */
4
12
  export declare function run(input: {
5
13
  argv: readonly string[];
6
14
  cwd: string;
@@ -1,5 +1,6 @@
1
1
  import { runCheck } from "./commands/check.js";
2
2
  import { runCreate } from "./commands/create.js";
3
+ import { runScaffold } from "./commands/scaffold.js";
3
4
  import { runSetDefault } from "./commands/set-default.js";
4
5
  import { runTaxonomy } from "./commands/taxonomy.js";
5
6
  export const HELP = `Usage: kb <command> [options]
@@ -7,6 +8,7 @@ export const HELP = `Usage: kb <command> [options]
7
8
  Commands:
8
9
  check Validate a knowledge base, optionally scoped to selected notes.
9
10
  create Scaffold a new knowledge base and register it in the kb.yaml registry.
11
+ scaffold Write into an existing knowledge base any canonical file that it lacks.
10
12
  set-default Set, clear, or choose the default knowledge base.
11
13
  taxonomy Derive a knowledge base's taxonomy from the notes it already holds.
12
14
 
@@ -29,6 +31,9 @@ export async function run(input) {
29
31
  ...(input.selectKb !== undefined && { selectKb: input.selectKb }),
30
32
  });
31
33
  }
34
+ if (command === 'scaffold') {
35
+ return runScaffold({ argv: rest, cwd: input.cwd, ...(input.home !== undefined && { home: input.home }) });
36
+ }
32
37
  if (command === 'taxonomy') {
33
38
  return runTaxonomy({ argv: rest, cwd: input.cwd, ...(input.home !== undefined && { home: input.home }) });
34
39
  }
@@ -1,4 +1,5 @@
1
1
  import type { KbRegistryEntry } from '../types.js';
2
+ /** The user's choice from the interactive default-KB picker. */
2
3
  export type SelectKbChoice = {
3
4
  kind: 'kb';
4
5
  index: number;
@@ -7,10 +8,27 @@ export type SelectKbChoice = {
7
8
  } | {
8
9
  kind: 'cancel';
9
10
  };
11
+ /**
12
+ * Presents the registered KBs and resolves the user's choice. Injected into `runSetDefault` so the dispatcher itself
13
+ * stays free of terminal I/O and remains directly testable; the real implementation is {@link readlineSelectKbPrompt}.
14
+ */
10
15
  export type SelectKbPrompt = (input: {
11
16
  entries: readonly KbRegistryEntry[];
12
17
  currentDefaultName?: string;
13
18
  }) => Promise<SelectKbChoice>;
19
+ /**
20
+ * Renders the numbered selection list: each registered KB (marking the current default) followed by a trailing
21
+ * `(none)` option numbered `entries.length + 1`. When no default is set, `(none)` carries the current marker instead.
22
+ */
14
23
  export declare function formatKbSelection(entries: readonly KbRegistryEntry[], currentDefaultName?: string): string;
24
+ /**
25
+ * Maps a trimmed answer to a {@link SelectKbChoice}: an empty line cancels, `1..kbCount` selects that KB, and
26
+ * `kbCount + 1` clears the default. Any other value returns `null`, signalling the caller to re-prompt.
27
+ */
15
28
  export declare function parseSelection(answer: string, kbCount: number): SelectKbChoice | null;
29
+ /**
30
+ * A readline-backed {@link SelectKbPrompt}: it prints the selection list, reads a line, and re-prompts until the answer
31
+ * resolves to a choice. This is the feature's sole interactive seam — `cli/index.ts` supplies it only when stdin is a
32
+ * TTY, and other commands (e.g. `kb create`) can reuse it for their own interactive default-KB selection.
33
+ */
16
34
  export declare const readlineSelectKbPrompt: SelectKbPrompt;
@@ -1,3 +1,4 @@
1
+ /** The outcome of resolving a `--vs` ref to its changed-note paths: the store-relative paths, or a failure message. */
1
2
  export type ChangedPathsResult = {
2
3
  ok: true;
3
4
  paths: string[];
@@ -5,6 +6,15 @@ export type ChangedPathsResult = {
5
6
  ok: false;
6
7
  message: string;
7
8
  };
9
+ /**
10
+ * Resolves the notes changed between `ref` and the working tree to store-relative paths.
11
+ *
12
+ * The change set is `git diff` of the working tree against `merge-base(ref, HEAD)` with `--diff-filter=AMR`, so it
13
+ * follows renames (reporting the destination), includes uncommitted edits to tracked files, and excludes deletions.
14
+ * Git emits toplevel-relative paths and resolves symlinks, so each path is rebased onto the real store root — a no-op
15
+ * when the store is the repository root, correct when it is nested. A git failure (unknown ref, not a repository)
16
+ * returns `{ ok: false }` for the caller to surface as a usage error rather than throwing.
17
+ */
8
18
  export declare function resolveChangedPaths(input: {
9
19
  storeRoot: string;
10
20
  ref: string;
@@ -1,28 +1,20 @@
1
- import { execFileSync } from 'node:child_process';
2
1
  import { realpathSync } from 'node:fs';
3
2
  import { join, relative, sep } from 'node:path';
4
- import { describeError } from '@williamthorsen/toolbelt.errors';
5
- import { isRecord } from "../../type-guards.js";
3
+ import { runGit } from "../../git/run-git.js";
6
4
  export function resolveChangedPaths(input) {
7
5
  const { storeRoot, ref } = input;
8
- const mergeBase = tryGit(storeRoot, ['merge-base', ref, 'HEAD']);
6
+ const mergeBase = runGit({ cwd: storeRoot, args: ['merge-base', ref, 'HEAD'] });
9
7
  if (!mergeBase.ok) {
10
8
  return { ok: false, message: `could not resolve ref "${ref}": ${mergeBase.message}` };
11
9
  }
12
- const toplevel = tryGit(storeRoot, ['rev-parse', '--show-toplevel']);
10
+ const toplevel = runGit({ cwd: storeRoot, args: ['rev-parse', '--show-toplevel'] });
13
11
  if (!toplevel.ok) {
14
12
  return { ok: false, message: toplevel.message };
15
13
  }
16
- const diff = tryGit(storeRoot, [
17
- 'diff',
18
- '--name-only',
19
- '-z',
20
- '--diff-filter=AMR',
21
- '--find-renames',
22
- mergeBase.stdout.trim(),
23
- '--',
24
- '.',
25
- ]);
14
+ const diff = runGit({
15
+ cwd: storeRoot,
16
+ args: ['diff', '--name-only', '-z', '--diff-filter=AMR', '--find-renames', mergeBase.stdout.trim(), '--', '.'],
17
+ });
26
18
  if (!diff.ok) {
27
19
  return { ok: false, message: diff.message };
28
20
  }
@@ -34,21 +26,3 @@ export function resolveChangedPaths(input) {
34
26
  .map((entry) => relative(realStoreRoot, join(repoRoot, entry)).split(sep).join('/'));
35
27
  return { ok: true, paths };
36
28
  }
37
- function extractGitErrorMessage(error) {
38
- if (isRecord(error) && typeof error.stderr === 'string' && error.stderr.trim() !== '') {
39
- return error.stderr.trim();
40
- }
41
- return describeError(error);
42
- }
43
- function tryGit(storeRoot, args) {
44
- try {
45
- const stdout = execFileSync('git', ['-C', storeRoot, ...args], {
46
- encoding: 'utf8',
47
- stdio: ['ignore', 'pipe', 'pipe'],
48
- });
49
- return { ok: true, stdout };
50
- }
51
- catch (error) {
52
- return { ok: false, message: extractGitErrorMessage(error) };
53
- }
54
- }
@@ -1,8 +1,21 @@
1
1
  import type { EnumeratedNote } from '../../check/enumerate.js';
2
+ /** The outcome of resolving selection patterns against an enumerated vault. */
2
3
  export interface SelectionResult {
4
+ /** Notes matched by at least one pattern, in enumeration order, deduplicated. */
3
5
  selected: EnumeratedNote[];
6
+ /** Patterns that matched no validatable note and are backed by no real on-disk path — likely typos. */
4
7
  unmatched: string[];
5
8
  }
9
+ /**
10
+ * Resolves selection patterns against an already-enumerated note set, returning the matched notes plus the patterns
11
+ * that matched nothing real.
12
+ *
13
+ * Each pattern is matched as a `picomatch` glob against the notes' store-relative paths, so the store's
14
+ * `targets`/`exclude` filtering is inherited and a quoted glob behaves the same as a shell-expanded one. A bare
15
+ * directory expands to its subtree. A pattern matching no note is reported in `unmatched` unless a real on-disk path
16
+ * backs it (a non-validatable file such as a README, an excluded subtree, or an empty directory), in which case it is
17
+ * dropped silently — distinguishing a typo from a legitimately out-of-scope target.
18
+ */
6
19
  export declare function selectNotes(input: {
7
20
  notes: readonly EnumeratedNote[];
8
21
  patterns: readonly string[];
@@ -5,13 +5,14 @@ import { leadingLiteralPrefix } from "../../check/glob-segments.js";
5
5
  import { isEnoent } from "../../type-guards.js";
6
6
  export async function selectNotes(input) {
7
7
  const { notes, storeRoot } = input;
8
- const relativePaths = new Set(notes.map((entry) => entry.relativePath));
8
+ const pathsByComposedForm = new Map(notes.map((entry) => [entry.relativePath.normalize('NFC'), entry.relativePath]));
9
9
  const selectedPaths = new Set();
10
10
  const unmatched = [];
11
11
  for (const rawPattern of input.patterns) {
12
12
  const pattern = normalizePattern(rawPattern);
13
- if (relativePaths.has(pattern)) {
14
- selectedPaths.add(pattern);
13
+ const exactPath = pathsByComposedForm.get(pattern.normalize('NFC'));
14
+ if (exactPath !== undefined) {
15
+ selectedPaths.add(exactPath);
15
16
  continue;
16
17
  }
17
18
  const direct = matchPaths(notes, pattern);