@lemoncat7/dsh-knowledge 1.2.0 → 2.0.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 (97) hide show
  1. package/README.md +22 -2
  2. package/docs/architecture.md +30 -4
  3. package/docs/requirements.md +10 -0
  4. package/lib/api.d.ts.map +1 -1
  5. package/lib/api.js +194 -1
  6. package/lib/api.js.map +1 -1
  7. package/lib/atomic-file.d.ts +1 -1
  8. package/lib/atomic-file.d.ts.map +1 -1
  9. package/lib/atomic-file.js +0 -1
  10. package/lib/atomic-file.js.map +1 -1
  11. package/lib/client.d.ts.map +1 -1
  12. package/lib/client.js +37 -34
  13. package/lib/client.js.map +2 -2
  14. package/lib/content-safety.d.ts +12 -0
  15. package/lib/content-safety.d.ts.map +1 -0
  16. package/lib/content-safety.js +38 -0
  17. package/lib/content-safety.js.map +1 -0
  18. package/lib/domain.d.ts +13 -0
  19. package/lib/domain.d.ts.map +1 -1
  20. package/lib/domain.js.map +1 -1
  21. package/lib/extraction.d.ts.map +1 -1
  22. package/lib/extraction.js +8 -2
  23. package/lib/extraction.js.map +1 -1
  24. package/lib/index.d.ts +1 -0
  25. package/lib/index.d.ts.map +1 -1
  26. package/lib/index.js +6 -1
  27. package/lib/index.js.map +1 -1
  28. package/lib/local-provider.d.ts +24 -1
  29. package/lib/local-provider.d.ts.map +1 -1
  30. package/lib/local-provider.js +302 -2
  31. package/lib/local-provider.js.map +1 -1
  32. package/lib/management-proxy.d.ts.map +1 -1
  33. package/lib/management-proxy.js +22 -13
  34. package/lib/management-proxy.js.map +1 -1
  35. package/lib/note-reference-handle.d.ts +15 -0
  36. package/lib/note-reference-handle.d.ts.map +1 -0
  37. package/lib/note-reference-handle.js +54 -0
  38. package/lib/note-reference-handle.js.map +1 -0
  39. package/lib/note-reference-tools.d.ts +6 -0
  40. package/lib/note-reference-tools.d.ts.map +1 -0
  41. package/lib/note-reference-tools.js +119 -0
  42. package/lib/note-reference-tools.js.map +1 -0
  43. package/lib/note-tools.d.ts +6 -0
  44. package/lib/note-tools.d.ts.map +1 -0
  45. package/lib/note-tools.js +283 -0
  46. package/lib/note-tools.js.map +1 -0
  47. package/lib/notes/domain.d.ts +44 -0
  48. package/lib/notes/domain.d.ts.map +1 -0
  49. package/lib/notes/domain.js +38 -0
  50. package/lib/notes/domain.js.map +1 -0
  51. package/lib/notes/store.d.ts +39 -0
  52. package/lib/notes/store.d.ts.map +1 -0
  53. package/lib/notes/store.js +407 -0
  54. package/lib/notes/store.js.map +1 -0
  55. package/lib/provider.d.ts +17 -0
  56. package/lib/provider.d.ts.map +1 -1
  57. package/lib/remote-provider.d.ts +19 -0
  58. package/lib/remote-provider.d.ts.map +1 -1
  59. package/lib/remote-provider.js +139 -13
  60. package/lib/remote-provider.js.map +1 -1
  61. package/lib/runtime.d.ts +1 -0
  62. package/lib/runtime.d.ts.map +1 -1
  63. package/lib/runtime.js.map +1 -1
  64. package/lib/theme-bridge.js +12 -12
  65. package/lib/theme-bridge.js.map +1 -1
  66. package/lib/tool-authorization.d.ts +14 -0
  67. package/lib/tool-authorization.d.ts.map +1 -0
  68. package/lib/tool-authorization.js +124 -0
  69. package/lib/tool-authorization.js.map +1 -0
  70. package/lib/tool-input.d.ts +6 -0
  71. package/lib/tool-input.d.ts.map +1 -0
  72. package/lib/tool-input.js +27 -0
  73. package/lib/tool-input.js.map +1 -0
  74. package/lib/tools.d.ts +2 -1
  75. package/lib/tools.d.ts.map +1 -1
  76. package/lib/tools.js +29 -179
  77. package/lib/tools.js.map +1 -1
  78. package/lib/tracking.d.ts +29 -0
  79. package/lib/tracking.d.ts.map +1 -0
  80. package/lib/tracking.js +141 -0
  81. package/lib/tracking.js.map +1 -0
  82. package/lib/web-markdown-preview.d.ts.map +1 -1
  83. package/lib/web-markdown-preview.js +10 -0
  84. package/lib/web-markdown-preview.js.map +1 -1
  85. package/lib/web-note-editor.d.ts +44 -0
  86. package/lib/web-note-editor.d.ts.map +1 -0
  87. package/lib/web-note-editor.js +118 -0
  88. package/lib/web-note-editor.js.map +1 -0
  89. package/lib/web.d.ts.map +1 -1
  90. package/lib/web.js +2 -1
  91. package/lib/web.js.map +1 -1
  92. package/package.json +12 -1
  93. package/web/app.js +1617 -89
  94. package/web/index.html +1 -0
  95. package/web/markdown-preview.js +8 -8
  96. package/web/note-editor.js +220 -0
  97. package/web/styles.css +512 -160
@@ -1,6 +1,7 @@
1
1
  import { dirname, join } from 'node:path';
2
- import { createHash, randomBytes } from 'node:crypto';
2
+ import { createHash, randomBytes, randomUUID } from 'node:crypto';
3
3
  import { mkdirSync } from 'node:fs';
4
+ import { tmpdir } from 'node:os';
4
5
  import { DatabaseSync } from 'node:sqlite';
5
6
  import { contentHash, DEFAULT_KNOWLEDGE_BASE_ID, newId, normalizeDraft, normalizeKnowledgeBaseDraft, normalizeKnowledgeMountDraft, normalizeKnowledgeSettings, nowIso, } from './domain.js';
6
7
  import { renderKnowledgeMarkdown } from './documents/markdown.js';
@@ -8,6 +9,7 @@ import { knowledgeDocumentPath } from './documents/path.js';
8
9
  import { KnowledgeDocumentStore } from './documents/store.js';
9
10
  import { enqueueDocumentProjection } from './documents/projection-queue.js';
10
11
  import { mergeKnowledgeBodies } from './knowledge-merge.js';
12
+ import { NoteStore } from './notes/store.js';
11
13
  const ENTRY_COLUMNS = `
12
14
  id, knowledge_base_id, title, body, type, tags_json, scope_kind, scope_id, confidence,
13
15
  status, document_state, finalized_at, finalization_note, version, source_json, created_at, updated_at
@@ -22,6 +24,7 @@ const JOINED_ENTRY_COLUMNS = `
22
24
  const EXTRACTION_LEASE_MS = 15 * 60 * 1000;
23
25
  export class LocalKnowledgeProvider {
24
26
  mode = 'local';
27
+ notes;
25
28
  db;
26
29
  documentStore;
27
30
  documentsReady;
@@ -29,6 +32,10 @@ export class LocalKnowledgeProvider {
29
32
  constructor(path) {
30
33
  if (path !== ':memory:')
31
34
  mkdirSync(dirname(path), { recursive: true });
35
+ const inMemory = path === ':memory:';
36
+ this.notes = new NoteStore(inMemory
37
+ ? join(tmpdir(), `dsh-knowledge-notes-${randomUUID()}`)
38
+ : join(dirname(path), 'notes'), inMemory);
32
39
  this.documentStore = new KnowledgeDocumentStore(join(dirname(path), 'documents'));
33
40
  this.db = new DatabaseSync(path);
34
41
  this.db.exec('PRAGMA journal_mode = WAL; PRAGMA foreign_keys = ON; PRAGMA busy_timeout = 5000;');
@@ -37,7 +44,7 @@ export class LocalKnowledgeProvider {
37
44
  }
38
45
  migrate() {
39
46
  let version = Number(this.db.prepare('PRAGMA user_version').get().user_version ?? 0);
40
- if (version > 9)
47
+ if (version > 10)
41
48
  throw new Error(`knowledge database schema ${version} is newer than this plugin supports`);
42
49
  if (version === 0)
43
50
  this.db.exec(`
@@ -246,11 +253,52 @@ export class LocalKnowledgeProvider {
246
253
  PRAGMA user_version = 9;
247
254
  COMMIT;
248
255
  `);
256
+ if (version <= 8)
257
+ version = 9;
258
+ if (version === 9) {
259
+ this.db.exec('BEGIN IMMEDIATE');
260
+ try {
261
+ this.db.exec(`
262
+ CREATE TABLE knowledge_note_references (
263
+ knowledge_id TEXT NOT NULL REFERENCES knowledge_entries(id) ON DELETE CASCADE,
264
+ note_id TEXT NOT NULL,
265
+ source TEXT NOT NULL CHECK(source IN ('user','agent','legacy')),
266
+ source_session_id TEXT,
267
+ created_at TEXT NOT NULL,
268
+ PRIMARY KEY(knowledge_id, note_id)
269
+ );
270
+ CREATE INDEX knowledge_note_references_note ON knowledge_note_references(note_id, created_at DESC);
271
+ `);
272
+ this.backfillLegacyNoteReferences();
273
+ this.db.exec('PRAGMA user_version = 10; COMMIT');
274
+ }
275
+ catch (error) {
276
+ this.db.exec('ROLLBACK');
277
+ throw error;
278
+ }
279
+ }
249
280
  // Alpha v2 used a migration note as the default base's routing description.
250
281
  // Clear only that exact placeholder so existing user-authored descriptions stay untouched.
251
282
  this.db.prepare("UPDATE knowledge_bases SET description='' WHERE id=? AND description=?")
252
283
  .run(DEFAULT_KNOWLEDGE_BASE_ID, '由 0.2 版本迁移的知识。');
253
284
  }
285
+ backfillLegacyNoteReferences() {
286
+ const insert = this.db.prepare(`
287
+ INSERT OR IGNORE INTO knowledge_note_references(knowledge_id,note_id,source,source_session_id,created_at)
288
+ VALUES(?,?,'legacy',NULL,?)
289
+ `);
290
+ const timestamp = nowIso();
291
+ const rows = this.db.prepare("SELECT id,body FROM knowledge_entries WHERE body LIKE '%note://note_%'").all();
292
+ for (const row of rows) {
293
+ const noteIds = String(row.body).match(/note:\/\/(note_[a-f0-9]{32})/giu) ?? [];
294
+ for (const reference of new Set(noteIds)) {
295
+ const noteId = reference.slice('note://'.length).toLocaleLowerCase();
296
+ const note = this.notes.get(noteId);
297
+ if (note !== undefined && note.kind !== 'folder')
298
+ insert.run(String(row.id), note.id, timestamp);
299
+ }
300
+ }
301
+ }
254
302
  assertOpen() {
255
303
  if (this.closed)
256
304
  throw new Error('knowledge provider is closed');
@@ -429,6 +477,68 @@ export class LocalKnowledgeProvider {
429
477
  const sql = `SELECT * FROM knowledge_documents${where.length === 0 ? '' : ` WHERE ${where.join(' AND ')}`} ORDER BY knowledge_base_id, CASE WHEN rel_path='README.md' THEN 0 ELSE 1 END, rel_path`;
430
478
  return this.db.prepare(sql).all(...args).map(rowToDocument);
431
479
  }
480
+ async listDocumentIndex(request) {
481
+ this.assertOpen();
482
+ await this.documentsReady;
483
+ const limit = Math.max(1, Math.min(request.limit, 100));
484
+ const where = [];
485
+ const args = [];
486
+ const knowledgeBaseIds = request.knowledgeBaseIds === undefined
487
+ ? undefined
488
+ : [...new Set(request.knowledgeBaseIds.map(id => id.trim()).filter(Boolean))];
489
+ if (knowledgeBaseIds !== undefined && knowledgeBaseIds.length === 0)
490
+ return { items: [], total: 0 };
491
+ if (knowledgeBaseIds !== undefined) {
492
+ where.push(`knowledge_base_id IN (${knowledgeBaseIds.map(() => '?').join(',')})`);
493
+ args.push(...knowledgeBaseIds);
494
+ }
495
+ if (request.activeKnowledgeBasesOnly) {
496
+ where.push("knowledge_base_id IN (SELECT id FROM knowledge_bases WHERE status='active')");
497
+ }
498
+ const query = request.query?.trim();
499
+ if (query) {
500
+ const like = `%${escapeSqlLike(query)}%`;
501
+ where.push(`(title LIKE ? ESCAPE '\\' OR rel_path LIKE ? ESCAPE '\\' OR content LIKE ? ESCAPE '\\')`);
502
+ args.push(like, like, like);
503
+ }
504
+ const sourceWhere = where.length === 0 ? '' : ` WHERE ${where.join(' AND ')}`;
505
+ const cursorWhere = [];
506
+ const cursorArgs = [];
507
+ if (request.cursor !== undefined) {
508
+ const cursor = decodeDocumentCursor(request.cursor);
509
+ cursorWhere.push(`(
510
+ knowledge_base_id > ? OR
511
+ (knowledge_base_id = ? AND sort_rank > ?) OR
512
+ (knowledge_base_id = ? AND sort_rank = ? AND rel_path > ?) OR
513
+ (knowledge_base_id = ? AND sort_rank = ? AND rel_path = ? AND id > ?)
514
+ )`);
515
+ cursorArgs.push(cursor.knowledgeBaseId, cursor.knowledgeBaseId, cursor.sortRank, cursor.knowledgeBaseId, cursor.sortRank, cursor.relPath, cursor.knowledgeBaseId, cursor.sortRank, cursor.relPath, cursor.id);
516
+ }
517
+ const rows = this.db.prepare(`
518
+ WITH document_index AS (
519
+ SELECT
520
+ id, knowledge_base_id, rel_path, title, entry_count, content_hash,
521
+ document_state, finalized_at, finalization_note, created_at, updated_at,
522
+ CASE WHEN rel_path='README.md' THEN 0 ELSE 1 END AS sort_rank
523
+ FROM knowledge_documents${sourceWhere}
524
+ )
525
+ SELECT * FROM document_index
526
+ ${cursorWhere.length === 0 ? '' : `WHERE ${cursorWhere.join(' AND ')}`}
527
+ ORDER BY knowledge_base_id, sort_rank, rel_path, id
528
+ LIMIT ?
529
+ `).all(...args, ...cursorArgs, limit + 1);
530
+ const pageRows = rows.slice(0, limit);
531
+ const items = pageRows.map(rowToDocumentSummary);
532
+ const last = pageRows.at(-1);
533
+ const total = Number(this.db.prepare(`SELECT COUNT(*) AS total FROM knowledge_documents${sourceWhere}`).get(...args).total ?? 0);
534
+ return {
535
+ items,
536
+ total,
537
+ ...rows.length <= limit || last === undefined ? {} : {
538
+ nextCursor: encodeDocumentCursor(String(last.knowledge_base_id), Number(last.sort_rank), String(last.rel_path), String(last.id)),
539
+ },
540
+ };
541
+ }
432
542
  async getDocument(id) {
433
543
  this.assertOpen();
434
544
  await this.documentsReady;
@@ -871,6 +981,152 @@ export class LocalKnowledgeProvider {
871
981
  if (current !== undefined)
872
982
  await this.syncKnowledgeDocumentsQueued(current.knowledgeBaseId);
873
983
  }
984
+ async listNotes(request = {}) {
985
+ this.assertOpen();
986
+ return this.notes.list(request);
987
+ }
988
+ async getNote(id) {
989
+ this.assertOpen();
990
+ return this.notes.get(id);
991
+ }
992
+ async readNote(id) {
993
+ this.assertOpen();
994
+ return this.notes.read(id);
995
+ }
996
+ async createNoteFolder(name, parentId = null) {
997
+ this.assertOpen();
998
+ return this.notes.createFolder(name, parentId);
999
+ }
1000
+ async createNoteDocument(name, parentId = null, content = '') {
1001
+ this.assertOpen();
1002
+ return this.notes.createDocument(name, parentId, content);
1003
+ }
1004
+ async updateNoteContent(id, content) {
1005
+ this.assertOpen();
1006
+ return this.notes.updateContent(id, content);
1007
+ }
1008
+ async renameNote(id, name) {
1009
+ this.assertOpen();
1010
+ return this.notes.rename(id, name);
1011
+ }
1012
+ async moveNote(id, parentId) {
1013
+ this.assertOpen();
1014
+ return this.notes.move(id, parentId);
1015
+ }
1016
+ async deleteNote(id) {
1017
+ this.assertOpen();
1018
+ const noteIds = this.notes.subtree(id).filter(node => node.kind !== 'folder').map(node => node.id);
1019
+ const references = this.noteReferencesForNotes(noteIds);
1020
+ if (references.length > 0)
1021
+ throw conflict(`note content is referenced by ${references.length} knowledge document(s)`);
1022
+ await this.notes.delete(id);
1023
+ this.deleteNoteReferences(noteIds);
1024
+ }
1025
+ async searchNotes(query, limit) {
1026
+ this.assertOpen();
1027
+ const value = query.trim();
1028
+ if (value.length === 0)
1029
+ return [];
1030
+ const boundedLimit = Math.max(1, Math.min(Math.trunc(limit), 100));
1031
+ return this.notes.list({ query: value, limit: Math.min(500, boundedLimit * 3) })
1032
+ .filter(node => node.kind !== 'folder')
1033
+ .slice(0, boundedLimit);
1034
+ }
1035
+ async listKnowledgeNoteReferences(knowledgeId) {
1036
+ this.assertOpen();
1037
+ const entry = await this.get(knowledgeId);
1038
+ if (entry === undefined)
1039
+ throw notFound('knowledge entry', knowledgeId);
1040
+ const rows = this.db.prepare(`
1041
+ SELECT knowledge_id,note_id,source,source_session_id,created_at
1042
+ FROM knowledge_note_references
1043
+ WHERE knowledge_id=?
1044
+ ORDER BY created_at,note_id
1045
+ `).all(knowledgeId);
1046
+ return rows.flatMap(row => {
1047
+ const note = this.notes.get(String(row.note_id));
1048
+ if (note === undefined || note.kind === 'folder')
1049
+ return [];
1050
+ return [{
1051
+ knowledgeId: entry.id,
1052
+ knowledgeBaseId: entry.knowledgeBaseId,
1053
+ documentTitle: entry.title,
1054
+ note,
1055
+ source: String(row.source),
1056
+ ...row.source_session_id == null ? {} : { sourceSessionId: String(row.source_session_id) },
1057
+ createdAt: String(row.created_at),
1058
+ }];
1059
+ });
1060
+ }
1061
+ async addKnowledgeNoteReference(knowledgeId, noteId, source, sourceSessionId) {
1062
+ this.assertOpen();
1063
+ const entry = await this.get(knowledgeId);
1064
+ if (entry === undefined)
1065
+ throw notFound('knowledge entry', knowledgeId);
1066
+ if (entry.status !== 'active')
1067
+ throw conflict('archived knowledge documents cannot change note references');
1068
+ if (entry.documentState !== 'open')
1069
+ throw finalizedConflict(entry);
1070
+ const note = this.notes.get(noteId);
1071
+ if (note === undefined)
1072
+ throw notFound('note node', noteId);
1073
+ if (note.kind === 'folder')
1074
+ throw inputError('knowledge documents can reference note documents or files, not folders');
1075
+ if (source !== 'user' && source !== 'agent' && source !== 'legacy')
1076
+ throw inputError('invalid knowledge note reference source');
1077
+ const sessionId = sourceSessionId?.trim() || undefined;
1078
+ if (sessionId !== undefined && sessionId.length > 200)
1079
+ throw inputError('reference source session id is too long');
1080
+ const createdAt = nowIso();
1081
+ this.db.prepare(`
1082
+ INSERT OR IGNORE INTO knowledge_note_references(knowledge_id,note_id,source,source_session_id,created_at)
1083
+ VALUES(?,?,?,?,?)
1084
+ `).run(entry.id, note.id, source, sessionId ?? null, createdAt);
1085
+ const reference = (await this.listKnowledgeNoteReferences(entry.id)).find(item => item.note.id === note.id);
1086
+ if (reference === undefined)
1087
+ throw new Error('knowledge note reference was not persisted');
1088
+ return reference;
1089
+ }
1090
+ async deleteKnowledgeNoteReference(knowledgeId, noteId) {
1091
+ this.assertOpen();
1092
+ const entry = await this.get(knowledgeId);
1093
+ if (entry === undefined)
1094
+ throw notFound('knowledge entry', knowledgeId);
1095
+ if (entry.status !== 'active')
1096
+ throw conflict('archived knowledge documents cannot change note references');
1097
+ if (entry.documentState !== 'open')
1098
+ throw finalizedConflict(entry);
1099
+ const result = this.db.prepare('DELETE FROM knowledge_note_references WHERE knowledge_id=? AND note_id=?').run(knowledgeId, noteId);
1100
+ if (result.changes === 0)
1101
+ throw notFound('knowledge note reference', `${knowledgeId}/${noteId}`);
1102
+ }
1103
+ noteReferencesForNotes(noteIds) {
1104
+ this.assertOpen();
1105
+ const ids = [...new Set(noteIds)];
1106
+ if (ids.length === 0)
1107
+ return [];
1108
+ const placeholders = ids.map(() => '?').join(',');
1109
+ return this.db.prepare(`
1110
+ SELECT r.note_id,e.knowledge_base_id,e.id AS document_id,e.title AS document_title
1111
+ FROM knowledge_note_references r
1112
+ JOIN knowledge_entries e ON e.id=r.knowledge_id
1113
+ WHERE r.note_id IN (${placeholders})
1114
+ ORDER BY e.updated_at DESC,e.id
1115
+ `).all(...ids).map(row => ({
1116
+ noteId: String(row.note_id),
1117
+ knowledgeBaseId: String(row.knowledge_base_id),
1118
+ documentId: String(row.document_id),
1119
+ documentTitle: String(row.document_title),
1120
+ }));
1121
+ }
1122
+ deleteNoteReferences(noteIds) {
1123
+ this.assertOpen();
1124
+ const ids = [...new Set(noteIds)];
1125
+ if (ids.length === 0)
1126
+ return;
1127
+ const placeholders = ids.map(() => '?').join(',');
1128
+ this.db.prepare(`DELETE FROM knowledge_note_references WHERE note_id IN (${placeholders})`).run(...ids);
1129
+ }
874
1130
  async propose(input, sourceKey) {
875
1131
  this.assertOpen();
876
1132
  const proposal = normalizeProposal(input);
@@ -1200,6 +1456,7 @@ export class LocalKnowledgeProvider {
1200
1456
  await this.documentsReady;
1201
1457
  this.closed = true;
1202
1458
  this.db.close();
1459
+ await this.notes.close();
1203
1460
  }
1204
1461
  writeVersion(entry, changeKind) {
1205
1462
  const snapshot = {
@@ -1353,6 +1610,21 @@ function rowToDocument(row) {
1353
1610
  updatedAt: String(row.updated_at),
1354
1611
  };
1355
1612
  }
1613
+ function rowToDocumentSummary(row) {
1614
+ return {
1615
+ id: String(row.id),
1616
+ knowledgeBaseId: String(row.knowledge_base_id),
1617
+ relPath: String(row.rel_path),
1618
+ title: String(row.title),
1619
+ entryCount: Number(row.entry_count),
1620
+ contentHash: String(row.content_hash),
1621
+ documentState: row.document_state == null ? 'open' : String(row.document_state),
1622
+ ...row.finalized_at == null ? {} : { finalizedAt: String(row.finalized_at) },
1623
+ ...row.finalization_note == null ? {} : { finalizationNote: String(row.finalization_note) },
1624
+ createdAt: String(row.created_at),
1625
+ updatedAt: String(row.updated_at),
1626
+ };
1627
+ }
1356
1628
  function rowToVersion(row) {
1357
1629
  const snapshot = JSON.parse(String(row.snapshot_json));
1358
1630
  if (snapshot.knowledgeBaseId === undefined)
@@ -1613,12 +1885,40 @@ function decodeCursor(cursor) {
1613
1885
  throw Object.assign(new Error('invalid pagination cursor'), { code: 'BAD_REQUEST' });
1614
1886
  }
1615
1887
  }
1888
+ function encodeDocumentCursor(knowledgeBaseId, sortRank, relPath, id) {
1889
+ return Buffer.from(JSON.stringify({ knowledgeBaseId, sortRank, relPath, id })).toString('base64url');
1890
+ }
1891
+ function decodeDocumentCursor(cursor) {
1892
+ try {
1893
+ const value = JSON.parse(Buffer.from(cursor, 'base64url').toString('utf8'));
1894
+ if (typeof value.knowledgeBaseId !== 'string'
1895
+ || (value.sortRank !== 0 && value.sortRank !== 1)
1896
+ || typeof value.relPath !== 'string'
1897
+ || typeof value.id !== 'string')
1898
+ throw new Error();
1899
+ return {
1900
+ knowledgeBaseId: value.knowledgeBaseId,
1901
+ sortRank: value.sortRank,
1902
+ relPath: value.relPath,
1903
+ id: value.id,
1904
+ };
1905
+ }
1906
+ catch {
1907
+ throw Object.assign(new Error('invalid document pagination cursor'), { code: 'BAD_REQUEST', status: 400 });
1908
+ }
1909
+ }
1910
+ function escapeSqlLike(value) {
1911
+ return value.replaceAll('\\', '\\\\').replaceAll('%', '\\%').replaceAll('_', '\\_');
1912
+ }
1616
1913
  function notFound(kind, id) {
1617
1914
  return Object.assign(new Error(`${kind} "${id}" was not found`), { code: 'NOT_FOUND' });
1618
1915
  }
1619
1916
  function conflict(message) {
1620
1917
  return Object.assign(new Error(message), { code: 'CONFLICT' });
1621
1918
  }
1919
+ function inputError(message) {
1920
+ return Object.assign(new Error(message), { code: 'BAD_REQUEST', status: 400 });
1921
+ }
1622
1922
  function finalizedConflict(entry) {
1623
1923
  const label = entry.documentState === 'resolved' ? 'resolved' : 'collection complete';
1624
1924
  return conflict(`knowledge document "${entry.title}" (${entry.id}) is finalized as ${label}; reopen it before making changes`);