@dikolab/kbdb 0.2.2 → 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 (118) hide show
  1. package/README.md +50 -15
  2. package/dist/cli.cjs +461 -72
  3. package/dist/cli.cjs.map +4 -4
  4. package/dist/cli.mjs +453 -64
  5. package/dist/cli.mjs.map +4 -4
  6. package/dist/kbdb-worker.cjs +395 -60
  7. package/dist/kbdb-worker.cjs.map +4 -4
  8. package/dist/mod.cjs +17 -0
  9. package/dist/mod.cjs.map +2 -2
  10. package/dist/mod.mjs +17 -0
  11. package/dist/mod.mjs.map +2 -2
  12. package/dist/src/shared/cli/functions/dispatch-command.function.d.ts +3 -1
  13. package/dist/src/shared/cli/functions/install-default-skills.function.d.ts +12 -0
  14. package/dist/src/shared/cli/functions/run-check.function.d.ts +47 -5
  15. package/dist/src/shared/cli/functions/run-export.function.d.ts +34 -0
  16. package/dist/src/shared/cli/functions/run-gc.function.d.ts +6 -0
  17. package/dist/src/shared/cli/functions/run-learn.function.d.ts +35 -6
  18. package/dist/src/shared/cli/functions/run-recall.function.d.ts +45 -0
  19. package/dist/src/shared/cli/functions/run-search.function.d.ts +52 -15
  20. package/dist/src/shared/cli/index.d.ts +4 -0
  21. package/dist/src/shared/cli/typings/cli-options.interface.d.ts +13 -0
  22. package/dist/src/shared/cli/typings/learn-result.model.d.ts +19 -0
  23. package/dist/src/shared/cli/typings/search-display.model.d.ts +18 -0
  24. package/dist/src/shared/mcp/classes/mcp-server.class.d.ts +8 -0
  25. package/dist/src/shared/mcp/typings/mcp-client.interface.d.ts +43 -0
  26. package/dist/src/shared/wasm-codec/functions/decode-add-section-result.function.d.ts +48 -0
  27. package/dist/src/shared/wasm-codec/functions/decode-scored-results.function.d.ts +58 -0
  28. package/dist/src/shared/wasm-codec/functions/decode-section-records.function.d.ts +33 -0
  29. package/dist/src/shared/wasm-codec/functions/encode-add-section-params.function.d.ts +52 -0
  30. package/dist/src/shared/wasm-codec/index.d.ts +2 -0
  31. package/dist/src/shared/worker-client/classes/worker-client.class.d.ts +16 -0
  32. package/dist/src/shared/worker-client/index.d.ts +1 -1
  33. package/dist/src/shared/worker-client/typings/add-section-result.model.d.ts +29 -15
  34. package/dist/src/shared/worker-client/typings/export-result.model.d.ts +14 -0
  35. package/dist/src/shared/worker-client/typings/gc-result.model.d.ts +6 -0
  36. package/dist/src/shared/worker-client/typings/search-result.model.d.ts +12 -0
  37. package/dist/src/shared/worker-client/typings/section.model.d.ts +16 -0
  38. package/dist/src/shared/worker-daemon/classes/worker-daemon.class.d.ts +35 -0
  39. package/dist/wasm/fs-database/kbdb_fs_database.d.ts +22 -1
  40. package/dist/wasm/fs-database/kbdb_fs_database.js +28 -1
  41. package/dist/wasm/fs-database/kbdb_fs_database_bg.wasm +0 -0
  42. package/dist/wasm/fs-database/kbdb_fs_database_bg.wasm.d.ts +1 -0
  43. package/dist/wasm/kb-worker/kbdb_worker.d.ts +48 -1
  44. package/dist/wasm/kb-worker/kbdb_worker.js +60 -1
  45. package/dist/wasm/kb-worker/kbdb_worker_bg.wasm +0 -0
  46. package/dist/wasm/kb-worker/kbdb_worker_bg.wasm.d.ts +2 -0
  47. package/dist/wasm/query-parser/kbdb_query_parser_bg.wasm +0 -0
  48. package/dist/worker.mjs +396 -60
  49. package/dist/worker.mjs.map +4 -4
  50. package/docs/details/cli.md +102 -15
  51. package/docs/details/library-api.md +55 -6
  52. package/docs/details/mcp-server.md +8 -4
  53. package/docs/details/search-and-ranking.md +63 -5
  54. package/docs/details/storage.md +2 -0
  55. package/docs/goals/cli.md +255 -14
  56. package/docs/goals/database.md +244 -17
  57. package/docs/goals/document.md +1 -1
  58. package/docs/goals/mcp.md +83 -14
  59. package/docs/goals/query-parser.md +98 -4
  60. package/docs/goals/query-result.md +177 -10
  61. package/docs/goals/skills.md +72 -0
  62. package/docs/goals/worker-daemon.md +54 -7
  63. package/docs/modules/rust/fs-database/functions/integrity.md +11 -0
  64. package/docs/modules/rust/fs-database/functions/section.md +23 -6
  65. package/docs/modules/rust/fs-database/module.md +1 -0
  66. package/docs/modules/rust/fs-database/types/section.md +27 -1
  67. package/docs/modules/rust/overview.md +2 -1
  68. package/docs/modules/rust/query-parser/functions/module.md +2 -2
  69. package/docs/modules/rust/query-parser/functions/pipeline.md +44 -6
  70. package/docs/modules/rust/query-parser/module.md +5 -4
  71. package/docs/modules/rust/query-parser/types/keyphrase.md +11 -0
  72. package/docs/modules/rust/query-parser/types/module.md +3 -3
  73. package/docs/modules/typescript/shared/cli/functions.md +68 -3
  74. package/package.json +1 -1
  75. package/src/shared/cli/functions/dispatch-command.function.ts +53 -1
  76. package/src/shared/cli/functions/format-output.function.ts +36 -12
  77. package/src/shared/cli/functions/generate-help.function.ts +56 -1
  78. package/src/shared/cli/functions/install-default-skills.function.ts +296 -0
  79. package/src/shared/cli/functions/parse-args.function.ts +13 -0
  80. package/src/shared/cli/functions/run-check.function.ts +87 -6
  81. package/src/shared/cli/functions/run-export.function.ts +40 -0
  82. package/src/shared/cli/functions/run-gc.function.ts +6 -0
  83. package/src/shared/cli/functions/run-init.function.ts +2 -0
  84. package/src/shared/cli/functions/run-learn.function.ts +68 -16
  85. package/src/shared/cli/functions/run-recall.function.ts +66 -0
  86. package/src/shared/cli/functions/run-search.function.ts +58 -15
  87. package/src/shared/cli/index.ts +4 -0
  88. package/src/shared/cli/typings/cli-options.interface.ts +13 -0
  89. package/src/shared/cli/typings/learn-result.model.ts +16 -0
  90. package/src/shared/cli/typings/search-display.model.ts +18 -0
  91. package/src/shared/mcp/classes/mcp-server.class.ts +30 -1
  92. package/src/shared/mcp/functions/create-tool-definitions.function.ts +46 -5
  93. package/src/shared/mcp/functions/handle-tool-call.function.ts +70 -2
  94. package/src/shared/mcp/typings/mcp-client.interface.ts +45 -0
  95. package/src/shared/wasm-codec/functions/decode-add-section-result.function.ts +112 -0
  96. package/src/shared/wasm-codec/functions/decode-scored-results.function.ts +86 -0
  97. package/src/shared/wasm-codec/functions/decode-section-records.function.ts +87 -0
  98. package/src/shared/wasm-codec/functions/encode-add-section-params.function.ts +68 -0
  99. package/src/shared/wasm-codec/index.ts +5 -0
  100. package/src/shared/worker-client/classes/worker-client.class.ts +19 -0
  101. package/src/shared/worker-client/index.ts +4 -1
  102. package/src/shared/worker-client/typings/add-section-result.model.ts +30 -15
  103. package/src/shared/worker-client/typings/export-result.model.ts +14 -0
  104. package/src/shared/worker-client/typings/gc-result.model.ts +6 -0
  105. package/src/shared/worker-client/typings/search-result.model.ts +12 -0
  106. package/src/shared/worker-client/typings/section.model.ts +16 -0
  107. package/src/shared/worker-daemon/classes/worker-daemon.class.ts +315 -46
  108. package/src/shared/worker-daemon/functions/create-host-imports.function.ts +19 -4
  109. package/src/shared/worker-daemon/functions/create-ipc-server.function.ts +5 -1
  110. package/src/wasm/fs-database/kbdb_fs_database.d.ts +22 -1
  111. package/src/wasm/fs-database/kbdb_fs_database.js +28 -1
  112. package/src/wasm/fs-database/kbdb_fs_database_bg.wasm +0 -0
  113. package/src/wasm/fs-database/kbdb_fs_database_bg.wasm.d.ts +1 -0
  114. package/src/wasm/kb-worker/kbdb_worker.d.ts +48 -1
  115. package/src/wasm/kb-worker/kbdb_worker.js +60 -1
  116. package/src/wasm/kb-worker/kbdb_worker_bg.wasm +0 -0
  117. package/src/wasm/kb-worker/kbdb_worker_bg.wasm.d.ts +2 -0
  118. package/src/wasm/query-parser/kbdb_query_parser_bg.wasm +0 -0
@@ -0,0 +1,296 @@
1
+ import { createHash } from 'node:crypto';
2
+ import { existsSync, mkdirSync, writeFileSync } from 'node:fs';
3
+ import { join } from 'node:path';
4
+
5
+ /**
6
+ * RFC 4648 base32 alphabet (upper-case, no padding).
7
+ * Used to encode the SHA-256-derived kbid bytes.
8
+ */
9
+ const BASE32_ALPHABET = 'ABCDEFGHIJKLMNOPQRSTUVWXYZ234567';
10
+
11
+ /**
12
+ * Encodes a byte array as a lowercase base32 string without padding.
13
+ *
14
+ * Mirrors the `data_encoding::BASE32_NOPAD` encoding used in the
15
+ * Rust engine's `KbId::from_content`.
16
+ *
17
+ * @param bytes - raw bytes to encode
18
+ * @returns lowercase base32-encoded string
19
+ */
20
+ function base32Encode(bytes: Uint8Array): string {
21
+ let result = '';
22
+ let bits = 0;
23
+ let value = 0;
24
+ for (const byte of bytes) {
25
+ value = (value << 8) | byte;
26
+ bits += 8;
27
+ while (bits >= 5) {
28
+ result += BASE32_ALPHABET[(value >>> (bits - 5)) & 0x1f];
29
+ bits -= 5;
30
+ }
31
+ }
32
+ if (bits > 0) {
33
+ result += BASE32_ALPHABET[(value << (5 - bits)) & 0x1f];
34
+ }
35
+ return result.toLowerCase();
36
+ }
37
+
38
+ /**
39
+ * Computes a 26-character kbid for a section using the same algorithm
40
+ * as the Rust engine's `hash_section`.
41
+ *
42
+ * Algorithm:
43
+ * 1. Build input: `"section <mime> <byteLen>\0<content>"`
44
+ * 2. SHA-256 hash the input
45
+ * 3. Take the first 16 bytes
46
+ * 4. Base32 encode (RFC 4648, no padding), lowercased → 26 chars
47
+ *
48
+ * @param mimeType - MIME type string (e.g. `"text/markdown"`)
49
+ * @param content - UTF-8 section content string
50
+ * @returns 26-character lowercase base32 kbid
51
+ */
52
+ function computeSectionKbid(mimeType: string, content: string): string {
53
+ const contentBytes = Buffer.from(content, 'utf-8');
54
+ const header = `section ${mimeType} ${contentBytes.length.toString()}\0`;
55
+ const headerBytes = Buffer.from(header, 'utf-8');
56
+ const input = Buffer.concat([headerBytes, contentBytes]);
57
+ const hash = createHash('sha256').update(input).digest();
58
+ const truncated = new Uint8Array(hash.buffer, 0, 16);
59
+ return base32Encode(truncated);
60
+ }
61
+
62
+ /**
63
+ * Escapes backslashes and double-quotes in a string for TOML inline
64
+ * string values.
65
+ *
66
+ * Mirrors the `escape_toml_str` function in the Rust engine.
67
+ *
68
+ * @param s - string to escape
69
+ * @returns escaped string safe for use inside TOML double-quoted strings
70
+ */
71
+ function escapeToml(s: string): string {
72
+ return s.replace(/\\/g, '\\\\').replace(/"/g, '\\"');
73
+ }
74
+
75
+ /**
76
+ * Serializes a skill section record to the `.section` file format.
77
+ *
78
+ * Format:
79
+ * ```
80
+ * kbid = "..."
81
+ * section_type = "text/markdown"
82
+ * type_name = "skill"
83
+ * title = "..."
84
+ * description = "..."
85
+ * size = 0
86
+ * token_count = 0
87
+ * heading_token_count = 0
88
+ * body_token_count = 0
89
+ * code_token_count = 0
90
+ * created_at = 0
91
+ * checksum = "..."
92
+ *
93
+ * ---
94
+ * <json content>
95
+ * ```
96
+ *
97
+ * @param kbid - 26-char content-addressed identifier
98
+ * @param title - skill name used as section title
99
+ * @param description - human-readable description
100
+ * @param content - serialised JSON skill definition
101
+ * @returns file content string
102
+ */
103
+ function serializeSkillSection(
104
+ kbid: string,
105
+ title: string,
106
+ description: string,
107
+ content: string,
108
+ ): string {
109
+ const size = Buffer.byteLength(content, 'utf-8');
110
+ const header = [
111
+ `kbid = "${escapeToml(kbid)}"`,
112
+ `section_type = "text/markdown"`,
113
+ `type_name = "skill"`,
114
+ `title = "${escapeToml(title)}"`,
115
+ `description = "${escapeToml(description)}"`,
116
+ `size = ${size.toString()}`,
117
+ `token_count = 0`,
118
+ `heading_token_count = 0`,
119
+ `body_token_count = 0`,
120
+ `code_token_count = 0`,
121
+ `created_at = 0`,
122
+ `checksum = "${escapeToml(kbid)}"`,
123
+ '',
124
+ ].join('\n');
125
+ return `${header}\n---\n${content}`;
126
+ }
127
+
128
+ /** Descriptor for a skill to install. */
129
+ interface SkillDescriptor {
130
+ /** Unique skill name (kebab-case). */
131
+ readonly name: string;
132
+ /** Human-readable description. */
133
+ readonly description: string;
134
+ /** Template body text. */
135
+ readonly body: string;
136
+ }
137
+
138
+ /**
139
+ * Installs a single skill section file under `sectionsDir`.
140
+ *
141
+ * Creates the shard subdirectory if it does not exist. Skips
142
+ * writing if the section file already exists, preserving any
143
+ * user modifications.
144
+ *
145
+ * @param sectionsDir - absolute path to `.kbdb/sections`
146
+ * @param skill - skill definition to serialise and store
147
+ */
148
+ function installSkill(
149
+ sectionsDir: string,
150
+ skill: SkillDescriptor,
151
+ ): void {
152
+ const content = JSON.stringify({
153
+ name: skill.name,
154
+ description: skill.description,
155
+ arguments: [],
156
+ body: skill.body,
157
+ });
158
+ const kbid = computeSectionKbid('text/markdown', content);
159
+ const prefix = kbid.slice(0, 2);
160
+ const shardDir = join(sectionsDir, prefix);
161
+ const filePath = join(shardDir, `${kbid}.section`);
162
+
163
+ if (existsSync(filePath)) {
164
+ return;
165
+ }
166
+
167
+ mkdirSync(shardDir, { recursive: true });
168
+
169
+ const fileContent = serializeSkillSection(
170
+ kbid,
171
+ skill.name,
172
+ skill.description,
173
+ content,
174
+ );
175
+ writeFileSync(filePath, fileContent, 'utf-8');
176
+ }
177
+
178
+ /**
179
+ * Body text for the `query-guidance` default skill.
180
+ *
181
+ * Teaches AI agents how to effectively search the knowledge base
182
+ * using lexical BM25F queries.
183
+ */
184
+ const QUERY_GUIDANCE_BODY =
185
+ 'This knowledge base uses lexical search (stemmed BM25F), not' +
186
+ ' semantic/vector search. Follow these guidelines for effective' +
187
+ ' retrieval:\n\n' +
188
+ '1. Issue MULTIPLE short keyword queries rather than one long' +
189
+ ' natural-language question. Each query should target 2-3' +
190
+ ' specific terms.\n\n' +
191
+ '2. EXPAND SYNONYMS yourself. "login" will not match "sign-in"' +
192
+ ' unless both terms appear in the content. Try alternative' +
193
+ ' phrasings: search for "login", then "sign-in", then' +
194
+ ' "authentication".\n\n' +
195
+ '3. USE BOOLEAN AND PHRASE SYNTAX:\n' +
196
+ ' - Exact phrases: "error handling"\n' +
197
+ ' - Boolean AND: auth AND token\n' +
198
+ ' - Boolean OR: cache OR redis\n' +
199
+ ' - Exclusion: auth -oauth\n\n' +
200
+ '4. LEAN ON HEADINGS. Headings carry 2x weight in ranking.' +
201
+ ' Search for terms that are likely in section titles.\n\n' +
202
+ '5. STEMMING IS AUTOMATIC. "connections" matches "connect",' +
203
+ ' "running" matches "run". You do not need to try inflected' +
204
+ ' forms.\n\n' +
205
+ '6. USE RECALL AFTER SEARCH. After finding relevant sections' +
206
+ ' with search, use recall with increasing depth (0-3) to' +
207
+ ' progressively expand context: depth 0 = full content, depth' +
208
+ ' 1 = parent documents + back-references, depth 2 = siblings' +
209
+ ' + forward references, depth 3 = referenced sections.\n\n' +
210
+ '7. CODE IDENTIFIERS ARE SPLIT. camelCase and snake_case names' +
211
+ ' are decomposed: getUserById becomes searchable as "get",' +
212
+ ' "user", "by", "id".\n\n' +
213
+ '8. PAGINATION. Default limit is 20 results. Use offset for' +
214
+ ' additional pages if total > limit.';
215
+
216
+ /**
217
+ * Body text for the `memory-protocol` default skill.
218
+ *
219
+ * Teaches AI agents when and what to store in the knowledge base
220
+ * to keep it accurate and useful.
221
+ */
222
+ const MEMORY_PROTOCOL_BODY =
223
+ 'Follow these guidelines to keep the knowledge base accurate' +
224
+ ' and useful:\n\n' +
225
+ '1. STORE DURABLE FACTS: decisions, architecture choices,' +
226
+ ' configuration values, corrections to prior knowledge, API' +
227
+ ' contracts, and resolved incidents. These are the facts that' +
228
+ ' matter weeks later.\n\n' +
229
+ '2. DO NOT STORE: transient conversation context, debugging' +
230
+ ' output, ephemeral task state, raw log dumps, or information' +
231
+ ' that changes every session. The brain should contain' +
232
+ ' knowledge, not activity logs.\n\n' +
233
+ '3. SEARCH BEFORE STORING. Always search the knowledge base' +
234
+ ' before adding new content. If a section already covers the' +
235
+ ' topic, use learn with --replace to update it rather than' +
236
+ ' creating a duplicate.\n\n' +
237
+ '4. USE --replace FOR CORRECTIONS. When a fact changes (e.g.,' +
238
+ ' rate limit updated from 100 to 200 req/min), re-learn the' +
239
+ ' corrected content with --replace so the old version is' +
240
+ ' superseded. Without --replace, both the old and new versions' +
241
+ ' coexist with equal ranking.\n\n' +
242
+ '5. ATTACH TAGS for scoping. Use tags like project name,' +
243
+ ' domain, or topic (e.g., --tags auth,api,v2) so queries can' +
244
+ ' be filtered by scope later.\n\n' +
245
+ '6. PREFER RECENT RESULTS. When search returns multiple' +
246
+ ' sections covering the same topic, prefer the one with the' +
247
+ ' most recent created_at timestamp -- it is more likely to' +
248
+ ' reflect the current state.\n\n' +
249
+ '7. USE RECALL AFTER SEARCH. After finding relevant sections,' +
250
+ ' use recall with increasing depth (0-3) to expand context' +
251
+ ' progressively before making decisions.\n\n' +
252
+ '8. INCLUDE HEADINGS. Always provide a descriptive title when' +
253
+ ' storing content. Headings carry 2x ranking weight -- omitting' +
254
+ ' them significantly reduces searchability.';
255
+
256
+ /**
257
+ * Default skills installed on every fresh `kbdb db init`.
258
+ *
259
+ * These provide baseline guidance that improves AI agent
260
+ * effectiveness out of the box. They are regular skill sections
261
+ * and can be deleted by users.
262
+ */
263
+ const DEFAULT_SKILLS: readonly SkillDescriptor[] = [
264
+ {
265
+ name: 'query-guidance',
266
+ description:
267
+ 'How to effectively search this knowledge base.' +
268
+ ' Read this before issuing search queries.',
269
+ body: QUERY_GUIDANCE_BODY,
270
+ },
271
+ {
272
+ name: 'memory-protocol',
273
+ description:
274
+ 'Guidelines for what to store in this knowledge base' +
275
+ ' and when. Read this before using the learn tool.',
276
+ body: MEMORY_PROTOCOL_BODY,
277
+ },
278
+ ];
279
+
280
+ /**
281
+ * Writes the two default skills (`query-guidance` and
282
+ * `memory-protocol`) as section files directly into the `.kbdb`
283
+ * directory scaffold.
284
+ *
285
+ * Called by `runInit` after the directory scaffold is created.
286
+ * Does not require a running worker daemon. Skips any skill whose
287
+ * section file already exists so user modifications are preserved.
288
+ *
289
+ * @param dbDir - absolute path to the `.kbdb` directory
290
+ */
291
+ export function installDefaultSkills(dbDir: string): void {
292
+ const sectionsDir = join(dbDir, 'sections');
293
+ for (const skill of DEFAULT_SKILLS) {
294
+ installSkill(sectionsDir, skill);
295
+ }
296
+ }
@@ -115,6 +115,19 @@ export function parseArgs(argv: string[]): CliOptions {
115
115
  case '--skills':
116
116
  options.skills = (argv[++i] ?? '').split(',');
117
117
  break;
118
+ case '--tags':
119
+ options.tags = (argv[++i] ?? '').split(',');
120
+ break;
121
+ case '--replace':
122
+ options.replace = true;
123
+ break;
124
+ case '--depth':
125
+ case '-d':
126
+ options.depth = Math.min(
127
+ 3,
128
+ Math.max(0, parseInt(argv[++i] ?? '', 10) || 0),
129
+ );
130
+ break;
118
131
  case '--content':
119
132
  case '-c':
120
133
  options.content = true;
@@ -25,6 +25,17 @@ interface IntegrityError {
25
25
  readonly detail: string;
26
26
  }
27
27
 
28
+ /**
29
+ * A single entry in the divergent-content map.
30
+ * Groups kbids under a common source key.
31
+ */
32
+ interface DivergentContentEntry {
33
+ /** Source key that has multiple divergent content versions. */
34
+ readonly sourceKey: string;
35
+ /** Kbids of sections sharing this source key but with different content. */
36
+ readonly kbids: string[];
37
+ }
38
+
28
39
  /**
29
40
  * Integrity check report returned by the daemon.
30
41
  */
@@ -41,6 +52,38 @@ interface IntegrityCheckResult {
41
52
  readonly references_checked: number;
42
53
  /** Time taken to complete the check, in milliseconds. */
43
54
  readonly elapsed_ms: number;
55
+ /**
56
+ * Sections that share a source key but have divergent content.
57
+ * Each entry groups the kbids under a common source key.
58
+ * Empty when no divergent content is found.
59
+ */
60
+ readonly divergent_content: DivergentContentEntry[];
61
+ }
62
+
63
+ /** Raw daemon response from `integrityCheck`. */
64
+ interface RawIntegrityCheckResult {
65
+ /** `true` when all checks passed and no errors were found. */
66
+ readonly ok: boolean;
67
+ /** All integrity errors found; empty when `ok` is `true`. */
68
+ readonly errors: IntegrityError[];
69
+ /** Total number of sections examined. */
70
+ readonly sections_checked: number;
71
+ /** Total number of documents examined. */
72
+ readonly documents_checked: number;
73
+ /** Total number of marker references checked. */
74
+ readonly references_checked: number;
75
+ /** Time taken to complete the check, in milliseconds. */
76
+ readonly elapsed_ms: number;
77
+ /**
78
+ * Encoded divergent-content map.
79
+ *
80
+ * Format: `sk1:kbid1,kbid2;sk2:kbid3,kbid4`
81
+ *
82
+ * Each semicolon-separated segment is a source key followed by
83
+ * a colon and a comma-separated list of kbids. Absent when no
84
+ * divergent content exists.
85
+ */
86
+ readonly divergent_content?: string;
44
87
  }
45
88
 
46
89
  /**
@@ -56,23 +99,61 @@ export interface CheckClient {
56
99
  * @returns a report of all issues found; `ok` is `true` when
57
100
  * no errors were detected
58
101
  */
59
- integrityCheck(): Promise<IntegrityCheckResult>;
102
+ integrityCheck(): Promise<RawIntegrityCheckResult>;
103
+ }
104
+
105
+ /**
106
+ * Parses the encoded `divergent_content` string from the daemon
107
+ * into a structured array.
108
+ *
109
+ * Input format: `sk1:kbid1,kbid2;sk2:kbid3,kbid4`
110
+ *
111
+ * @param encoded - raw encoded string, or empty / absent
112
+ * @returns structured entries grouped by source key
113
+ */
114
+ function parseDivergentContent(
115
+ encoded: string | undefined,
116
+ ): DivergentContentEntry[] {
117
+ if (!encoded) return [];
118
+ return encoded
119
+ .split(';')
120
+ .filter((segment) => segment.includes(':'))
121
+ .map((segment) => {
122
+ const colonIdx = segment.indexOf(':');
123
+ const sourceKey = segment.slice(0, colonIdx);
124
+ const kbids = segment
125
+ .slice(colonIdx + 1)
126
+ .split(',')
127
+ .filter((k) => k.length > 0);
128
+ return { sourceKey, kbids };
129
+ })
130
+ .filter((entry) => entry.kbids.length > 0);
60
131
  }
61
132
 
62
133
  /**
63
134
  * Runs an integrity check on the database and returns the report.
64
135
  *
65
- * Delegates to `client.integrityCheck()`. The caller is responsible
66
- * for printing the result to stdout and setting the process exit code
67
- * based on `result.ok`.
136
+ * Delegates to `client.integrityCheck()`, then parses the encoded
137
+ * `divergent_content` field into structured form. The caller is
138
+ * responsible for printing the result to stdout and setting the
139
+ * process exit code based on `result.ok`.
68
140
  *
69
141
  * @param client - check client to delegate to
70
142
  * @param _options - parsed CLI options (unused; reserved for future flags)
71
- * @returns the integrity check report from the daemon
143
+ * @returns the integrity check report with parsed `divergent_content`
72
144
  */
73
145
  export async function runCheck(
74
146
  client: CheckClient,
75
147
  _options: CliOptions,
76
148
  ): Promise<IntegrityCheckResult> {
77
- return client.integrityCheck();
149
+ const raw = await client.integrityCheck();
150
+ return {
151
+ ok: raw.ok,
152
+ errors: raw.errors,
153
+ sections_checked: raw.sections_checked,
154
+ documents_checked: raw.documents_checked,
155
+ references_checked: raw.references_checked,
156
+ elapsed_ms: raw.elapsed_ms,
157
+ divergent_content: parseDivergentContent(raw.divergent_content),
158
+ };
78
159
  }
@@ -0,0 +1,40 @@
1
+ import type { CliOptions } from '../typings/cli-options.interface.ts';
2
+ import type { ExportResult } from '../../worker-client/typings/export-result.model.ts';
3
+
4
+ /**
5
+ * Minimal client interface required by `runExport`.
6
+ *
7
+ * Decouples the CLI function from the concrete `WorkerClient`
8
+ * implementation, making it straightforward to test with mocks.
9
+ */
10
+ export interface ExportClient {
11
+ /**
12
+ * Exports the database to the given directory path.
13
+ *
14
+ * @param params - optional target path; the daemon defaults to
15
+ * `./kbdb-export/` when `path` is `undefined`
16
+ * @returns export report with the resolved path and file counts
17
+ */
18
+ export(params: { path?: string }): Promise<ExportResult>;
19
+ }
20
+
21
+ /**
22
+ * Exports the knowledge-base database to a target directory via the
23
+ * provided client.
24
+ *
25
+ * The target path is taken from `options.args[0]`. When no argument
26
+ * is given the daemon resolves its own default (`./kbdb-export/`).
27
+ *
28
+ * @param client - export client to delegate to
29
+ * @param options - parsed CLI options whose first positional
30
+ * argument, if present, is the target directory path
31
+ * @returns the export result containing the resolved path and file
32
+ * counts
33
+ */
34
+ export async function runExport(
35
+ client: ExportClient,
36
+ options: CliOptions,
37
+ ): Promise<ExportResult> {
38
+ const path = options.args[0];
39
+ return client.export({ path });
40
+ }
@@ -10,6 +10,12 @@ interface GcResult {
10
10
  readonly removed_bytes: number;
11
11
  /** Time taken to complete garbage collection, in milliseconds. */
12
12
  readonly elapsed_ms: number;
13
+ /**
14
+ * Number of sections whose `source_path` file no longer
15
+ * contains their heading text (advisory; not auto-removed).
16
+ * Absent on older daemon versions.
17
+ */
18
+ readonly stale_sources?: number;
13
19
  }
14
20
 
15
21
  /**
@@ -1,6 +1,7 @@
1
1
  import { existsSync, mkdirSync, writeFileSync } from 'node:fs';
2
2
  import { join, resolve } from 'node:path';
3
3
  import type { CliOptions } from '../typings/cli-options.interface.ts';
4
+ import { installDefaultSkills } from './install-default-skills.function.ts';
4
5
 
5
6
  /**
6
7
  * Result returned by `runInit` describing whether the directory was
@@ -82,6 +83,7 @@ export function runInit(options: CliOptions): InitResult {
82
83
  };
83
84
  }
84
85
  createScaffold(dbDir);
86
+ installDefaultSkills(dbDir);
85
87
  return { ok: true, targetPath: dbDir };
86
88
  } catch (err: unknown) {
87
89
  const errorMessage =
@@ -4,6 +4,30 @@ import type { CliOptions } from '../typings/cli-options.interface.ts';
4
4
  import type { LearnResult } from '../typings/learn-result.model.ts';
5
5
  import { SUPPORTED_EXTENSIONS } from '../constants/defaults.constant.ts';
6
6
 
7
+ /** Enriched add-section result returned by the daemon. */
8
+ interface AddSectionResult {
9
+ /** Knowledge-base identifier assigned to the new section. */
10
+ readonly kbid: string;
11
+ /**
12
+ * Kbid of the section superseded via `--replace`,
13
+ * or `null` / `undefined` when none.
14
+ */
15
+ readonly superseded?: string | null;
16
+ /**
17
+ * Advisory warning strings (e.g. `"heading_missing"`).
18
+ * May be absent from older daemon versions.
19
+ */
20
+ readonly warnings?: string[];
21
+ /**
22
+ * Near-duplicate sections with similarity >= 0.85.
23
+ * May be absent from older daemon versions.
24
+ */
25
+ readonly near_duplicates?: {
26
+ readonly kbid: string;
27
+ readonly similarity: number;
28
+ }[];
29
+ }
30
+
7
31
  /**
8
32
  * Minimal client interface required by `runLearn`.
9
33
  *
@@ -14,9 +38,9 @@ export interface LearnClient {
14
38
  /**
15
39
  * Ingests a content section into the knowledge base.
16
40
  *
17
- * @param params - section type label and raw text content
18
- * @returns result object containing the knowledge-base identifier
19
- * assigned to the section
41
+ * @param params - section metadata and raw text content
42
+ * @returns enriched result containing the new kbid plus
43
+ * optional supersession, warnings, and near-duplicate info
20
44
  */
21
45
  addSection(params: {
22
46
  sectionType: string;
@@ -24,7 +48,14 @@ export interface LearnClient {
24
48
  title?: string;
25
49
  description?: string;
26
50
  docid?: string;
27
- }): Promise<{ kbid: string }>;
51
+ tags?: string[];
52
+ replace?: boolean;
53
+ /**
54
+ * Absolute path to the source file. Required for supersession
55
+ * via source key when `replace` is `true`.
56
+ */
57
+ sourcePath?: string;
58
+ }): Promise<AddSectionResult>;
28
59
  }
29
60
 
30
61
  /** Injectable I/O for testing without real stdin. */
@@ -67,13 +98,10 @@ export async function runLearn(
67
98
  title: options.title,
68
99
  description: options.description,
69
100
  docid: options.docid,
101
+ tags: options.tags,
102
+ replace: options.replace,
70
103
  });
71
- results.push({
72
- kbid: result.kbid,
73
- path: '-',
74
- size: content.length,
75
- skipped: false,
76
- });
104
+ results.push(mapAddSectionResult(result, '-', content.length));
77
105
  continue;
78
106
  }
79
107
 
@@ -84,20 +112,44 @@ export async function runLearn(
84
112
  sectionType: 'text',
85
113
  content,
86
114
  title: options.title,
115
+ description: options.description,
87
116
  docid: options.docid,
117
+ tags: options.tags,
118
+ replace: options.replace,
119
+ sourcePath: file,
88
120
  });
89
- results.push({
90
- kbid: result.kbid,
91
- path: file,
92
- size: content.length,
93
- skipped: false,
94
- });
121
+ results.push(
122
+ mapAddSectionResult(result, file, content.length),
123
+ );
95
124
  }
96
125
  }
97
126
 
98
127
  return results;
99
128
  }
100
129
 
130
+ /**
131
+ * Maps an `AddSectionResult` to a `LearnResult` record.
132
+ *
133
+ * @param result - raw result from `client.addSection()`
134
+ * @param path - source file path or `'-'` for stdin
135
+ * @param size - UTF-8 character count of the ingested content
136
+ */
137
+ function mapAddSectionResult(
138
+ result: AddSectionResult,
139
+ path: string,
140
+ size: number,
141
+ ): LearnResult {
142
+ return {
143
+ kbid: result.kbid,
144
+ path,
145
+ size,
146
+ skipped: false,
147
+ superseded: result.superseded ?? null,
148
+ warnings: result.warnings ?? [],
149
+ near_duplicates: result.near_duplicates ?? [],
150
+ };
151
+ }
152
+
101
153
  function readProcessStdin(): Promise<string> {
102
154
  return new Promise<string>((resolve) => {
103
155
  const chunks: Buffer[] = [];