@noesis-brain/mcp-server 2.0.0 → 2.0.1
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.
- package/README.md +2 -2
- package/dist/database/PostgresAdapter.js +216 -216
- package/dist/services/embedding.js +19 -19
- package/dist/tools/index.js +5 -5
- package/package.json +2 -2
- package/scripts/noesis-sync.mjs +469 -469
- package/skill-templates/noesis-refine-note.md +92 -92
- package/skill-templates/noesis-sync.md +110 -110
- package/templates/claude-md-block.md +22 -22
package/README.md
CHANGED
|
@@ -1,10 +1,10 @@
|
|
|
1
|
-
# Noesis MCP Server
|
|
1
|
+
# Noesis — Markdown Notes Manager for Claude Code (MCP Server)
|
|
2
2
|
|
|
3
3
|
[](https://www.npmjs.com/package/@noesis-brain/mcp-server)
|
|
4
4
|
[](https://opensource.org/licenses/MIT)
|
|
5
5
|
[](https://modelcontextprotocol.io/)
|
|
6
6
|
|
|
7
|
-
Official [Model Context Protocol](https://modelcontextprotocol.io/) server for [Noesis](https://noesisbrain.com)
|
|
7
|
+
Official [Model Context Protocol](https://modelcontextprotocol.io/) server for [Noesis](https://noesisbrain.com), the markdown notes manager for Claude Code. Search, sync, and manage your `.md` knowledge base via 19 AI-powered tools — turn your notes into a first-class context source for every prompt.
|
|
8
8
|
|
|
9
9
|
## What This Is
|
|
10
10
|
|
|
@@ -44,21 +44,21 @@ export class PostgresAdapter {
|
|
|
44
44
|
}
|
|
45
45
|
// Use PostgreSQL full-text search with ts_rank
|
|
46
46
|
const userFilter = this.buildUserFilter(1);
|
|
47
|
-
let sql = `
|
|
48
|
-
SELECT
|
|
49
|
-
n.id,
|
|
50
|
-
n.title,
|
|
51
|
-
n.content,
|
|
52
|
-
n.file_path,
|
|
53
|
-
n.description,
|
|
54
|
-
n.points,
|
|
55
|
-
n.is_favorite,
|
|
56
|
-
n.modified_at,
|
|
57
|
-
ts_rank(n.fts_vector, plainto_tsquery('english', $${userFilter.nextParamIndex})) as relevance_score
|
|
58
|
-
FROM notes n
|
|
59
|
-
WHERE n.fts_vector @@ plainto_tsquery('english', $${userFilter.nextParamIndex})
|
|
60
|
-
AND COALESCE(n.is_trashed, FALSE) = FALSE
|
|
61
|
-
AND ${userFilter.condition}
|
|
47
|
+
let sql = `
|
|
48
|
+
SELECT
|
|
49
|
+
n.id,
|
|
50
|
+
n.title,
|
|
51
|
+
n.content,
|
|
52
|
+
n.file_path,
|
|
53
|
+
n.description,
|
|
54
|
+
n.points,
|
|
55
|
+
n.is_favorite,
|
|
56
|
+
n.modified_at,
|
|
57
|
+
ts_rank(n.fts_vector, plainto_tsquery('english', $${userFilter.nextParamIndex})) as relevance_score
|
|
58
|
+
FROM notes n
|
|
59
|
+
WHERE n.fts_vector @@ plainto_tsquery('english', $${userFilter.nextParamIndex})
|
|
60
|
+
AND COALESCE(n.is_trashed, FALSE) = FALSE
|
|
61
|
+
AND ${userFilter.condition}
|
|
62
62
|
`;
|
|
63
63
|
const params = [...userFilter.params, searchTerms];
|
|
64
64
|
let paramIndex = userFilter.nextParamIndex + 1;
|
|
@@ -92,20 +92,20 @@ export class PostgresAdapter {
|
|
|
92
92
|
const { limit = 10, root } = options;
|
|
93
93
|
const likePattern = `%${query}%`;
|
|
94
94
|
const userFilter = this.buildUserFilter(1);
|
|
95
|
-
let sql = `
|
|
96
|
-
SELECT
|
|
97
|
-
id,
|
|
98
|
-
title,
|
|
99
|
-
content,
|
|
100
|
-
file_path,
|
|
101
|
-
description,
|
|
102
|
-
points,
|
|
103
|
-
is_favorite,
|
|
104
|
-
modified_at
|
|
105
|
-
FROM notes
|
|
106
|
-
WHERE (title ILIKE $${userFilter.nextParamIndex} OR description ILIKE $${userFilter.nextParamIndex} OR content ILIKE $${userFilter.nextParamIndex})
|
|
107
|
-
AND COALESCE(is_trashed, FALSE) = FALSE
|
|
108
|
-
AND ${userFilter.condition}
|
|
95
|
+
let sql = `
|
|
96
|
+
SELECT
|
|
97
|
+
id,
|
|
98
|
+
title,
|
|
99
|
+
content,
|
|
100
|
+
file_path,
|
|
101
|
+
description,
|
|
102
|
+
points,
|
|
103
|
+
is_favorite,
|
|
104
|
+
modified_at
|
|
105
|
+
FROM notes
|
|
106
|
+
WHERE (title ILIKE $${userFilter.nextParamIndex} OR description ILIKE $${userFilter.nextParamIndex} OR content ILIKE $${userFilter.nextParamIndex})
|
|
107
|
+
AND COALESCE(is_trashed, FALSE) = FALSE
|
|
108
|
+
AND ${userFilter.condition}
|
|
109
109
|
`;
|
|
110
110
|
const params = [...userFilter.params, likePattern];
|
|
111
111
|
let paramIndex = userFilter.nextParamIndex + 1;
|
|
@@ -149,10 +149,10 @@ export class PostgresAdapter {
|
|
|
149
149
|
async listNotes(options = {}) {
|
|
150
150
|
const { limit = 50, offset = 0, root } = options;
|
|
151
151
|
const userFilter = this.buildUserFilter(1);
|
|
152
|
-
let sql = `
|
|
153
|
-
SELECT * FROM notes
|
|
154
|
-
WHERE COALESCE(is_trashed, FALSE) = FALSE
|
|
155
|
-
AND ${userFilter.condition}
|
|
152
|
+
let sql = `
|
|
153
|
+
SELECT * FROM notes
|
|
154
|
+
WHERE COALESCE(is_trashed, FALSE) = FALSE
|
|
155
|
+
AND ${userFilter.condition}
|
|
156
156
|
`;
|
|
157
157
|
const params = [...userFilter.params];
|
|
158
158
|
let paramIndex = userFilter.nextParamIndex;
|
|
@@ -173,13 +173,13 @@ export class PostgresAdapter {
|
|
|
173
173
|
const cutoffDate = new Date();
|
|
174
174
|
cutoffDate.setDate(cutoffDate.getDate() - days);
|
|
175
175
|
const userFilter = this.buildUserFilter(3);
|
|
176
|
-
const result = await this.pool.query(`
|
|
177
|
-
SELECT * FROM notes
|
|
178
|
-
WHERE modified_at >= $1
|
|
179
|
-
AND COALESCE(is_trashed, FALSE) = FALSE
|
|
180
|
-
AND ${userFilter.condition}
|
|
181
|
-
ORDER BY modified_at DESC
|
|
182
|
-
LIMIT $2
|
|
176
|
+
const result = await this.pool.query(`
|
|
177
|
+
SELECT * FROM notes
|
|
178
|
+
WHERE modified_at >= $1
|
|
179
|
+
AND COALESCE(is_trashed, FALSE) = FALSE
|
|
180
|
+
AND ${userFilter.condition}
|
|
181
|
+
ORDER BY modified_at DESC
|
|
182
|
+
LIMIT $2
|
|
183
183
|
`, [cutoffDate.toISOString(), limit, ...userFilter.params]);
|
|
184
184
|
return result.rows;
|
|
185
185
|
}
|
|
@@ -188,11 +188,11 @@ export class PostgresAdapter {
|
|
|
188
188
|
*/
|
|
189
189
|
async getRoots() {
|
|
190
190
|
const userFilter = this.buildUserFilter(1);
|
|
191
|
-
const result = await this.pool.query(`
|
|
192
|
-
SELECT id, path, name, COALESCE(is_visible, TRUE) as is_visible
|
|
193
|
-
FROM roots
|
|
194
|
-
WHERE COALESCE(is_visible, TRUE) = TRUE
|
|
195
|
-
AND ${userFilter.condition}
|
|
191
|
+
const result = await this.pool.query(`
|
|
192
|
+
SELECT id, path, name, COALESCE(is_visible, TRUE) as is_visible
|
|
193
|
+
FROM roots
|
|
194
|
+
WHERE COALESCE(is_visible, TRUE) = TRUE
|
|
195
|
+
AND ${userFilter.condition}
|
|
196
196
|
`, userFilter.params);
|
|
197
197
|
return result.rows;
|
|
198
198
|
}
|
|
@@ -283,19 +283,19 @@ export class PostgresAdapter {
|
|
|
283
283
|
*/
|
|
284
284
|
async getNotesForPull(options = {}) {
|
|
285
285
|
const { root, rootId } = options;
|
|
286
|
-
let sql = `
|
|
287
|
-
SELECT
|
|
288
|
-
n.id,
|
|
289
|
-
n.relative_path,
|
|
290
|
-
n.content,
|
|
291
|
-
n.title,
|
|
292
|
-
r.name as root_name,
|
|
293
|
-
r.path as root_path,
|
|
294
|
-
n.modified_at
|
|
295
|
-
FROM notes n
|
|
296
|
-
JOIN roots r ON n.root_id = r.id
|
|
297
|
-
WHERE COALESCE(n.is_trashed, FALSE) = FALSE
|
|
298
|
-
AND n.relative_path IS NOT NULL
|
|
286
|
+
let sql = `
|
|
287
|
+
SELECT
|
|
288
|
+
n.id,
|
|
289
|
+
n.relative_path,
|
|
290
|
+
n.content,
|
|
291
|
+
n.title,
|
|
292
|
+
r.name as root_name,
|
|
293
|
+
r.path as root_path,
|
|
294
|
+
n.modified_at
|
|
295
|
+
FROM notes n
|
|
296
|
+
JOIN roots r ON n.root_id = r.id
|
|
297
|
+
WHERE COALESCE(n.is_trashed, FALSE) = FALSE
|
|
298
|
+
AND n.relative_path IS NOT NULL
|
|
299
299
|
`;
|
|
300
300
|
const params = [];
|
|
301
301
|
let paramIndex = 1;
|
|
@@ -336,20 +336,20 @@ export class PostgresAdapter {
|
|
|
336
336
|
return 'skipped';
|
|
337
337
|
}
|
|
338
338
|
// Update existing note (also sets relative_path and project in case they were NULL)
|
|
339
|
-
await this.pool.query(`
|
|
340
|
-
UPDATE notes SET
|
|
341
|
-
content = $1,
|
|
342
|
-
hash = $2,
|
|
343
|
-
title = $3,
|
|
344
|
-
description = $4,
|
|
345
|
-
keywords = $5::jsonb,
|
|
346
|
-
file_path = $6,
|
|
347
|
-
relative_path = $7,
|
|
348
|
-
file_size = $8,
|
|
349
|
-
project = $9,
|
|
350
|
-
modified_at = NOW(),
|
|
351
|
-
indexed_at = NOW()
|
|
352
|
-
WHERE id = $10
|
|
339
|
+
await this.pool.query(`
|
|
340
|
+
UPDATE notes SET
|
|
341
|
+
content = $1,
|
|
342
|
+
hash = $2,
|
|
343
|
+
title = $3,
|
|
344
|
+
description = $4,
|
|
345
|
+
keywords = $5::jsonb,
|
|
346
|
+
file_path = $6,
|
|
347
|
+
relative_path = $7,
|
|
348
|
+
file_size = $8,
|
|
349
|
+
project = $9,
|
|
350
|
+
modified_at = NOW(),
|
|
351
|
+
indexed_at = NOW()
|
|
352
|
+
WHERE id = $10
|
|
353
353
|
`, [
|
|
354
354
|
file.content,
|
|
355
355
|
file.hash,
|
|
@@ -365,11 +365,11 @@ export class PostgresAdapter {
|
|
|
365
365
|
return 'updated';
|
|
366
366
|
}
|
|
367
367
|
// Insert new note (include user_id and project if configured)
|
|
368
|
-
await this.pool.query(`
|
|
369
|
-
INSERT INTO notes (
|
|
370
|
-
root_id, file_path, relative_path, content, hash,
|
|
371
|
-
title, description, keywords, file_size, project, modified_at, indexed_at, user_id
|
|
372
|
-
) VALUES ($1, $2, $3, $4, $5, $6, $7, $8::jsonb, $9, $10, NOW(), NOW(), $11)
|
|
368
|
+
await this.pool.query(`
|
|
369
|
+
INSERT INTO notes (
|
|
370
|
+
root_id, file_path, relative_path, content, hash,
|
|
371
|
+
title, description, keywords, file_size, project, modified_at, indexed_at, user_id
|
|
372
|
+
) VALUES ($1, $2, $3, $4, $5, $6, $7, $8::jsonb, $9, $10, NOW(), NOW(), $11)
|
|
373
373
|
`, [
|
|
374
374
|
file.rootId,
|
|
375
375
|
file.path,
|
|
@@ -390,11 +390,11 @@ export class PostgresAdapter {
|
|
|
390
390
|
*/
|
|
391
391
|
async getRootsForSync() {
|
|
392
392
|
const userFilter = this.buildUserFilter(1);
|
|
393
|
-
const result = await this.pool.query(`
|
|
394
|
-
SELECT id, name, path, last_scanned_at
|
|
395
|
-
FROM roots
|
|
396
|
-
WHERE COALESCE(is_visible, TRUE) = TRUE
|
|
397
|
-
AND ${userFilter.condition}
|
|
393
|
+
const result = await this.pool.query(`
|
|
394
|
+
SELECT id, name, path, last_scanned_at
|
|
395
|
+
FROM roots
|
|
396
|
+
WHERE COALESCE(is_visible, TRUE) = TRUE
|
|
397
|
+
AND ${userFilter.condition}
|
|
398
398
|
`, userFilter.params);
|
|
399
399
|
return result.rows.map(row => ({
|
|
400
400
|
id: row.id,
|
|
@@ -416,10 +416,10 @@ export class PostgresAdapter {
|
|
|
416
416
|
*/
|
|
417
417
|
async createRoot(options) {
|
|
418
418
|
const { name, path, type = 'folder' } = options;
|
|
419
|
-
const result = await this.pool.query(`
|
|
420
|
-
INSERT INTO roots (name, path, type, is_active, is_visible, user_id)
|
|
421
|
-
VALUES ($1, $2, $3, FALSE, TRUE, $4)
|
|
422
|
-
RETURNING id, name, path
|
|
419
|
+
const result = await this.pool.query(`
|
|
420
|
+
INSERT INTO roots (name, path, type, is_active, is_visible, user_id)
|
|
421
|
+
VALUES ($1, $2, $3, FALSE, TRUE, $4)
|
|
422
|
+
RETURNING id, name, path
|
|
423
423
|
`, [name, path, type, this.userId]);
|
|
424
424
|
return result.rows[0];
|
|
425
425
|
}
|
|
@@ -427,9 +427,9 @@ export class PostgresAdapter {
|
|
|
427
427
|
* Log a sync operation (for Dashboard sync activity display)
|
|
428
428
|
*/
|
|
429
429
|
async logSyncOperation(options) {
|
|
430
|
-
await this.pool.query(`
|
|
431
|
-
INSERT INTO sync_logs (root_id, files_scanned, files_added, files_updated, files_deleted, source, machine_name, notes)
|
|
432
|
-
VALUES ($1, $2, $3, $4, $5, $6, $7, $8)
|
|
430
|
+
await this.pool.query(`
|
|
431
|
+
INSERT INTO sync_logs (root_id, files_scanned, files_added, files_updated, files_deleted, source, machine_name, notes)
|
|
432
|
+
VALUES ($1, $2, $3, $4, $5, $6, $7, $8)
|
|
433
433
|
`, [
|
|
434
434
|
options.rootId,
|
|
435
435
|
options.filesScanned,
|
|
@@ -445,10 +445,10 @@ export class PostgresAdapter {
|
|
|
445
445
|
* Get recent sync logs for a root
|
|
446
446
|
*/
|
|
447
447
|
async getSyncLogs(rootId, limit = 10) {
|
|
448
|
-
let sql = `
|
|
449
|
-
SELECT id, root_id, synced_at, files_scanned, files_added,
|
|
450
|
-
files_updated, files_deleted, source, machine_name, notes
|
|
451
|
-
FROM sync_logs
|
|
448
|
+
let sql = `
|
|
449
|
+
SELECT id, root_id, synced_at, files_scanned, files_added,
|
|
450
|
+
files_updated, files_deleted, source, machine_name, notes
|
|
451
|
+
FROM sync_logs
|
|
452
452
|
`;
|
|
453
453
|
const params = [];
|
|
454
454
|
let paramIndex = 1;
|
|
@@ -490,9 +490,9 @@ export class PostgresAdapter {
|
|
|
490
490
|
* Set last sync time in settings
|
|
491
491
|
*/
|
|
492
492
|
async setLastSyncTime(timestamp) {
|
|
493
|
-
await this.pool.query(`
|
|
494
|
-
INSERT INTO settings (key, value) VALUES ('last_sync_time', $1)
|
|
495
|
-
ON CONFLICT (key) DO UPDATE SET value = $1
|
|
493
|
+
await this.pool.query(`
|
|
494
|
+
INSERT INTO settings (key, value) VALUES ('last_sync_time', $1)
|
|
495
|
+
ON CONFLICT (key) DO UPDATE SET value = $1
|
|
496
496
|
`, [timestamp]);
|
|
497
497
|
}
|
|
498
498
|
/**
|
|
@@ -500,12 +500,12 @@ export class PostgresAdapter {
|
|
|
500
500
|
*/
|
|
501
501
|
async getNoteCountByRoot() {
|
|
502
502
|
const userFilter = this.buildUserFilter(1);
|
|
503
|
-
const result = await this.pool.query(`
|
|
504
|
-
SELECT root_id, COUNT(*) as count
|
|
505
|
-
FROM notes
|
|
506
|
-
WHERE COALESCE(is_trashed, FALSE) = FALSE
|
|
507
|
-
AND ${userFilter.condition}
|
|
508
|
-
GROUP BY root_id
|
|
503
|
+
const result = await this.pool.query(`
|
|
504
|
+
SELECT root_id, COUNT(*) as count
|
|
505
|
+
FROM notes
|
|
506
|
+
WHERE COALESCE(is_trashed, FALSE) = FALSE
|
|
507
|
+
AND ${userFilter.condition}
|
|
508
|
+
GROUP BY root_id
|
|
509
509
|
`, userFilter.params);
|
|
510
510
|
const counts = new Map();
|
|
511
511
|
for (const row of result.rows) {
|
|
@@ -565,16 +565,16 @@ export class PostgresAdapter {
|
|
|
565
565
|
*/
|
|
566
566
|
async getNoteForEnhancement(id) {
|
|
567
567
|
const userFilter = this.buildUserFilter(2);
|
|
568
|
-
const result = await this.pool.query(`
|
|
569
|
-
SELECT
|
|
570
|
-
n.id, n.title, n.description, n.keywords,
|
|
571
|
-
COALESCE(n.aliases, '{}') as aliases,
|
|
572
|
-
n.content, n.file_path, n.ai_enhanced_at,
|
|
573
|
-
r.name as root_name
|
|
574
|
-
FROM notes n
|
|
575
|
-
LEFT JOIN roots r ON n.root_id = r.id
|
|
576
|
-
WHERE n.id = $1 AND COALESCE(n.is_trashed, FALSE) = FALSE
|
|
577
|
-
AND n.${userFilter.condition}
|
|
568
|
+
const result = await this.pool.query(`
|
|
569
|
+
SELECT
|
|
570
|
+
n.id, n.title, n.description, n.keywords,
|
|
571
|
+
COALESCE(n.aliases, '{}') as aliases,
|
|
572
|
+
n.content, n.file_path, n.ai_enhanced_at,
|
|
573
|
+
r.name as root_name
|
|
574
|
+
FROM notes n
|
|
575
|
+
LEFT JOIN roots r ON n.root_id = r.id
|
|
576
|
+
WHERE n.id = $1 AND COALESCE(n.is_trashed, FALSE) = FALSE
|
|
577
|
+
AND n.${userFilter.condition}
|
|
578
578
|
`, [id, ...userFilter.params]);
|
|
579
579
|
if (result.rows.length === 0)
|
|
580
580
|
return null;
|
|
@@ -597,19 +597,19 @@ export class PostgresAdapter {
|
|
|
597
597
|
async getNotesNeedingEnhancement(options = {}) {
|
|
598
598
|
const { root, limit = 50, important_only = false } = options;
|
|
599
599
|
const userFilter = this.buildUserFilter(1);
|
|
600
|
-
let sql = `
|
|
601
|
-
SELECT
|
|
602
|
-
id, title, file_path,
|
|
603
|
-
(description IS NOT NULL AND description != '') as has_description,
|
|
604
|
-
(keywords IS NOT NULL AND jsonb_array_length(keywords) > 0) as has_keywords,
|
|
605
|
-
COALESCE(is_favorite, FALSE) as is_favorite,
|
|
606
|
-
(content LIKE '%⭐%') as has_stars,
|
|
607
|
-
COALESCE(points, 0) as points,
|
|
608
|
-
ai_enhanced_at
|
|
609
|
-
FROM notes
|
|
610
|
-
WHERE COALESCE(is_trashed, FALSE) = FALSE
|
|
611
|
-
AND (description IS NULL OR description = '' OR keywords IS NULL OR jsonb_array_length(keywords) = 0)
|
|
612
|
-
AND ${userFilter.condition}
|
|
600
|
+
let sql = `
|
|
601
|
+
SELECT
|
|
602
|
+
id, title, file_path,
|
|
603
|
+
(description IS NOT NULL AND description != '') as has_description,
|
|
604
|
+
(keywords IS NOT NULL AND jsonb_array_length(keywords) > 0) as has_keywords,
|
|
605
|
+
COALESCE(is_favorite, FALSE) as is_favorite,
|
|
606
|
+
(content LIKE '%⭐%') as has_stars,
|
|
607
|
+
COALESCE(points, 0) as points,
|
|
608
|
+
ai_enhanced_at
|
|
609
|
+
FROM notes
|
|
610
|
+
WHERE COALESCE(is_trashed, FALSE) = FALSE
|
|
611
|
+
AND (description IS NULL OR description = '' OR keywords IS NULL OR jsonb_array_length(keywords) = 0)
|
|
612
|
+
AND ${userFilter.condition}
|
|
613
613
|
`;
|
|
614
614
|
const params = [...userFilter.params];
|
|
615
615
|
let paramIndex = userFilter.nextParamIndex;
|
|
@@ -674,16 +674,16 @@ export class PostgresAdapter {
|
|
|
674
674
|
*/
|
|
675
675
|
async getNoteForScoring(id) {
|
|
676
676
|
const userFilter = this.buildUserFilter(2);
|
|
677
|
-
const result = await this.pool.query(`
|
|
678
|
-
SELECT
|
|
679
|
-
id, title, description, keywords, content, file_path,
|
|
680
|
-
importance_score, quality_score,
|
|
681
|
-
COALESCE(relations, '[]'::jsonb) as relations,
|
|
682
|
-
COALESCE(is_favorite, FALSE) as is_favorite,
|
|
683
|
-
COALESCE(points, 0) as points
|
|
684
|
-
FROM notes
|
|
685
|
-
WHERE id = $1 AND COALESCE(is_trashed, FALSE) = FALSE
|
|
686
|
-
AND ${userFilter.condition}
|
|
677
|
+
const result = await this.pool.query(`
|
|
678
|
+
SELECT
|
|
679
|
+
id, title, description, keywords, content, file_path,
|
|
680
|
+
importance_score, quality_score,
|
|
681
|
+
COALESCE(relations, '[]'::jsonb) as relations,
|
|
682
|
+
COALESCE(is_favorite, FALSE) as is_favorite,
|
|
683
|
+
COALESCE(points, 0) as points
|
|
684
|
+
FROM notes
|
|
685
|
+
WHERE id = $1 AND COALESCE(is_trashed, FALSE) = FALSE
|
|
686
|
+
AND ${userFilter.condition}
|
|
687
687
|
`, [id, ...userFilter.params]);
|
|
688
688
|
if (result.rows.length === 0)
|
|
689
689
|
return null;
|
|
@@ -770,11 +770,11 @@ export class PostgresAdapter {
|
|
|
770
770
|
async getNotesForRelationAnalysis(excludeId, options = {}) {
|
|
771
771
|
const { root, limit = 50 } = options;
|
|
772
772
|
const userFilter = this.buildUserFilter(2);
|
|
773
|
-
let sql = `
|
|
774
|
-
SELECT id, title, description, keywords, file_path
|
|
775
|
-
FROM notes
|
|
776
|
-
WHERE id != $1 AND COALESCE(is_trashed, FALSE) = FALSE
|
|
777
|
-
AND ${userFilter.condition}
|
|
773
|
+
let sql = `
|
|
774
|
+
SELECT id, title, description, keywords, file_path
|
|
775
|
+
FROM notes
|
|
776
|
+
WHERE id != $1 AND COALESCE(is_trashed, FALSE) = FALSE
|
|
777
|
+
AND ${userFilter.condition}
|
|
778
778
|
`;
|
|
779
779
|
const params = [excludeId, ...userFilter.params];
|
|
780
780
|
let paramIndex = userFilter.nextParamIndex;
|
|
@@ -810,54 +810,54 @@ export class PostgresAdapter {
|
|
|
810
810
|
paramIndex++;
|
|
811
811
|
}
|
|
812
812
|
// Get aggregate stats
|
|
813
|
-
const statsResult = await this.pool.query(`
|
|
814
|
-
SELECT
|
|
815
|
-
COUNT(*) as total,
|
|
816
|
-
COUNT(importance_score) as with_importance,
|
|
817
|
-
COUNT(quality_score) as with_quality,
|
|
818
|
-
COUNT(*) FILTER (WHERE jsonb_array_length(COALESCE(relations, '[]'::jsonb)) > 0) as with_relations,
|
|
819
|
-
COUNT(*) FILTER (WHERE description IS NOT NULL AND description != '') as with_description,
|
|
820
|
-
COUNT(*) FILTER (WHERE keywords IS NOT NULL AND jsonb_array_length(keywords) > 0) as with_keywords
|
|
821
|
-
FROM notes
|
|
822
|
-
WHERE ${whereClause}
|
|
813
|
+
const statsResult = await this.pool.query(`
|
|
814
|
+
SELECT
|
|
815
|
+
COUNT(*) as total,
|
|
816
|
+
COUNT(importance_score) as with_importance,
|
|
817
|
+
COUNT(quality_score) as with_quality,
|
|
818
|
+
COUNT(*) FILTER (WHERE jsonb_array_length(COALESCE(relations, '[]'::jsonb)) > 0) as with_relations,
|
|
819
|
+
COUNT(*) FILTER (WHERE description IS NOT NULL AND description != '') as with_description,
|
|
820
|
+
COUNT(*) FILTER (WHERE keywords IS NOT NULL AND jsonb_array_length(keywords) > 0) as with_keywords
|
|
821
|
+
FROM notes
|
|
822
|
+
WHERE ${whereClause}
|
|
823
823
|
`, params);
|
|
824
824
|
const stats = statsResult.rows[0];
|
|
825
825
|
// Get low quality docs (score < 50 or NULL, prioritize NULL)
|
|
826
826
|
const lowQualityParams = [...params, limit];
|
|
827
|
-
const lowQualityResult = await this.pool.query(`
|
|
828
|
-
SELECT id, title, quality_score, file_path
|
|
829
|
-
FROM notes
|
|
830
|
-
WHERE ${whereClause} AND (quality_score IS NULL OR quality_score < 50)
|
|
831
|
-
ORDER BY quality_score NULLS FIRST, modified_at DESC
|
|
832
|
-
LIMIT $${paramIndex}
|
|
827
|
+
const lowQualityResult = await this.pool.query(`
|
|
828
|
+
SELECT id, title, quality_score, file_path
|
|
829
|
+
FROM notes
|
|
830
|
+
WHERE ${whereClause} AND (quality_score IS NULL OR quality_score < 50)
|
|
831
|
+
ORDER BY quality_score NULLS FIRST, modified_at DESC
|
|
832
|
+
LIMIT $${paramIndex}
|
|
833
833
|
`, lowQualityParams);
|
|
834
834
|
// Get low importance docs (score < 30 or NULL)
|
|
835
|
-
const lowImportanceResult = await this.pool.query(`
|
|
836
|
-
SELECT id, title, importance_score, file_path
|
|
837
|
-
FROM notes
|
|
838
|
-
WHERE ${whereClause} AND (importance_score IS NULL OR importance_score < 30)
|
|
839
|
-
ORDER BY importance_score NULLS FIRST, modified_at DESC
|
|
840
|
-
LIMIT $${paramIndex}
|
|
835
|
+
const lowImportanceResult = await this.pool.query(`
|
|
836
|
+
SELECT id, title, importance_score, file_path
|
|
837
|
+
FROM notes
|
|
838
|
+
WHERE ${whereClause} AND (importance_score IS NULL OR importance_score < 30)
|
|
839
|
+
ORDER BY importance_score NULLS FIRST, modified_at DESC
|
|
840
|
+
LIMIT $${paramIndex}
|
|
841
841
|
`, lowQualityParams);
|
|
842
842
|
// Get orphan docs (no relations)
|
|
843
|
-
const orphansResult = await this.pool.query(`
|
|
844
|
-
SELECT id, title, file_path
|
|
845
|
-
FROM notes
|
|
846
|
-
WHERE ${whereClause} AND (relations IS NULL OR jsonb_array_length(relations) = 0)
|
|
847
|
-
ORDER BY modified_at DESC
|
|
848
|
-
LIMIT $${paramIndex}
|
|
843
|
+
const orphansResult = await this.pool.query(`
|
|
844
|
+
SELECT id, title, file_path
|
|
845
|
+
FROM notes
|
|
846
|
+
WHERE ${whereClause} AND (relations IS NULL OR jsonb_array_length(relations) = 0)
|
|
847
|
+
ORDER BY modified_at DESC
|
|
848
|
+
LIMIT $${paramIndex}
|
|
849
849
|
`, lowQualityParams);
|
|
850
850
|
// Get docs missing metadata
|
|
851
|
-
const missingMetadataResult = await this.pool.query(`
|
|
852
|
-
SELECT
|
|
853
|
-
id, title, file_path,
|
|
854
|
-
(description IS NULL OR description = '') as missing_desc,
|
|
855
|
-
(keywords IS NULL OR jsonb_array_length(keywords) = 0) as missing_kw
|
|
856
|
-
FROM notes
|
|
857
|
-
WHERE ${whereClause}
|
|
858
|
-
AND ((description IS NULL OR description = '') OR (keywords IS NULL OR jsonb_array_length(keywords) = 0))
|
|
859
|
-
ORDER BY modified_at DESC
|
|
860
|
-
LIMIT $${paramIndex}
|
|
851
|
+
const missingMetadataResult = await this.pool.query(`
|
|
852
|
+
SELECT
|
|
853
|
+
id, title, file_path,
|
|
854
|
+
(description IS NULL OR description = '') as missing_desc,
|
|
855
|
+
(keywords IS NULL OR jsonb_array_length(keywords) = 0) as missing_kw
|
|
856
|
+
FROM notes
|
|
857
|
+
WHERE ${whereClause}
|
|
858
|
+
AND ((description IS NULL OR description = '') OR (keywords IS NULL OR jsonb_array_length(keywords) = 0))
|
|
859
|
+
ORDER BY modified_at DESC
|
|
860
|
+
LIMIT $${paramIndex}
|
|
861
861
|
`, lowQualityParams);
|
|
862
862
|
return {
|
|
863
863
|
total: parseInt(stats.total),
|
|
@@ -889,12 +889,12 @@ export class PostgresAdapter {
|
|
|
889
889
|
async getNotesWithoutEmbeddings(options = {}) {
|
|
890
890
|
const { limit = 50, root } = options;
|
|
891
891
|
const userFilter = this.buildUserFilter(1);
|
|
892
|
-
let sql = `
|
|
893
|
-
SELECT id, title, content, file_path
|
|
894
|
-
FROM notes
|
|
895
|
-
WHERE embedding IS NULL
|
|
896
|
-
AND COALESCE(is_trashed, FALSE) = FALSE
|
|
897
|
-
AND ${userFilter.condition}
|
|
892
|
+
let sql = `
|
|
893
|
+
SELECT id, title, content, file_path
|
|
894
|
+
FROM notes
|
|
895
|
+
WHERE embedding IS NULL
|
|
896
|
+
AND COALESCE(is_trashed, FALSE) = FALSE
|
|
897
|
+
AND ${userFilter.condition}
|
|
898
898
|
`;
|
|
899
899
|
const params = [...userFilter.params];
|
|
900
900
|
let paramIndex = userFilter.nextParamIndex;
|
|
@@ -925,14 +925,14 @@ export class PostgresAdapter {
|
|
|
925
925
|
const { limit = 10, root } = options;
|
|
926
926
|
const vectorString = `[${embedding.join(',')}]`;
|
|
927
927
|
const userFilter = this.buildUserFilter(2);
|
|
928
|
-
let sql = `
|
|
929
|
-
SELECT
|
|
930
|
-
id, title, description, file_path,
|
|
931
|
-
1 - (embedding <=> $1::vector) as similarity
|
|
932
|
-
FROM notes
|
|
933
|
-
WHERE embedding IS NOT NULL
|
|
934
|
-
AND COALESCE(is_trashed, FALSE) = FALSE
|
|
935
|
-
AND ${userFilter.condition}
|
|
928
|
+
let sql = `
|
|
929
|
+
SELECT
|
|
930
|
+
id, title, description, file_path,
|
|
931
|
+
1 - (embedding <=> $1::vector) as similarity
|
|
932
|
+
FROM notes
|
|
933
|
+
WHERE embedding IS NOT NULL
|
|
934
|
+
AND COALESCE(is_trashed, FALSE) = FALSE
|
|
935
|
+
AND ${userFilter.condition}
|
|
936
936
|
`;
|
|
937
937
|
const params = [vectorString, ...userFilter.params];
|
|
938
938
|
let paramIndex = userFilter.nextParamIndex;
|
|
@@ -957,11 +957,11 @@ export class PostgresAdapter {
|
|
|
957
957
|
*/
|
|
958
958
|
async getNoteEmbedding(id) {
|
|
959
959
|
const userFilter = this.buildUserFilter(2);
|
|
960
|
-
const result = await this.pool.query(`
|
|
961
|
-
SELECT id, title, embedding
|
|
962
|
-
FROM notes
|
|
963
|
-
WHERE id = $1 AND COALESCE(is_trashed, FALSE) = FALSE
|
|
964
|
-
AND ${userFilter.condition}
|
|
960
|
+
const result = await this.pool.query(`
|
|
961
|
+
SELECT id, title, embedding
|
|
962
|
+
FROM notes
|
|
963
|
+
WHERE id = $1 AND COALESCE(is_trashed, FALSE) = FALSE
|
|
964
|
+
AND ${userFilter.condition}
|
|
965
965
|
`, [id, ...userFilter.params]);
|
|
966
966
|
if (result.rows.length === 0)
|
|
967
967
|
return null;
|
|
@@ -992,17 +992,17 @@ export class PostgresAdapter {
|
|
|
992
992
|
}
|
|
993
993
|
// Find similar notes (excluding the source note)
|
|
994
994
|
const similarUserFilter = this.buildUserFilter(4);
|
|
995
|
-
const result = await this.pool.query(`
|
|
996
|
-
SELECT
|
|
997
|
-
id, title, description, file_path,
|
|
998
|
-
1 - (embedding <=> $1) as similarity
|
|
999
|
-
FROM notes
|
|
1000
|
-
WHERE id != $2
|
|
1001
|
-
AND embedding IS NOT NULL
|
|
1002
|
-
AND COALESCE(is_trashed, FALSE) = FALSE
|
|
1003
|
-
AND ${similarUserFilter.condition}
|
|
1004
|
-
ORDER BY embedding <=> $1
|
|
1005
|
-
LIMIT $3
|
|
995
|
+
const result = await this.pool.query(`
|
|
996
|
+
SELECT
|
|
997
|
+
id, title, description, file_path,
|
|
998
|
+
1 - (embedding <=> $1) as similarity
|
|
999
|
+
FROM notes
|
|
1000
|
+
WHERE id != $2
|
|
1001
|
+
AND embedding IS NOT NULL
|
|
1002
|
+
AND COALESCE(is_trashed, FALSE) = FALSE
|
|
1003
|
+
AND ${similarUserFilter.condition}
|
|
1004
|
+
ORDER BY embedding <=> $1
|
|
1005
|
+
LIMIT $3
|
|
1006
1006
|
`, [noteResult.rows[0].embedding, noteId, limit, ...similarUserFilter.params]);
|
|
1007
1007
|
return result.rows.map(row => ({
|
|
1008
1008
|
id: row.id,
|
|
@@ -1017,14 +1017,14 @@ export class PostgresAdapter {
|
|
|
1017
1017
|
*/
|
|
1018
1018
|
async getEmbeddingStats() {
|
|
1019
1019
|
const userFilter = this.buildUserFilter(1);
|
|
1020
|
-
const result = await this.pool.query(`
|
|
1021
|
-
SELECT
|
|
1022
|
-
COUNT(*) as total,
|
|
1023
|
-
COUNT(embedding) as with_embeddings,
|
|
1024
|
-
COUNT(*) - COUNT(embedding) as without_embeddings
|
|
1025
|
-
FROM notes
|
|
1026
|
-
WHERE COALESCE(is_trashed, FALSE) = FALSE
|
|
1027
|
-
AND ${userFilter.condition}
|
|
1020
|
+
const result = await this.pool.query(`
|
|
1021
|
+
SELECT
|
|
1022
|
+
COUNT(*) as total,
|
|
1023
|
+
COUNT(embedding) as with_embeddings,
|
|
1024
|
+
COUNT(*) - COUNT(embedding) as without_embeddings
|
|
1025
|
+
FROM notes
|
|
1026
|
+
WHERE COALESCE(is_trashed, FALSE) = FALSE
|
|
1027
|
+
AND ${userFilter.condition}
|
|
1028
1028
|
`, userFilter.params);
|
|
1029
1029
|
const row = result.rows[0];
|
|
1030
1030
|
return {
|
|
@@ -77,25 +77,25 @@ export async function generateMetadata(content) {
|
|
|
77
77
|
if (contentToAnalyze.length > maxChars) {
|
|
78
78
|
contentToAnalyze = contentToAnalyze.substring(0, maxChars) + '...';
|
|
79
79
|
}
|
|
80
|
-
const prompt = `Analyze this markdown document and generate metadata for it.
|
|
81
|
-
|
|
82
|
-
Document content:
|
|
83
|
-
---
|
|
84
|
-
${contentToAnalyze}
|
|
85
|
-
---
|
|
86
|
-
|
|
87
|
-
Generate the following metadata in JSON format:
|
|
88
|
-
1. title: A concise, descriptive title (max 80 chars). IMPORTANT: If the document starts with a # heading on the first line, use that FIRST heading as the title (clean it up if needed). Only generate a new title if there's no H1 heading at the start.
|
|
89
|
-
2. description: A brief summary of what this document is about (1-2 sentences, max 200 chars)
|
|
90
|
-
3. keywords: An array of 5-15 relevant keywords/tags including:
|
|
91
|
-
- Main topics and concepts
|
|
92
|
-
- Technical terms and technologies (Redis, SignalR, MongoDB, etc.)
|
|
93
|
-
- Component/service/class names found in code (e.g., PrintController, BtXmlExecutor, DevicesInternalController)
|
|
94
|
-
- Include BOTH CamelCase (PrintController) AND space-separated (Print Controller) versions of component names
|
|
95
|
-
- API endpoint patterns or route names if mentioned
|
|
96
|
-
Avoid generic words like "document", "file", "note", "example", "sample".
|
|
97
|
-
|
|
98
|
-
Respond with ONLY valid JSON in this exact format, no markdown code blocks:
|
|
80
|
+
const prompt = `Analyze this markdown document and generate metadata for it.
|
|
81
|
+
|
|
82
|
+
Document content:
|
|
83
|
+
---
|
|
84
|
+
${contentToAnalyze}
|
|
85
|
+
---
|
|
86
|
+
|
|
87
|
+
Generate the following metadata in JSON format:
|
|
88
|
+
1. title: A concise, descriptive title (max 80 chars). IMPORTANT: If the document starts with a # heading on the first line, use that FIRST heading as the title (clean it up if needed). Only generate a new title if there's no H1 heading at the start.
|
|
89
|
+
2. description: A brief summary of what this document is about (1-2 sentences, max 200 chars)
|
|
90
|
+
3. keywords: An array of 5-15 relevant keywords/tags including:
|
|
91
|
+
- Main topics and concepts
|
|
92
|
+
- Technical terms and technologies (Redis, SignalR, MongoDB, etc.)
|
|
93
|
+
- Component/service/class names found in code (e.g., PrintController, BtXmlExecutor, DevicesInternalController)
|
|
94
|
+
- Include BOTH CamelCase (PrintController) AND space-separated (Print Controller) versions of component names
|
|
95
|
+
- API endpoint patterns or route names if mentioned
|
|
96
|
+
Avoid generic words like "document", "file", "note", "example", "sample".
|
|
97
|
+
|
|
98
|
+
Respond with ONLY valid JSON in this exact format, no markdown code blocks:
|
|
99
99
|
{"title": "...", "description": "...", "keywords": ["...", "..."]}`;
|
|
100
100
|
try {
|
|
101
101
|
const result = await genAI.models.generateContent({
|