@williamthorsen/kb 0.0.0 → 0.3.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 (114) hide show
  1. package/LICENSE +7 -0
  2. package/README.md +236 -1
  3. package/bin/kb.js +26 -0
  4. package/dist/esm/check/check.d.ts +11 -0
  5. package/dist/esm/check/check.js +17 -0
  6. package/dist/esm/check/enumerate.d.ts +14 -0
  7. package/dist/esm/check/enumerate.js +71 -0
  8. package/dist/esm/check/glob-segments.d.ts +2 -0
  9. package/dist/esm/check/glob-segments.js +12 -0
  10. package/dist/esm/check/index.d.ts +2 -0
  11. package/dist/esm/check/index.js +2 -0
  12. package/dist/esm/cli/commands/check.d.ts +20 -0
  13. package/dist/esm/cli/commands/check.js +179 -0
  14. package/dist/esm/cli/commands/create.d.ts +16 -0
  15. package/dist/esm/cli/commands/create.js +111 -0
  16. package/dist/esm/cli/commands/set-default.d.ts +15 -0
  17. package/dist/esm/cli/commands/set-default.js +123 -0
  18. package/dist/esm/cli/format.d.ts +24 -0
  19. package/dist/esm/cli/format.js +55 -0
  20. package/dist/esm/cli/index.d.ts +1 -0
  21. package/dist/esm/cli/index.js +25 -0
  22. package/dist/esm/cli/run.d.ts +9 -0
  23. package/dist/esm/cli/run.js +37 -0
  24. package/dist/esm/cli/select-kb-prompt.d.ts +16 -0
  25. package/dist/esm/cli/select-kb-prompt.js +44 -0
  26. package/dist/esm/cli/targeting/resolve-changed-paths.d.ts +11 -0
  27. package/dist/esm/cli/targeting/resolve-changed-paths.js +53 -0
  28. package/dist/esm/cli/targeting/select-notes.d.ts +10 -0
  29. package/dist/esm/cli/targeting/select-notes.js +60 -0
  30. package/dist/esm/config/config-schema.d.ts +10 -0
  31. package/dist/esm/config/config-schema.js +10 -0
  32. package/dist/esm/config/index.d.ts +4 -0
  33. package/dist/esm/config/index.js +4 -0
  34. package/dist/esm/config/kb-loader-error.d.ts +5 -0
  35. package/dist/esm/config/kb-loader-error.js +14 -0
  36. package/dist/esm/config/load-config.d.ts +5 -0
  37. package/dist/esm/config/load-config.js +36 -0
  38. package/dist/esm/config/note-scope.d.ts +7 -0
  39. package/dist/esm/config/note-scope.js +10 -0
  40. package/dist/esm/create/create.d.ts +26 -0
  41. package/dist/esm/create/create.js +51 -0
  42. package/dist/esm/create/index.d.ts +3 -0
  43. package/dist/esm/create/index.js +2 -0
  44. package/dist/esm/create/render-seeds.d.ts +2 -0
  45. package/dist/esm/create/render-seeds.js +29 -0
  46. package/dist/esm/discovery/find-kb-root.d.ts +4 -0
  47. package/dist/esm/discovery/find-kb-root.js +22 -0
  48. package/dist/esm/discovery/index.d.ts +9 -0
  49. package/dist/esm/discovery/index.js +5 -0
  50. package/dist/esm/discovery/kb-registry-schema.d.ts +16 -0
  51. package/dist/esm/discovery/kb-registry-schema.js +10 -0
  52. package/dist/esm/discovery/load-registry.d.ts +15 -0
  53. package/dist/esm/discovery/load-registry.js +114 -0
  54. package/dist/esm/discovery/register-store.d.ts +9 -0
  55. package/dist/esm/discovery/register-store.js +26 -0
  56. package/dist/esm/discovery/registry-document.d.ts +2 -0
  57. package/dist/esm/discovery/registry-document.js +17 -0
  58. package/dist/esm/discovery/set-default-kb.d.ts +7 -0
  59. package/dist/esm/discovery/set-default-kb.js +53 -0
  60. package/dist/esm/filesystem/exists.d.ts +6 -0
  61. package/dist/esm/filesystem/exists.js +24 -0
  62. package/dist/esm/filesystem/index.d.ts +1 -0
  63. package/dist/esm/filesystem/index.js +1 -0
  64. package/dist/esm/frontmatter/index.d.ts +2 -0
  65. package/dist/esm/frontmatter/index.js +1 -0
  66. package/dist/esm/frontmatter/parse-note.d.ts +8 -0
  67. package/dist/esm/frontmatter/parse-note.js +98 -0
  68. package/dist/esm/index.d.ts +2 -0
  69. package/dist/esm/index.js +1 -0
  70. package/dist/esm/layout/index.d.ts +1 -0
  71. package/dist/esm/layout/index.js +1 -0
  72. package/dist/esm/layout/store-layout.d.ts +15 -0
  73. package/dist/esm/layout/store-layout.js +23 -0
  74. package/dist/esm/lints/index.d.ts +2 -0
  75. package/dist/esm/lints/index.js +2 -0
  76. package/dist/esm/lints/paths.d.ts +6 -0
  77. package/dist/esm/lints/paths.js +12 -0
  78. package/dist/esm/lints/tag-alias.d.ts +6 -0
  79. package/dist/esm/lints/tag-alias.js +21 -0
  80. package/dist/esm/note-io/field-validators.d.ts +2 -0
  81. package/dist/esm/note-io/field-validators.js +30 -0
  82. package/dist/esm/note-io/index.d.ts +4 -0
  83. package/dist/esm/note-io/index.js +4 -0
  84. package/dist/esm/note-io/read-note.d.ts +8 -0
  85. package/dist/esm/note-io/read-note.js +22 -0
  86. package/dist/esm/note-io/write-note.d.ts +2 -0
  87. package/dist/esm/note-io/write-note.js +21 -0
  88. package/dist/esm/note-io/yaml-fields.d.ts +5 -0
  89. package/dist/esm/note-io/yaml-fields.js +49 -0
  90. package/dist/esm/records/assertion.d.ts +25 -0
  91. package/dist/esm/records/assertion.js +136 -0
  92. package/dist/esm/records/event.d.ts +28 -0
  93. package/dist/esm/records/event.js +128 -0
  94. package/dist/esm/records/index.d.ts +2 -0
  95. package/dist/esm/records/index.js +2 -0
  96. package/dist/esm/tags/canonicalize.d.ts +3 -0
  97. package/dist/esm/tags/canonicalize.js +6 -0
  98. package/dist/esm/tags/index.d.ts +3 -0
  99. package/dist/esm/tags/index.js +2 -0
  100. package/dist/esm/tags/load-aliases.d.ts +5 -0
  101. package/dist/esm/tags/load-aliases.js +72 -0
  102. package/dist/esm/type-guards.d.ts +3 -0
  103. package/dist/esm/type-guards.js +12 -0
  104. package/dist/esm/types.d.ts +50 -0
  105. package/dist/esm/types.js +1 -0
  106. package/dist/esm/vault-integrity/build-vault-index.d.ts +4 -0
  107. package/dist/esm/vault-integrity/build-vault-index.js +14 -0
  108. package/dist/esm/vault-integrity/check-vault-integrity.d.ts +7 -0
  109. package/dist/esm/vault-integrity/check-vault-integrity.js +49 -0
  110. package/dist/esm/vault-integrity/index.d.ts +3 -0
  111. package/dist/esm/vault-integrity/index.js +3 -0
  112. package/dist/esm/vault-integrity/wikilink-parse.d.ts +7 -0
  113. package/dist/esm/vault-integrity/wikilink-parse.js +75 -0
  114. package/package.json +108 -2
@@ -0,0 +1,111 @@
1
+ import { homedir } from 'node:os';
2
+ import { join } from 'node:path';
3
+ import { create } from "../../create/create.js";
4
+ import { runSetDefault } from "./set-default.js";
5
+ export const CREATE_HELP = `Usage: kb create [options]
6
+
7
+ Scaffold a new knowledge base in the current directory and register it in the user-global kb.yaml registry.
8
+
9
+ When the registry has no default knowledge base, the new store becomes the default.
10
+ If other knowledge bases are already registered, you are prompted to choose one (or set it later with "kb set-default").
11
+
12
+ Creates:
13
+ .kb/config.yaml check configuration (commented; defaults apply)
14
+ .kb/tag-aliases.yaml tag-alias map (empty)
15
+ content/, content/events/
16
+
17
+ Options:
18
+ --name <name> Registry name for the store. Defaults to the directory name.
19
+ --no-register Scaffold without writing the kb.yaml registry entry.
20
+ -h, --help Show this help.
21
+
22
+ Exit codes:
23
+ 0 store created
24
+ 2 usage error, an existing .kb/ in the directory, or an already-registered name
25
+ `;
26
+ export async function runCreate(input) {
27
+ let options;
28
+ try {
29
+ options = parseCreateArgs(input.argv);
30
+ }
31
+ catch (error) {
32
+ return buildUsageError(error);
33
+ }
34
+ if (options.help) {
35
+ return { exitCode: 0, stdout: CREATE_HELP, stderr: '' };
36
+ }
37
+ const registryPath = join(input.home ?? homedir(), '.agents', 'kb.yaml');
38
+ const base = { targetDir: input.cwd, ...(options.name !== null && { name: options.name }) };
39
+ const outcome = options.noRegister
40
+ ? await create({ ...base, register: false })
41
+ : await create({ ...base, register: true, registryPath });
42
+ if (!outcome.ok) {
43
+ return { exitCode: 2, stdout: '', stderr: `kb create: ${outcome.message}\n` };
44
+ }
45
+ const summary = formatCreated(outcome.created, registryPath);
46
+ if (outcome.created.defaultKb !== 'needs-selection') {
47
+ return { exitCode: 0, stdout: summary, stderr: '' };
48
+ }
49
+ if (input.selectKb === undefined) {
50
+ return { exitCode: 0, stdout: summary + UNSET_DEFAULT_HINT, stderr: '' };
51
+ }
52
+ const selection = await runSetDefault({
53
+ argv: [],
54
+ ...(input.home !== undefined && { home: input.home }),
55
+ selectKb: input.selectKb,
56
+ });
57
+ return { exitCode: 0, stdout: summary + selection.stdout, stderr: selection.stderr };
58
+ }
59
+ export function parseCreateArgs(argv) {
60
+ let name = null;
61
+ let noRegister = false;
62
+ let help = false;
63
+ for (let index = 0; index < argv.length; index += 1) {
64
+ const arg = argv[index];
65
+ if (arg === undefined)
66
+ continue;
67
+ if (arg === '--help' || arg === '-h') {
68
+ help = true;
69
+ continue;
70
+ }
71
+ if (arg === '--no-register') {
72
+ noRegister = true;
73
+ continue;
74
+ }
75
+ if (arg === '--name') {
76
+ const next = argv[index + 1] ?? null;
77
+ if (next === null || next.startsWith('--')) {
78
+ throw new Error('--name requires a value');
79
+ }
80
+ name = next;
81
+ index += 1;
82
+ continue;
83
+ }
84
+ if (arg.startsWith('--name=')) {
85
+ const value = arg.slice('--name='.length);
86
+ if (value === '') {
87
+ throw new Error('--name requires a value');
88
+ }
89
+ name = value;
90
+ continue;
91
+ }
92
+ throw new Error(`unknown flag: ${arg}`);
93
+ }
94
+ return { name, noRegister, help };
95
+ }
96
+ const UNSET_DEFAULT_HINT = 'Multiple knowledge bases are registered and no default is set. Run `kb set-default` to choose one.\n';
97
+ function buildUsageError(error) {
98
+ const message = error instanceof Error ? error.message : String(error);
99
+ return { exitCode: 2, stdout: '', stderr: `kb create: ${message}\n${CREATE_HELP}` };
100
+ }
101
+ function formatCreated(created, registryPath) {
102
+ const lines = [`Created knowledge base "${created.name}" at ${created.storePath}`];
103
+ for (const path of created.created) {
104
+ lines.push(` ${path}`);
105
+ }
106
+ lines.push(created.registered ? `Registered in ${registryPath}` : 'Not registered (--no-register).');
107
+ if (created.defaultKb === 'set') {
108
+ lines.push('Set as the default knowledge base.');
109
+ }
110
+ return `${lines.join('\n')}\n`;
111
+ }
@@ -0,0 +1,15 @@
1
+ import type { SelectKbPrompt } from '../select-kb-prompt.js';
2
+ import type { CommandOutput } from './check.js';
3
+ 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";
4
+ export declare function runSetDefault(input: {
5
+ argv: readonly string[];
6
+ home?: string;
7
+ selectKb?: SelectKbPrompt;
8
+ }): Promise<CommandOutput>;
9
+ interface SetDefaultOptions {
10
+ name: string | null;
11
+ none: boolean;
12
+ help: boolean;
13
+ }
14
+ export declare function parseSetDefaultArgs(argv: readonly string[]): SetDefaultOptions;
15
+ export {};
@@ -0,0 +1,123 @@
1
+ import { homedir } from 'node:os';
2
+ import { join } from 'node:path';
3
+ import { tryLoadKbRegistry } from "../../discovery/load-registry.js";
4
+ import { clearDefaultKb, setDefaultKb } from "../../discovery/set-default-kb.js";
5
+ export const SET_DEFAULT_HELP = `Usage: kb set-default [name] [options]
6
+
7
+ Set, clear, or interactively choose the user-global default knowledge base —
8
+ the top-level default_kb pointer in ~/.agents/kb.yaml.
9
+
10
+ kb set-default <name> Set default_kb to the named registered KB.
11
+ kb set-default --none Clear default_kb.
12
+ kb set-default List the registered KBs and prompt for a choice
13
+ (including a "(none)" option to clear).
14
+
15
+ Options:
16
+ --none Clear the default knowledge base.
17
+ -h, --help Show this help.
18
+
19
+ Exit codes:
20
+ 0 default set or cleared, or the selection was cancelled
21
+ 2 usage error, an unregistered name, no registered KBs, or no name supplied
22
+ when stdin is not interactive
23
+ `;
24
+ export async function runSetDefault(input) {
25
+ let options;
26
+ try {
27
+ options = parseSetDefaultArgs(input.argv);
28
+ }
29
+ catch (error) {
30
+ return buildUsageError(error);
31
+ }
32
+ if (options.help) {
33
+ return { exitCode: 0, stdout: SET_DEFAULT_HELP, stderr: '' };
34
+ }
35
+ const home = input.home ?? homedir();
36
+ const registryPath = join(home, '.agents', 'kb.yaml');
37
+ const { config, error } = await tryLoadKbRegistry({ home });
38
+ if (error !== undefined) {
39
+ return { exitCode: 2, stdout: '', stderr: `kb set-default: ${error}\n` };
40
+ }
41
+ const entries = config.entries;
42
+ if (options.none) {
43
+ await clearDefaultKb({ registryPath });
44
+ return { exitCode: 0, stdout: 'The default knowledge base has been cleared.\n', stderr: '' };
45
+ }
46
+ if (options.name !== null) {
47
+ if (entries.length === 0) {
48
+ return { exitCode: 2, stdout: '', stderr: buildNoStoresMessage() };
49
+ }
50
+ if (entries.every((entry) => entry.name !== options.name)) {
51
+ return { exitCode: 2, stdout: '', stderr: buildNotRegisteredMessage(options.name, entries) };
52
+ }
53
+ await setDefaultKb({ registryPath, name: options.name });
54
+ return { exitCode: 0, stdout: buildSetConfirmation(options.name), stderr: '' };
55
+ }
56
+ if (input.selectKb === undefined) {
57
+ return {
58
+ exitCode: 2,
59
+ stdout: '',
60
+ stderr: `kb set-default: a knowledge base name is required when stdin is not interactive\n${SET_DEFAULT_HELP}`,
61
+ };
62
+ }
63
+ if (entries.length === 0) {
64
+ return { exitCode: 2, stdout: '', stderr: buildNoStoresMessage() };
65
+ }
66
+ const choice = await input.selectKb({
67
+ entries,
68
+ ...(config.defaultKb !== undefined && { currentDefaultName: config.defaultKb.name }),
69
+ });
70
+ if (choice.kind === 'cancel') {
71
+ return { exitCode: 0, stdout: 'No changes made.\n', stderr: '' };
72
+ }
73
+ if (choice.kind === 'none') {
74
+ await clearDefaultKb({ registryPath });
75
+ return { exitCode: 0, stdout: 'The default knowledge base has been cleared.\n', stderr: '' };
76
+ }
77
+ const chosen = entries[choice.index];
78
+ if (chosen === undefined) {
79
+ return { exitCode: 2, stdout: '', stderr: 'kb set-default: invalid selection\n' };
80
+ }
81
+ await setDefaultKb({ registryPath, name: chosen.name });
82
+ return { exitCode: 0, stdout: buildSetConfirmation(chosen.name), stderr: '' };
83
+ }
84
+ export function parseSetDefaultArgs(argv) {
85
+ let name = null;
86
+ let none = false;
87
+ let help = false;
88
+ for (const arg of argv) {
89
+ if (arg === '--help' || arg === '-h') {
90
+ help = true;
91
+ continue;
92
+ }
93
+ if (arg === '--none') {
94
+ none = true;
95
+ continue;
96
+ }
97
+ if (arg.startsWith('-')) {
98
+ throw new Error(`unknown flag: ${arg}`);
99
+ }
100
+ if (name !== null) {
101
+ throw new Error('set-default accepts at most one knowledge base name');
102
+ }
103
+ name = arg;
104
+ }
105
+ if (name !== null && none) {
106
+ throw new Error('a name cannot be combined with --none');
107
+ }
108
+ return { name, none, help };
109
+ }
110
+ function buildNoStoresMessage() {
111
+ return 'kb set-default: no knowledge bases registered; run `kb create` first\n';
112
+ }
113
+ function buildNotRegisteredMessage(name, entries) {
114
+ const names = entries.map((entry) => entry.name).join(', ');
115
+ return `kb set-default: "${name}" is not a registered knowledge base (registered: ${names})\n`;
116
+ }
117
+ function buildSetConfirmation(name) {
118
+ return `Default knowledge base has been set to "${name}".\n`;
119
+ }
120
+ function buildUsageError(error) {
121
+ const message = error instanceof Error ? error.message : String(error);
122
+ return { exitCode: 2, stdout: '', stderr: `kb set-default: ${message}\n${SET_DEFAULT_HELP}` };
123
+ }
@@ -0,0 +1,24 @@
1
+ import type { Finding } from '../types.js';
2
+ export interface StoreRef {
3
+ name: string | null;
4
+ path: string;
5
+ }
6
+ export interface CheckSummary {
7
+ notes: number;
8
+ total: number;
9
+ errors: number;
10
+ warnings: number;
11
+ }
12
+ export type CheckScope = 'vault' | 'patterns' | 'vs';
13
+ export declare function formatHuman(input: {
14
+ summary: CheckSummary;
15
+ findings: readonly Finding[];
16
+ targets: readonly string[];
17
+ scope: CheckScope;
18
+ }): string;
19
+ export declare function formatJson(input: {
20
+ store: StoreRef;
21
+ summary: CheckSummary;
22
+ findings: readonly Finding[];
23
+ }): string;
24
+ export declare function summarize(findings: readonly Finding[], noteCount: number): CheckSummary;
@@ -0,0 +1,55 @@
1
+ export function formatHuman(input) {
2
+ const { summary, findings, targets, scope } = input;
3
+ if (summary.notes === 0) {
4
+ return `${zeroMatchLine(scope, targets)}\n`;
5
+ }
6
+ if (findings.length === 0) {
7
+ return `✓ no findings (${summary.notes} notes checked)\n`;
8
+ }
9
+ const lines = [];
10
+ for (const [path, group] of groupByPath(findings)) {
11
+ lines.push(path);
12
+ for (const finding of group) {
13
+ const location = finding.line === undefined ? '' : ` (line ${finding.line})`;
14
+ lines.push(` ${finding.severity} ${finding.rule}${location}: ${finding.message}`);
15
+ }
16
+ }
17
+ lines.push('', `${summary.total} findings (${summary.errors} errors, ${summary.warnings} warnings) in ${summary.notes} notes`);
18
+ return `${lines.join('\n')}\n`;
19
+ }
20
+ export function formatJson(input) {
21
+ return `${JSON.stringify({ store: input.store, summary: input.summary, findings: input.findings }, null, 2)}\n`;
22
+ }
23
+ export function summarize(findings, noteCount) {
24
+ let errors = 0;
25
+ let warnings = 0;
26
+ for (const finding of findings) {
27
+ if (finding.severity === 'error')
28
+ errors += 1;
29
+ else
30
+ warnings += 1;
31
+ }
32
+ return { notes: noteCount, total: findings.length, errors, warnings };
33
+ }
34
+ function groupByPath(findings) {
35
+ const groups = new Map();
36
+ for (const finding of findings) {
37
+ let group = groups.get(finding.path);
38
+ if (group === undefined) {
39
+ group = [];
40
+ groups.set(finding.path, group);
41
+ }
42
+ group.push(finding);
43
+ }
44
+ return groups;
45
+ }
46
+ function zeroMatchLine(scope, targets) {
47
+ switch (scope) {
48
+ case 'patterns':
49
+ return 'no notes matched the given paths (0 checked)';
50
+ case 'vs':
51
+ return 'no changed notes to check (0 checked)';
52
+ case 'vault':
53
+ return `no notes matched ${targets.join(', ')} (0 checked)`;
54
+ }
55
+ }
@@ -0,0 +1 @@
1
+ export {};
@@ -0,0 +1,25 @@
1
+ import process from 'node:process';
2
+ import { run } from "./run.js";
3
+ import { readlineSelectKbPrompt } from "./select-kb-prompt.js";
4
+ async function main() {
5
+ let output;
6
+ try {
7
+ const selectKb = process.stdin.isTTY ? readlineSelectKbPrompt : undefined;
8
+ output = await run({
9
+ argv: process.argv.slice(2),
10
+ cwd: process.cwd(),
11
+ ...(selectKb !== undefined && { selectKb }),
12
+ });
13
+ }
14
+ catch (error) {
15
+ const message = error instanceof Error ? error.message : String(error);
16
+ process.stderr.write(`kb: unexpected error: ${message}\n`);
17
+ process.exit(2);
18
+ }
19
+ if (output.stdout !== '')
20
+ process.stdout.write(output.stdout);
21
+ if (output.stderr !== '')
22
+ process.stderr.write(output.stderr);
23
+ process.exit(output.exitCode);
24
+ }
25
+ await main();
@@ -0,0 +1,9 @@
1
+ import { type CommandOutput } from './commands/check.js';
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\nRun \"kb <command> --help\" for command options.\n";
4
+ export declare function run(input: {
5
+ argv: readonly string[];
6
+ cwd: string;
7
+ home?: string;
8
+ selectKb?: SelectKbPrompt;
9
+ }): Promise<CommandOutput>;
@@ -0,0 +1,37 @@
1
+ import { runCheck } from "./commands/check.js";
2
+ import { runCreate } from "./commands/create.js";
3
+ import { runSetDefault } from "./commands/set-default.js";
4
+ export const HELP = `Usage: kb <command> [options]
5
+
6
+ Commands:
7
+ check Validate a knowledge base, optionally scoped to selected notes.
8
+ create Scaffold a new knowledge base and register it in the kb.yaml registry.
9
+ set-default Set, clear, or choose the default knowledge base.
10
+
11
+ Run "kb <command> --help" for command options.
12
+ `;
13
+ export async function run(input) {
14
+ const [command, ...rest] = input.argv;
15
+ if (command === undefined || command === '--help' || command === '-h') {
16
+ return { exitCode: 0, stdout: HELP, stderr: '' };
17
+ }
18
+ if (command === 'check') {
19
+ return runCheck({ argv: rest, cwd: input.cwd, ...(input.home !== undefined && { home: input.home }) });
20
+ }
21
+ if (command === 'create') {
22
+ return runCreate({
23
+ argv: rest,
24
+ cwd: input.cwd,
25
+ ...(input.home !== undefined && { home: input.home }),
26
+ ...(input.selectKb !== undefined && { selectKb: input.selectKb }),
27
+ });
28
+ }
29
+ if (command === 'set-default') {
30
+ return runSetDefault({
31
+ argv: rest,
32
+ ...(input.home !== undefined && { home: input.home }),
33
+ ...(input.selectKb !== undefined && { selectKb: input.selectKb }),
34
+ });
35
+ }
36
+ return { exitCode: 2, stdout: '', stderr: `kb: unknown command "${command}"\n${HELP}` };
37
+ }
@@ -0,0 +1,16 @@
1
+ import type { KbRegistryEntry } from '../types.js';
2
+ export type SelectKbChoice = {
3
+ kind: 'kb';
4
+ index: number;
5
+ } | {
6
+ kind: 'none';
7
+ } | {
8
+ kind: 'cancel';
9
+ };
10
+ export type SelectKbPrompt = (input: {
11
+ entries: readonly KbRegistryEntry[];
12
+ currentDefaultName?: string;
13
+ }) => Promise<SelectKbChoice>;
14
+ export declare function formatKbSelection(entries: readonly KbRegistryEntry[], currentDefaultName?: string): string;
15
+ export declare function parseSelection(answer: string, kbCount: number): SelectKbChoice | null;
16
+ export declare const readlineSelectKbPrompt: SelectKbPrompt;
@@ -0,0 +1,44 @@
1
+ import process from 'node:process';
2
+ import { createInterface } from 'node:readline';
3
+ export function formatKbSelection(entries, currentDefaultName) {
4
+ const lines = ['Select the default knowledge base:'];
5
+ for (const [index, entry] of entries.entries()) {
6
+ const suffix = entry.name === currentDefaultName ? ' (current default)' : '';
7
+ lines.push(` ${String(index + 1)}) ${entry.name}${suffix}`);
8
+ }
9
+ const noneSuffix = currentDefaultName === undefined ? ' (current)' : '';
10
+ lines.push(` ${String(entries.length + 1)}) (none) — no default${noneSuffix}`);
11
+ return `${lines.join('\n')}\n`;
12
+ }
13
+ export function parseSelection(answer, kbCount) {
14
+ if (answer === '')
15
+ return { kind: 'cancel' };
16
+ const choice = Number(answer);
17
+ if (!Number.isInteger(choice))
18
+ return null;
19
+ if (choice >= 1 && choice <= kbCount)
20
+ return { kind: 'kb', index: choice - 1 };
21
+ if (choice === kbCount + 1)
22
+ return { kind: 'none' };
23
+ return null;
24
+ }
25
+ export const readlineSelectKbPrompt = async ({ entries, currentDefaultName }) => {
26
+ const rl = createInterface({ input: process.stdin, output: process.stdout });
27
+ try {
28
+ process.stdout.write(formatKbSelection(entries, currentDefaultName));
29
+ const prompt = `Enter a number [1-${String(entries.length + 1)}], or press Enter to cancel: `;
30
+ let choice = parseSelection((await ask(rl, prompt)).trim(), entries.length);
31
+ while (choice === null) {
32
+ choice = parseSelection((await ask(rl, prompt)).trim(), entries.length);
33
+ }
34
+ return choice;
35
+ }
36
+ finally {
37
+ rl.close();
38
+ }
39
+ };
40
+ function ask(rl, query) {
41
+ return new Promise((resolve) => {
42
+ rl.question(query, resolve);
43
+ });
44
+ }
@@ -0,0 +1,11 @@
1
+ export type ChangedPathsResult = {
2
+ ok: true;
3
+ paths: string[];
4
+ } | {
5
+ ok: false;
6
+ message: string;
7
+ };
8
+ export declare function resolveChangedPaths(input: {
9
+ storeRoot: string;
10
+ ref: string;
11
+ }): ChangedPathsResult;
@@ -0,0 +1,53 @@
1
+ import { execFileSync } from 'node:child_process';
2
+ import { realpathSync } from 'node:fs';
3
+ import { join, relative, sep } from 'node:path';
4
+ import { isRecord } from "../../type-guards.js";
5
+ export function resolveChangedPaths(input) {
6
+ const { storeRoot, ref } = input;
7
+ const mergeBase = tryGit(storeRoot, ['merge-base', ref, 'HEAD']);
8
+ if (!mergeBase.ok) {
9
+ return { ok: false, message: `could not resolve ref "${ref}": ${mergeBase.message}` };
10
+ }
11
+ const toplevel = tryGit(storeRoot, ['rev-parse', '--show-toplevel']);
12
+ if (!toplevel.ok) {
13
+ return { ok: false, message: toplevel.message };
14
+ }
15
+ const diff = tryGit(storeRoot, [
16
+ 'diff',
17
+ '--name-only',
18
+ '-z',
19
+ '--diff-filter=AMR',
20
+ '--find-renames',
21
+ mergeBase.stdout.trim(),
22
+ '--',
23
+ '.',
24
+ ]);
25
+ if (!diff.ok) {
26
+ return { ok: false, message: diff.message };
27
+ }
28
+ const realStoreRoot = realpathSync(storeRoot);
29
+ const repoRoot = toplevel.stdout.trim();
30
+ const paths = diff.stdout
31
+ .split('\0')
32
+ .filter((entry) => entry !== '')
33
+ .map((entry) => relative(realStoreRoot, join(repoRoot, entry)).split(sep).join('/'));
34
+ return { ok: true, paths };
35
+ }
36
+ function extractGitErrorMessage(error) {
37
+ if (isRecord(error) && typeof error.stderr === 'string' && error.stderr.trim() !== '') {
38
+ return error.stderr.trim();
39
+ }
40
+ return error instanceof Error ? error.message : String(error);
41
+ }
42
+ function tryGit(storeRoot, args) {
43
+ try {
44
+ const stdout = execFileSync('git', ['-C', storeRoot, ...args], {
45
+ encoding: 'utf8',
46
+ stdio: ['ignore', 'pipe', 'pipe'],
47
+ });
48
+ return { ok: true, stdout };
49
+ }
50
+ catch (error) {
51
+ return { ok: false, message: extractGitErrorMessage(error) };
52
+ }
53
+ }
@@ -0,0 +1,10 @@
1
+ import type { EnumeratedNote } from '../../check/enumerate.js';
2
+ export interface SelectionResult {
3
+ selected: EnumeratedNote[];
4
+ unmatched: string[];
5
+ }
6
+ export declare function selectNotes(input: {
7
+ notes: readonly EnumeratedNote[];
8
+ patterns: readonly string[];
9
+ storeRoot: string;
10
+ }): Promise<SelectionResult>;
@@ -0,0 +1,60 @@
1
+ import { stat } from 'node:fs/promises';
2
+ import { join } from 'node:path';
3
+ import picomatch from 'picomatch';
4
+ import { leadingLiteralPrefix } from "../../check/glob-segments.js";
5
+ import { isEnoent } from "../../type-guards.js";
6
+ export async function selectNotes(input) {
7
+ const { notes, storeRoot } = input;
8
+ const relativePaths = new Set(notes.map((entry) => entry.relativePath));
9
+ const selectedPaths = new Set();
10
+ const unmatched = [];
11
+ for (const rawPattern of input.patterns) {
12
+ const pattern = normalizePattern(rawPattern);
13
+ if (relativePaths.has(pattern)) {
14
+ selectedPaths.add(pattern);
15
+ continue;
16
+ }
17
+ const direct = matchPaths(notes, pattern);
18
+ if (direct.length > 0) {
19
+ for (const path of direct)
20
+ selectedPaths.add(path);
21
+ continue;
22
+ }
23
+ const resolved = await resolveEmptyPattern({ notes, pattern, storeRoot, selectedPaths });
24
+ if (!resolved)
25
+ unmatched.push(rawPattern);
26
+ }
27
+ const selected = notes.filter((entry) => selectedPaths.has(entry.relativePath));
28
+ return { selected, unmatched };
29
+ }
30
+ function matchPaths(notes, pattern) {
31
+ const isMatch = picomatch(pattern, { dot: false });
32
+ return notes.filter((entry) => isMatch(entry.relativePath)).map((entry) => entry.relativePath);
33
+ }
34
+ function normalizePattern(pattern) {
35
+ return pattern.replace(/^\.\//, '').replace(/\/$/, '');
36
+ }
37
+ async function resolveEmptyPattern(input) {
38
+ const { notes, pattern, storeRoot, selectedPaths } = input;
39
+ if (leadingLiteralPrefix(pattern) === pattern) {
40
+ const target = await tryStat(join(storeRoot, pattern));
41
+ if (target?.isDirectory()) {
42
+ for (const path of matchPaths(notes, `${pattern}/**`))
43
+ selectedPaths.add(path);
44
+ return true;
45
+ }
46
+ return target !== null;
47
+ }
48
+ const prefix = leadingLiteralPrefix(pattern);
49
+ return prefix !== '' && (await tryStat(join(storeRoot, prefix))) !== null;
50
+ }
51
+ async function tryStat(path) {
52
+ try {
53
+ return await stat(path);
54
+ }
55
+ catch (error) {
56
+ if (isEnoent(error))
57
+ return null;
58
+ throw error;
59
+ }
60
+ }
@@ -0,0 +1,10 @@
1
+ import { z } from 'zod';
2
+ export declare const configFileShape: z.ZodObject<{
3
+ targets: z.ZodOptional<z.ZodArray<z.ZodString>>;
4
+ exclude: z.ZodOptional<z.ZodArray<z.ZodString>>;
5
+ }, z.core.$strip>;
6
+ export interface KbConfig {
7
+ targets: readonly string[];
8
+ exclude: readonly string[];
9
+ }
10
+ export declare const defaultKbConfig: KbConfig;
@@ -0,0 +1,10 @@
1
+ import { z } from 'zod';
2
+ import { CONTENT_DIR } from "../layout/index.js";
3
+ export const configFileShape = z.object({
4
+ targets: z.array(z.string()).optional(),
5
+ exclude: z.array(z.string()).optional(),
6
+ });
7
+ export const defaultKbConfig = {
8
+ targets: [`${CONTENT_DIR}/**/*.md`],
9
+ exclude: ['**/node_modules/**'],
10
+ };
@@ -0,0 +1,4 @@
1
+ export { configFileShape, defaultKbConfig, type KbConfig } from './config-schema.js';
2
+ export { isKbLoaderError, KbLoaderError } from './kb-loader-error.js';
3
+ export { loadKbConfig } from './load-config.js';
4
+ export { createNoteScopeMatcher, type NoteScopeMatcher } from './note-scope.js';
@@ -0,0 +1,4 @@
1
+ export { configFileShape, defaultKbConfig } from "./config-schema.js";
2
+ export { isKbLoaderError, KbLoaderError } from "./kb-loader-error.js";
3
+ export { loadKbConfig } from "./load-config.js";
4
+ export { createNoteScopeMatcher } from "./note-scope.js";
@@ -0,0 +1,5 @@
1
+ export declare class KbLoaderError extends Error {
2
+ readonly kind: "KbLoaderError";
3
+ constructor(message: string);
4
+ }
5
+ export declare function isKbLoaderError(error: unknown): error is KbLoaderError;
@@ -0,0 +1,14 @@
1
+ export class KbLoaderError extends Error {
2
+ kind = 'KbLoaderError';
3
+ constructor(message) {
4
+ super(message);
5
+ this.name = 'KbLoaderError';
6
+ }
7
+ }
8
+ export function isKbLoaderError(error) {
9
+ if (typeof error !== 'object' || error === null) {
10
+ return false;
11
+ }
12
+ const candidate = error;
13
+ return candidate.kind === 'KbLoaderError';
14
+ }