@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.
- package/LICENSE +21 -0
- package/README.md +218 -0
- package/dist/api/NoesisClient.d.ts +501 -0
- package/dist/api/NoesisClient.d.ts.map +1 -0
- package/dist/api/NoesisClient.js +654 -0
- package/dist/api/NoesisClient.js.map +1 -0
- package/dist/cli/setup.d.ts +8 -0
- package/dist/cli/setup.d.ts.map +1 -0
- package/dist/cli/setup.js +148 -0
- package/dist/cli/setup.js.map +1 -0
- package/dist/database/PostgresAdapter.d.ts +385 -0
- package/dist/database/PostgresAdapter.d.ts.map +1 -0
- package/dist/database/PostgresAdapter.js +1043 -0
- package/dist/database/PostgresAdapter.js.map +1 -0
- package/dist/index.d.ts +31 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +126 -0
- package/dist/index.js.map +1 -0
- package/dist/services/embedding.d.ts +38 -0
- package/dist/services/embedding.d.ts.map +1 -0
- package/dist/services/embedding.js +126 -0
- package/dist/services/embedding.js.map +1 -0
- package/dist/tools/SyncStateManager.d.ts +65 -0
- package/dist/tools/SyncStateManager.d.ts.map +1 -0
- package/dist/tools/SyncStateManager.js +217 -0
- package/dist/tools/SyncStateManager.js.map +1 -0
- package/dist/tools/index.d.ts +14 -0
- package/dist/tools/index.d.ts.map +1 -0
- package/dist/tools/index.js +3345 -0
- package/dist/tools/index.js.map +1 -0
- package/dist/tools/navis.d.ts +11 -0
- package/dist/tools/navis.d.ts.map +1 -0
- package/dist/tools/navis.js +231 -0
- package/dist/tools/navis.js.map +1 -0
- package/dist/types/index.d.ts +104 -0
- package/dist/types/index.d.ts.map +1 -0
- package/dist/types/index.js +5 -0
- package/dist/types/index.js.map +1 -0
- package/dist/utils/suggestPath.d.ts +15 -0
- package/dist/utils/suggestPath.d.ts.map +1 -0
- package/dist/utils/suggestPath.js +52 -0
- package/dist/utils/suggestPath.js.map +1 -0
- package/package.json +71 -0
- package/scripts/noesis-sync.mjs +469 -0
- package/skill-templates/noesis-refine-note.md +92 -0
- package/skill-templates/noesis-sync.md +110 -0
- 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
|