@noesis-brain/mcp-server 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 (47) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +218 -0
  3. package/dist/api/NoesisClient.d.ts +501 -0
  4. package/dist/api/NoesisClient.d.ts.map +1 -0
  5. package/dist/api/NoesisClient.js +654 -0
  6. package/dist/api/NoesisClient.js.map +1 -0
  7. package/dist/cli/setup.d.ts +8 -0
  8. package/dist/cli/setup.d.ts.map +1 -0
  9. package/dist/cli/setup.js +148 -0
  10. package/dist/cli/setup.js.map +1 -0
  11. package/dist/database/PostgresAdapter.d.ts +385 -0
  12. package/dist/database/PostgresAdapter.d.ts.map +1 -0
  13. package/dist/database/PostgresAdapter.js +1043 -0
  14. package/dist/database/PostgresAdapter.js.map +1 -0
  15. package/dist/index.d.ts +31 -0
  16. package/dist/index.d.ts.map +1 -0
  17. package/dist/index.js +126 -0
  18. package/dist/index.js.map +1 -0
  19. package/dist/services/embedding.d.ts +38 -0
  20. package/dist/services/embedding.d.ts.map +1 -0
  21. package/dist/services/embedding.js +126 -0
  22. package/dist/services/embedding.js.map +1 -0
  23. package/dist/tools/SyncStateManager.d.ts +65 -0
  24. package/dist/tools/SyncStateManager.d.ts.map +1 -0
  25. package/dist/tools/SyncStateManager.js +217 -0
  26. package/dist/tools/SyncStateManager.js.map +1 -0
  27. package/dist/tools/index.d.ts +14 -0
  28. package/dist/tools/index.d.ts.map +1 -0
  29. package/dist/tools/index.js +3345 -0
  30. package/dist/tools/index.js.map +1 -0
  31. package/dist/tools/navis.d.ts +11 -0
  32. package/dist/tools/navis.d.ts.map +1 -0
  33. package/dist/tools/navis.js +231 -0
  34. package/dist/tools/navis.js.map +1 -0
  35. package/dist/types/index.d.ts +104 -0
  36. package/dist/types/index.d.ts.map +1 -0
  37. package/dist/types/index.js +5 -0
  38. package/dist/types/index.js.map +1 -0
  39. package/dist/utils/suggestPath.d.ts +15 -0
  40. package/dist/utils/suggestPath.d.ts.map +1 -0
  41. package/dist/utils/suggestPath.js +52 -0
  42. package/dist/utils/suggestPath.js.map +1 -0
  43. package/package.json +71 -0
  44. package/scripts/noesis-sync.mjs +469 -0
  45. package/skill-templates/noesis-refine-note.md +92 -0
  46. package/skill-templates/noesis-sync.md +110 -0
  47. package/templates/claude-md-block.md +22 -0
@@ -0,0 +1,654 @@
1
+ /**
2
+ * NoesisClient - HTTP client for Noesis API
3
+ *
4
+ * Replaces direct PostgreSQL access in MCP server.
5
+ * All operations go through authenticated API calls.
6
+ */
7
+ import crypto from 'crypto';
8
+ import os from 'os';
9
+ import path from 'path';
10
+ const _envFakePlatform = process.env.NOESIS_FAKE_PLATFORM;
11
+ const _realPlatform = process.platform === 'darwin' ? 'darwin'
12
+ : process.platform === 'win32' ? 'win32'
13
+ : 'linux';
14
+ export const CLIENT_OS = (_envFakePlatform === 'win32' || _envFakePlatform === 'darwin' || _envFakePlatform === 'linux')
15
+ ? _envFakePlatform
16
+ : _realPlatform;
17
+ /**
18
+ * Expand a leading `~` or `%USERPROFILE%` (home-directory shortcut) against
19
+ * this machine's home directory. Cloud-flow root paths are stored as
20
+ * `~/Noesis/...` in the cloud DB because the cloud cannot know each machine's
21
+ * $HOME -- the MCP server expands here at sync time. No-op for already-absolute
22
+ * paths.
23
+ *
24
+ * `%USERPROFILE%` is accepted because the frontend renders Windows-row root
25
+ * paths as `%USERPROFILE%\Noesis` for display (Windows shells don't expand `~`),
26
+ * and that displayed form ends up on the clipboard via the note's "Copy file
27
+ * path" button. Recognising it here keeps Copy → Paste → `sync_notes` working
28
+ * end-to-end on Windows.
29
+ *
30
+ * Phase 34 introduced this expansion. Older MCP server versions stored
31
+ * `cloud://` sentinels and never expanded; that path is gone.
32
+ */
33
+ export function expandHome(p) {
34
+ if (!p)
35
+ return '';
36
+ if (p === '~')
37
+ return os.homedir();
38
+ if (p.startsWith('~/') || p.startsWith('~\\')) {
39
+ return path.join(os.homedir(), p.slice(2));
40
+ }
41
+ // Windows: `%USERPROFILE%` and `%USERPROFILE%\...` (case-insensitive variable name).
42
+ const envHomeMatch = p.match(/^%USERPROFILE%([\\/].*)?$/i);
43
+ if (envHomeMatch) {
44
+ const tail = envHomeMatch[1];
45
+ if (!tail)
46
+ return os.homedir();
47
+ return path.join(os.homedir(), tail.slice(1));
48
+ }
49
+ return p;
50
+ }
51
+ /**
52
+ * Return the OS-active path from a root's local_paths map, or '' if
53
+ * missing. Tilde-prefixed values are expanded against `os.homedir()` for
54
+ * direct fs use. Callers that need an error on miss should use
55
+ * requireActiveRootPath in tools/index.ts instead.
56
+ */
57
+ export function getActivePathFromMap(localPaths) {
58
+ if (!localPaths)
59
+ return '';
60
+ return expandHome(localPaths[CLIENT_OS] || '');
61
+ }
62
+ /**
63
+ * HTTP client for Noesis API
64
+ */
65
+ export class NoesisClient {
66
+ baseUrl;
67
+ apiToken;
68
+ constructor(baseUrl, apiToken) {
69
+ // Remove trailing slash
70
+ this.baseUrl = baseUrl.replace(/\/$/, '');
71
+ this.apiToken = apiToken;
72
+ }
73
+ /**
74
+ * Make an authenticated HTTP request
75
+ */
76
+ async request(method, path, body) {
77
+ const url = `${this.baseUrl}${path}`;
78
+ const response = await fetch(url, {
79
+ method,
80
+ headers: {
81
+ 'Authorization': `Bearer ${this.apiToken}`,
82
+ 'Content-Type': 'application/json',
83
+ 'X-Client-OS': CLIENT_OS,
84
+ },
85
+ body: body ? JSON.stringify(body) : undefined,
86
+ });
87
+ if (!response.ok) {
88
+ const errorBody = await response.json().catch(() => ({ error: 'Request failed' }));
89
+ throw new Error(errorBody.error || `HTTP ${response.status}: ${response.statusText}`);
90
+ }
91
+ return response.json();
92
+ }
93
+ // ============================================
94
+ // ROOTS
95
+ // ============================================
96
+ async getRoots() {
97
+ const result = await this.request('GET', '/api/mcp/roots');
98
+ // Backfill `path` convenience field from local_paths[CLIENT_OS] for any
99
+ // call site still reading r.path directly.
100
+ return result.roots.map(r => ({ ...r, path: getActivePathFromMap(r.local_paths) }));
101
+ }
102
+ async getRootsForSync() {
103
+ const result = await this.request('GET', '/api/mcp/roots?forSync=true');
104
+ return result.roots.map(r => ({
105
+ id: r.id,
106
+ name: r.name,
107
+ local_paths: r.local_paths || {},
108
+ path: getActivePathFromMap(r.local_paths),
109
+ lastScannedAt: r.last_scanned_at || null
110
+ }));
111
+ }
112
+ /**
113
+ * Create a new root.
114
+ *
115
+ * Accepts either:
116
+ * - `{ name, local_paths: { win32?, darwin?, linux? } }` (preferred; phase32+),
117
+ * - `{ name, path }` (legacy single-path — auto-routed into local_paths[CLIENT_OS]).
118
+ */
119
+ async createRoot(options) {
120
+ let local_paths = options.local_paths;
121
+ if (!local_paths && options.path) {
122
+ local_paths = { [CLIENT_OS]: options.path };
123
+ }
124
+ const body = { name: options.name, local_paths };
125
+ if (options.type)
126
+ body.type = options.type;
127
+ const result = await this.request('POST', '/api/mcp/roots', body);
128
+ return { ...result.root, path: getActivePathFromMap(result.root.local_paths) };
129
+ }
130
+ async getRootByPath(path) {
131
+ try {
132
+ const result = await this.request('GET', `/api/mcp/roots/by-path?path=${encodeURIComponent(path)}`);
133
+ return { ...result.root, path: getActivePathFromMap(result.root.local_paths) };
134
+ }
135
+ catch {
136
+ return undefined;
137
+ }
138
+ }
139
+ /**
140
+ * Look up a note by (root_id, relative_path). Used by the MCP server's
141
+ * cross-platform `get_note` resolver after it has prefix-matched the input
142
+ * path against a root.
143
+ */
144
+ async getNoteByRelativePath(rootId, relativePath) {
145
+ try {
146
+ const params = new URLSearchParams({ root_id: String(rootId), relative_path: relativePath });
147
+ const result = await this.request('GET', `/api/mcp/notes/by-relative?${params}`);
148
+ return result.note;
149
+ }
150
+ catch {
151
+ return undefined;
152
+ }
153
+ }
154
+ async getNoteHashesByRoot(rootId) {
155
+ const result = await this.request('GET', `/api/mcp/roots/${rootId}/hashes`);
156
+ return new Map(Object.entries(result.hashes));
157
+ }
158
+ /**
159
+ * Get notes with hash, modified_at, content, and metadata for bidirectional sync comparison
160
+ */
161
+ async getNotesForSync(rootId) {
162
+ const result = await this.request('GET', `/api/mcp/roots/${rootId}/notes-for-sync`);
163
+ return result.notes;
164
+ }
165
+ /**
166
+ * Mark a cloud note as having an unresolved sync conflict.
167
+ * The structured BASE/LOCAL/CLOUD payload is stored in notes.conflict_marker (JSONB).
168
+ * Web UI surfaces a yellow banner when this is set.
169
+ */
170
+ async markConflict(noteId, payload) {
171
+ await this.request('POST', `/api/mcp/notes/${noteId}/mark-conflict`, payload);
172
+ }
173
+ /** Clear the conflict marker after a successful merge + push. */
174
+ async clearConflict(noteId) {
175
+ await this.request('POST', `/api/mcp/notes/${noteId}/clear-conflict`, {});
176
+ }
177
+ /**
178
+ * List notes with edited_online_at set — pending local sync from web UI edits.
179
+ * Optionally filter to a specific root by ID.
180
+ */
181
+ async getEditedOnlineNotes(rootId) {
182
+ const params = rootId !== undefined ? `?root_id=${rootId}` : '';
183
+ const result = await this.request('GET', `/api/mcp/edited-online-notes${params}`);
184
+ // Phase32: backend returns root_local_paths (JSONB map). Compute the
185
+ // OS-active root_path convenience field for back-compat with the existing
186
+ // tool surface.
187
+ return result.notes.map((n) => ({
188
+ id: n.id,
189
+ title: n.title,
190
+ relative_path: n.relative_path,
191
+ hash: n.hash,
192
+ edited_online_at: n.edited_online_at,
193
+ root_id: n.root_id,
194
+ root_local_paths: n.root_local_paths || {},
195
+ root_path: getActivePathFromMap(n.root_local_paths),
196
+ }));
197
+ }
198
+ // ============================================
199
+ // CATALOGS
200
+ // ============================================
201
+ async listCatalogs() {
202
+ const result = await this.request('GET', '/api/mcp/catalogs');
203
+ return result.catalogs;
204
+ }
205
+ async setNoteCatalogs(noteId, catalogs) {
206
+ await this.request('PUT', `/api/mcp/notes/${noteId}/catalogs`, { catalogs });
207
+ }
208
+ async setNoteRelatedCodes(noteId, codes) {
209
+ // Numeric ids hit the primary `codebase_ids` channel; raw path strings fall through
210
+ // the backend's find-or-create shim. Mixed input rides the same `related_codes` key
211
+ // and the server resolves per-element.
212
+ const allNumeric = codes.every(c => typeof c === 'number');
213
+ const body = allNumeric ? { codebase_ids: codes } : { related_codes: codes };
214
+ await this.request('PUT', `/api/mcp/notes/${noteId}/related-codes`, body);
215
+ }
216
+ // ============================================
217
+ // CODEBASES (managed registry)
218
+ // ============================================
219
+ async listCodebases(includeArchived = false) {
220
+ const params = new URLSearchParams();
221
+ if (includeArchived)
222
+ params.set('archived', 'true');
223
+ const qs = params.toString();
224
+ const result = await this.request('GET', `/api/mcp/codebases${qs ? `?${qs}` : ''}`);
225
+ return result.codebases;
226
+ }
227
+ async getCodebase(id) {
228
+ return this.request('GET', `/api/mcp/codebases/${id}`);
229
+ }
230
+ async getCodebaseUsage(id) {
231
+ return this.request('GET', `/api/mcp/codebases/${id}/usage`);
232
+ }
233
+ async createCodebase(input) {
234
+ return this.request('POST', `/api/mcp/codebases`, input);
235
+ }
236
+ async findOrCreateCodebase(input) {
237
+ return this.request('POST', `/api/mcp/codebases/find-or-create`, input);
238
+ }
239
+ async updateCodebase(id, patch) {
240
+ return this.request('PATCH', `/api/mcp/codebases/${id}`, patch);
241
+ }
242
+ async deleteCodebase(id) {
243
+ return this.request('DELETE', `/api/mcp/codebases/${id}`);
244
+ }
245
+ async updateRootScanTime(rootId) {
246
+ await this.request('PUT', `/api/mcp/roots/${rootId}/scan-time`, {});
247
+ }
248
+ // ============================================
249
+ // NOTES - SEARCH & READ
250
+ // ============================================
251
+ async searchNotes(query, options = {}) {
252
+ const { limit = 10, root, catalog } = options;
253
+ const params = new URLSearchParams({ q: query, limit: String(limit) });
254
+ if (root)
255
+ params.append('root', root);
256
+ if (catalog)
257
+ params.append('catalog', catalog);
258
+ const result = await this.request('GET', `/api/mcp/notes/search?${params}`);
259
+ // Transform results to include relevance percentage and excerpt
260
+ return result.notes.map(note => {
261
+ // Convert relevance_score to percentage (BM25 scores vary, normalize to 0-100)
262
+ const relevanceScore = note.relevance_score || 0;
263
+ const relevance = Math.min(Math.round(relevanceScore * 100), 100);
264
+ // Generate excerpt from content
265
+ let excerpt = note.description || '';
266
+ if (!excerpt && note.content) {
267
+ excerpt = note.content.substring(0, 200).replace(/\n/g, ' ').trim();
268
+ if (note.content.length > 200)
269
+ excerpt += '...';
270
+ }
271
+ return {
272
+ ...note,
273
+ relevance,
274
+ excerpt
275
+ };
276
+ });
277
+ }
278
+ async getNote(id) {
279
+ try {
280
+ const result = await this.request('GET', `/api/mcp/notes/${id}`);
281
+ return result.note;
282
+ }
283
+ catch {
284
+ return undefined;
285
+ }
286
+ }
287
+ async getBookmarkContext(noteId, bookmarkId, contextParagraphs = 2) {
288
+ try {
289
+ const params = new URLSearchParams({ paragraphs: String(contextParagraphs) });
290
+ return await this.request('GET', `/api/mcp/notes/${noteId}/bookmarks/${bookmarkId}/context?${params}`);
291
+ }
292
+ catch {
293
+ return undefined;
294
+ }
295
+ }
296
+ async getChatSession(id, opts = {}) {
297
+ try {
298
+ const params = new URLSearchParams();
299
+ if (opts.limit != null)
300
+ params.set('limit', String(opts.limit));
301
+ const qs = params.toString();
302
+ return await this.request('GET', `/api/mcp/chat/sessions/${id}${qs ? `?${qs}` : ''}`);
303
+ }
304
+ catch {
305
+ return undefined;
306
+ }
307
+ }
308
+ async getNoteByPath(filePath) {
309
+ try {
310
+ const result = await this.request('GET', `/api/mcp/notes/by-path?path=${encodeURIComponent(filePath)}`);
311
+ return result.note;
312
+ }
313
+ catch {
314
+ return undefined;
315
+ }
316
+ }
317
+ async searchByRelatedCode(path, limit = 20) {
318
+ const params = new URLSearchParams({
319
+ path,
320
+ limit: String(limit)
321
+ });
322
+ const result = await this.request('GET', `/api/mcp/notes/by-related-code?${params}`);
323
+ return result.notes;
324
+ }
325
+ async getRelationGraph(noteId, depth = 2) {
326
+ const params = new URLSearchParams({
327
+ id: String(noteId),
328
+ depth: String(depth)
329
+ });
330
+ const result = await this.request('GET', `/api/mcp/notes/relation-graph?${params}`);
331
+ return result.notes;
332
+ }
333
+ async listNotes(options = {}) {
334
+ const params = new URLSearchParams();
335
+ if (options.limit)
336
+ params.append('limit', String(options.limit));
337
+ if (options.offset)
338
+ params.append('offset', String(options.offset));
339
+ if (options.root)
340
+ params.append('root', options.root);
341
+ if (options.catalog)
342
+ params.append('catalog', options.catalog);
343
+ const result = await this.request('GET', `/api/mcp/notes?${params}`);
344
+ return result.notes;
345
+ }
346
+ async getRecentNotes(days = 7, limit = 20) {
347
+ const params = new URLSearchParams({
348
+ recent: String(days),
349
+ limit: String(limit)
350
+ });
351
+ const result = await this.request('GET', `/api/mcp/notes?${params}`);
352
+ return result.notes;
353
+ }
354
+ /**
355
+ * Get notes for pulling to local files
356
+ */
357
+ async getNotesForPull(options = {}) {
358
+ const params = new URLSearchParams();
359
+ if (options.root)
360
+ params.append('root', options.root);
361
+ params.append('for_pull', 'true');
362
+ const result = await this.request('GET', `/api/mcp/notes?${params}`);
363
+ return result.notes;
364
+ }
365
+ /**
366
+ * Get note count by root for sync status
367
+ */
368
+ async getNoteCountByRoot() {
369
+ const result = await this.request('GET', '/api/mcp/notes/count-by-root');
370
+ return new Map(Object.entries(result.counts).map(([k, v]) => [parseInt(k), v]));
371
+ }
372
+ // ============================================
373
+ // NOTES - WRITE (SYNC)
374
+ // ============================================
375
+ async upsertNote(file, metadata = {}, options = {}) {
376
+ const { force = false, regenerateMetadata = false, preserveMetadata = false, lastSyncedHash } = options;
377
+ const body = { file, metadata, force, regenerateMetadata, preserveMetadata };
378
+ if (lastSyncedHash !== undefined)
379
+ body.lastSyncedHash = lastSyncedHash;
380
+ const result = await this.request('POST', '/api/mcp/notes/upsert', body);
381
+ return result;
382
+ }
383
+ async logSyncOperation(options) {
384
+ await this.request('POST', '/api/mcp/sync/log', options);
385
+ }
386
+ // ============================================
387
+ // METADATA ENHANCEMENT
388
+ // ============================================
389
+ async getNoteForEnhancement(id) {
390
+ try {
391
+ const result = await this.request('GET', `/api/mcp/notes/${id}/for-enhancement`);
392
+ return result.note;
393
+ }
394
+ catch {
395
+ return undefined;
396
+ }
397
+ }
398
+ async updateNoteMetadata(id, metadata) {
399
+ try {
400
+ await this.request('PUT', `/api/mcp/notes/${id}/metadata`, metadata);
401
+ return true;
402
+ }
403
+ catch {
404
+ return false;
405
+ }
406
+ }
407
+ /**
408
+ * Update file metadata (file_size, hash) after pulling from cloud.
409
+ * Phase32: prefer (root_id, relative_path); falls back to legacy file_path
410
+ * if the caller hasn't been updated.
411
+ */
412
+ async updateFileMetadata(filePathOrIds, fileSize, hash) {
413
+ try {
414
+ const body = { file_size: fileSize, hash };
415
+ if (typeof filePathOrIds === 'string') {
416
+ body.file_path = filePathOrIds;
417
+ }
418
+ else {
419
+ body.root_id = filePathOrIds.rootId;
420
+ body.relative_path = filePathOrIds.relativePath;
421
+ }
422
+ await this.request('PUT', '/api/mcp/notes/file-metadata', body);
423
+ return true;
424
+ }
425
+ catch {
426
+ return false;
427
+ }
428
+ }
429
+ async getNotesNeedingEnhancement(options = {}) {
430
+ const params = new URLSearchParams();
431
+ if (options.limit)
432
+ params.append('limit', String(options.limit));
433
+ if (options.importantOnly)
434
+ params.append('important_only', 'true');
435
+ if (options.root)
436
+ params.append('root', options.root);
437
+ if (options.catalog)
438
+ params.append('catalog', options.catalog);
439
+ const result = await this.request('GET', `/api/mcp/notes/needing-enhancement?${params}`);
440
+ return result.notes;
441
+ }
442
+ // ============================================
443
+ // SCORES & RELATIONS
444
+ // ============================================
445
+ async updateImportanceScore(id, score) {
446
+ try {
447
+ await this.request('PUT', `/api/mcp/notes/${id}/importance`, { score });
448
+ return true;
449
+ }
450
+ catch {
451
+ return false;
452
+ }
453
+ }
454
+ async updateQualityScore(id, score) {
455
+ try {
456
+ await this.request('PUT', `/api/mcp/notes/${id}/quality`, { score });
457
+ return true;
458
+ }
459
+ catch {
460
+ return false;
461
+ }
462
+ }
463
+ async updateRelations(id, relations) {
464
+ const result = await this.request('PUT', `/api/mcp/notes/${id}/relations`, { relations });
465
+ return { updated: result.updated || relations.length, inversesCreated: 0 };
466
+ }
467
+ async moveNote(id, newPath, options = {}) {
468
+ try {
469
+ const result = await this.request('PUT', `/api/mcp/notes/${id}/move`, {
470
+ new_path: newPath,
471
+ new_relative_path: options.newRelativePath,
472
+ new_root_name: options.newRootName,
473
+ });
474
+ return result.note;
475
+ }
476
+ catch {
477
+ return null;
478
+ }
479
+ }
480
+ async updateNoteSignals(id, signals) {
481
+ try {
482
+ await this.request('PUT', `/api/mcp/notes/${id}/signals`, signals);
483
+ return true;
484
+ }
485
+ catch {
486
+ return false;
487
+ }
488
+ }
489
+ async trashNote(id) {
490
+ try {
491
+ await this.request('PUT', `/api/mcp/notes/${id}/trash`, {});
492
+ return true;
493
+ }
494
+ catch {
495
+ return false;
496
+ }
497
+ }
498
+ async getNoteForScoring(id) {
499
+ // Same as getNoteForEnhancement for now
500
+ return this.getNoteForEnhancement(id);
501
+ }
502
+ async getNotesForRelationAnalysis(excludeId, options = {}) {
503
+ const params = new URLSearchParams({ limit: String(options.limit || 50) });
504
+ if (options.root)
505
+ params.append('root', options.root);
506
+ const result = await this.request('GET', `/api/mcp/notes?${params}`);
507
+ return result.notes.filter(n => n.id !== excludeId);
508
+ }
509
+ // ============================================
510
+ // EMBEDDINGS
511
+ // ============================================
512
+ async getNotesWithoutEmbeddings(options = {}) {
513
+ const params = new URLSearchParams();
514
+ if (options.limit)
515
+ params.append('limit', String(options.limit));
516
+ if (options.root)
517
+ params.append('root', options.root);
518
+ const result = await this.request('GET', `/api/mcp/notes/without-embeddings?${params}`);
519
+ return result.notes;
520
+ }
521
+ async updateNoteEmbedding(id, embedding) {
522
+ try {
523
+ await this.request('PUT', `/api/mcp/notes/${id}/embedding`, { embedding });
524
+ return true;
525
+ }
526
+ catch {
527
+ return false;
528
+ }
529
+ }
530
+ async searchByEmbedding(embedding, options = {}) {
531
+ const result = await this.request('POST', '/api/mcp/notes/search-semantic', { embedding, ...options });
532
+ return result.notes;
533
+ }
534
+ async findSimilarNotes(noteId, options = {}) {
535
+ const params = new URLSearchParams();
536
+ if (options.limit)
537
+ params.append('limit', String(options.limit));
538
+ const result = await this.request('GET', `/api/mcp/notes/${noteId}/similar?${params}`);
539
+ return result.notes;
540
+ }
541
+ async getNoteEmbedding(id) {
542
+ // Get note and extract embedding if present
543
+ const note = await this.getNote(id);
544
+ if (!note)
545
+ return undefined;
546
+ // Embedding is not returned in standard note response for size reasons
547
+ return undefined;
548
+ }
549
+ async getEmbeddingStats() {
550
+ const result = await this.request('GET', '/api/mcp/stats/embeddings');
551
+ return result;
552
+ }
553
+ // ============================================
554
+ // STATUS & SETTINGS
555
+ // ============================================
556
+ async getLastSyncTime() {
557
+ const result = await this.request('GET', '/api/mcp/status');
558
+ return result.lastSyncTime;
559
+ }
560
+ async setLastSyncTime(timestamp) {
561
+ // This is typically handled by logSyncOperation
562
+ // Not directly exposed via API for now
563
+ }
564
+ async getKnowledgeBaseStats(options = {}) {
565
+ const params = new URLSearchParams();
566
+ if (options.limit)
567
+ params.append('limit', String(options.limit));
568
+ if (options.root)
569
+ params.append('root', options.root);
570
+ return this.request('GET', `/api/mcp/analyze?${params}`);
571
+ }
572
+ async getSyncLogs(rootId, limit = 10) {
573
+ const params = new URLSearchParams({ limit: String(limit) });
574
+ if (rootId)
575
+ params.append('root_id', String(rootId));
576
+ // Note: This endpoint is on the main API, not /api/mcp
577
+ const result = await this.request('GET', `/api/sync-logs?${params}`);
578
+ return result.logs;
579
+ }
580
+ // ============================================
581
+ // UTILITIES
582
+ // ============================================
583
+ /**
584
+ * Normalize line endings to LF for cross-platform consistency
585
+ * Matches backend behavior in src/backend/routes/mcp.ts
586
+ */
587
+ static normalizeLineEndings(content) {
588
+ return content.replace(/\r\n/g, '\n').replace(/\r/g, '\n');
589
+ }
590
+ // ─── Daily News Methods ───
591
+ async getNewsPreferences() {
592
+ const [settings, sources, seeds] = await Promise.all([
593
+ this.request('GET', '/api/news/settings'),
594
+ this.request('GET', '/api/news/sources'),
595
+ this.request('GET', '/api/news/seeds'),
596
+ ]);
597
+ return {
598
+ settings: settings.settings,
599
+ sources: sources.sources,
600
+ seeds: seeds.seeds,
601
+ };
602
+ }
603
+ async updateNewsPreferences(data) {
604
+ if (data.settings) {
605
+ await this.request('PATCH', '/api/news/settings', data.settings);
606
+ }
607
+ if (data.preferences) {
608
+ await this.request('POST', '/api/news/preferences/import', { preferences: data.preferences });
609
+ }
610
+ }
611
+ async addNewsSource(data) {
612
+ return this.request('POST', '/api/news/sources', data);
613
+ }
614
+ /**
615
+ * Compute SHA-256 hash of content (for sync comparison)
616
+ * Static method - can be used without API call
617
+ * Note: Normalizes line endings to match backend hash computation
618
+ */
619
+ static computeHash(content) {
620
+ return crypto.createHash('sha256').update(NoesisClient.normalizeLineEndings(content), 'utf8').digest('hex');
621
+ }
622
+ // ============================================
623
+ // NAVIS
624
+ // ============================================
625
+ async listNavis() {
626
+ return this.request('GET', '/api/navis');
627
+ }
628
+ async getNavi(id) {
629
+ const result = await this.request('GET', `/api/navis/${id}`);
630
+ return result.navi;
631
+ }
632
+ async createNavi(body) {
633
+ const result = await this.request('POST', '/api/navis', body);
634
+ return result.navi;
635
+ }
636
+ async updateNavi(id, body) {
637
+ const result = await this.request('PUT', `/api/navis/${id}`, body);
638
+ return result.navi;
639
+ }
640
+ async deleteNavi(id) {
641
+ await this.request('DELETE', `/api/navis/${id}`);
642
+ }
643
+ async duplicateNavi(id, name) {
644
+ const result = await this.request('POST', `/api/navis/${id}/duplicate`, name ? { name } : {});
645
+ return result.navi;
646
+ }
647
+ /**
648
+ * Close the client (no-op for HTTP client)
649
+ */
650
+ async close() {
651
+ // No-op - HTTP client doesn't need cleanup
652
+ }
653
+ }
654
+ //# sourceMappingURL=NoesisClient.js.map