@noesis-brain/mcp-server 2.1.0 → 2.1.2
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/dist/agent/runner.d.ts +10 -0
- package/dist/agent/runner.d.ts.map +1 -1
- package/dist/agent/runner.js +115 -12
- package/dist/agent/runner.js.map +1 -1
- package/dist/api/NoesisClient.d.ts +19 -0
- package/dist/api/NoesisClient.d.ts.map +1 -1
- package/dist/api/NoesisClient.js +44 -0
- package/dist/api/NoesisClient.js.map +1 -1
- package/dist/tools/backlog.d.ts.map +1 -1
- package/dist/tools/backlog.js +11 -7
- package/dist/tools/backlog.js.map +1 -1
- package/dist/tools/exam/examNoteTemplate.d.ts +32 -0
- package/dist/tools/exam/examNoteTemplate.d.ts.map +1 -0
- package/dist/tools/exam/examNoteTemplate.js +110 -0
- package/dist/tools/exam/examNoteTemplate.js.map +1 -0
- package/dist/tools/exam/types.d.ts +89 -0
- package/dist/tools/exam/types.d.ts.map +1 -0
- package/dist/tools/exam/types.js +14 -0
- package/dist/tools/exam/types.js.map +1 -0
- package/dist/tools/exam.d.ts +12 -0
- package/dist/tools/exam.d.ts.map +1 -0
- package/dist/tools/exam.js +46 -0
- package/dist/tools/exam.js.map +1 -0
- package/dist/tools/index.d.ts +40 -0
- package/dist/tools/index.d.ts.map +1 -1
- package/dist/tools/index.js +98 -70
- package/dist/tools/index.js.map +1 -1
- package/package.json +3 -2
- package/scripts/noesis-capture-watcher.mjs +51 -5
package/dist/tools/index.js
CHANGED
|
@@ -13,6 +13,7 @@ import { initEmbeddingService, generateEmbedding, generateEmbeddingsBatch } from
|
|
|
13
13
|
import { SyncStateManager, determineSyncDirection } from './SyncStateManager.js';
|
|
14
14
|
import { registerNaviTools } from './navis.js';
|
|
15
15
|
import { registerBacklogTools } from './backlog.js';
|
|
16
|
+
import { registerExamTools } from './exam.js';
|
|
16
17
|
import { suggestOtherOsPath } from '../utils/suggestPath.js';
|
|
17
18
|
import { diff3Merge, diffPatch } from 'node-diff3';
|
|
18
19
|
// ============================================
|
|
@@ -185,6 +186,7 @@ export function registerTools(server, services) {
|
|
|
185
186
|
// Register Navi management tools
|
|
186
187
|
registerNaviTools(server, client);
|
|
187
188
|
registerBacklogTools(server, client);
|
|
189
|
+
registerExamTools(server, client);
|
|
188
190
|
// Register search_notes tool
|
|
189
191
|
server.tool('search_notes', 'Search your knowledge base using full-text search. Returns relevant notes ranked by BM25 relevance.', {
|
|
190
192
|
query: z.string().describe('Search query (natural language or keywords)'),
|
|
@@ -1390,20 +1392,27 @@ export function registerTools(server, services) {
|
|
|
1390
1392
|
keywords: cloudKw.length > 0 ? cloudKw : undefined
|
|
1391
1393
|
})
|
|
1392
1394
|
: mergedContent;
|
|
1393
|
-
|
|
1394
|
-
|
|
1395
|
-
|
|
1396
|
-
|
|
1397
|
-
|
|
1398
|
-
|
|
1399
|
-
|
|
1400
|
-
|
|
1401
|
-
|
|
1402
|
-
|
|
1403
|
-
|
|
1404
|
-
|
|
1405
|
-
|
|
1406
|
-
|
|
1395
|
+
// Safety net: never silently destroy content on merge (see per-file branch).
|
|
1396
|
+
if (enrichedContent.length < localFile.content.length * 0.5 && localFile.content.length > 2048) {
|
|
1397
|
+
const lost = Math.round(100 * (1 - enrichedContent.length / localFile.content.length));
|
|
1398
|
+
result.errors.push(`Skipped push for ${relativePath}: merge would drop ${lost}% of content (${localFile.content.length}->${enrichedContent.length} bytes). Left unchanged to avoid data loss.`);
|
|
1399
|
+
}
|
|
1400
|
+
else {
|
|
1401
|
+
const enrichedHash = NoesisClient.computeHash(enrichedContent);
|
|
1402
|
+
const mergedFile = {
|
|
1403
|
+
...localFile,
|
|
1404
|
+
content: enrichedContent,
|
|
1405
|
+
hash: enrichedHash
|
|
1406
|
+
};
|
|
1407
|
+
// preserveMetadata=true: keep cloud's AI-generated title/description/keywords
|
|
1408
|
+
await client.upsertNote(mergedFile, metadata, { force: true, regenerateMetadata, preserveMetadata: true });
|
|
1409
|
+
// Write enriched content back to local file
|
|
1410
|
+
const localPath = path.join(syncRoot.path, relativePath);
|
|
1411
|
+
fs.writeFileSync(localPath, enrichedContent, 'utf-8');
|
|
1412
|
+
stateMgr.setBaseline(relativePath, { hash: enrichedHash, lastSyncedAt: new Date().toISOString() }, enrichedContent);
|
|
1413
|
+
result.details.push({ file: relativePath, action: 'pushed_update', reason: 'merged' });
|
|
1414
|
+
result.pushed.updated++;
|
|
1415
|
+
}
|
|
1407
1416
|
}
|
|
1408
1417
|
}
|
|
1409
1418
|
else if (direction === 'pull') {
|
|
@@ -3051,26 +3060,35 @@ export async function syncSpecificFiles(filePaths, dryRun, client, force = false
|
|
|
3051
3060
|
keywords: cloudKw.length > 0 ? cloudKw : undefined
|
|
3052
3061
|
})
|
|
3053
3062
|
: mergedContent;
|
|
3054
|
-
|
|
3055
|
-
|
|
3056
|
-
|
|
3057
|
-
|
|
3058
|
-
|
|
3059
|
-
|
|
3060
|
-
|
|
3061
|
-
|
|
3062
|
-
|
|
3063
|
-
|
|
3064
|
-
|
|
3065
|
-
|
|
3066
|
-
|
|
3067
|
-
|
|
3068
|
-
|
|
3069
|
-
|
|
3070
|
-
|
|
3071
|
-
|
|
3072
|
-
|
|
3073
|
-
|
|
3063
|
+
// Safety net: a merge must never silently destroy content. If the merged
|
|
3064
|
+
// result lost most of the local body, abort this file's push instead of
|
|
3065
|
+
// overwriting local + cloud with a truncated note.
|
|
3066
|
+
if (enrichedContent.length < localContent.length * 0.5 && localContent.length > 2048) {
|
|
3067
|
+
const lost = Math.round(100 * (1 - enrichedContent.length / localContent.length));
|
|
3068
|
+
result.errors.push(`Skipped push for ${relativePath}: merge would drop ${lost}% of content (${localContent.length}->${enrichedContent.length} bytes). Left unchanged to avoid data loss.`);
|
|
3069
|
+
}
|
|
3070
|
+
else {
|
|
3071
|
+
const enrichedHash = NoesisClient.computeHash(enrichedContent);
|
|
3072
|
+
const localFile = {
|
|
3073
|
+
path: normalizedPath,
|
|
3074
|
+
relativePath,
|
|
3075
|
+
content: enrichedContent,
|
|
3076
|
+
hash: enrichedHash,
|
|
3077
|
+
mtime: stats.mtime,
|
|
3078
|
+
size: stats.size,
|
|
3079
|
+
rootId: matchingRoot.id,
|
|
3080
|
+
rootName: matchingRoot.name,
|
|
3081
|
+
project
|
|
3082
|
+
};
|
|
3083
|
+
// preserveMetadata=true: keep cloud's AI-generated title/description/keywords
|
|
3084
|
+
await client.upsertNote(localFile, localMetadata, { force: true, regenerateMetadata, preserveMetadata: true });
|
|
3085
|
+
// Write enriched content back to local file
|
|
3086
|
+
fs.writeFileSync(normalizedPath, enrichedContent, 'utf-8');
|
|
3087
|
+
stateMgr.setBaseline(relativePath, { hash: enrichedHash, lastSyncedAt: new Date().toISOString() }, enrichedContent);
|
|
3088
|
+
result.details.push({ file: relativePath, action: 'pushed_update', reason: 'merged' });
|
|
3089
|
+
result.pushed.updated++;
|
|
3090
|
+
affectedRoots.add(matchingRoot.id);
|
|
3091
|
+
}
|
|
3074
3092
|
}
|
|
3075
3093
|
}
|
|
3076
3094
|
else if (direction === 'pull') {
|
|
@@ -3185,22 +3203,36 @@ export async function syncSpecificFiles(filePaths, dryRun, client, force = false
|
|
|
3185
3203
|
}]
|
|
3186
3204
|
};
|
|
3187
3205
|
}
|
|
3206
|
+
/**
|
|
3207
|
+
* Split leading YAML frontmatter from a markdown document.
|
|
3208
|
+
*
|
|
3209
|
+
* The opening `---` must be the very first line and the closing `---` must be on its
|
|
3210
|
+
* own line (line-anchored). This deliberately does NOT use `indexOf('---', 3)`: a bare
|
|
3211
|
+
* substring search matches a `---` inside a frontmatter value, a `---`/`----` thematic
|
|
3212
|
+
* break in the body, or a `---` inside a code fence — which previously caused notes to
|
|
3213
|
+
* be truncated / their frontmatter duplicated on sync.
|
|
3214
|
+
*
|
|
3215
|
+
* @returns raw = whole matched block incl. fences + trailing newline; inner = the YAML
|
|
3216
|
+
* between the fences; body = everything after the block. If there is no valid
|
|
3217
|
+
* leading frontmatter, raw/inner are '' and body is the full content.
|
|
3218
|
+
*/
|
|
3219
|
+
export function splitFrontmatter(content) {
|
|
3220
|
+
const m = content.match(/^---[ \t]*\r?\n([\s\S]*?)\r?\n---[ \t]*(?:\r?\n|$)/);
|
|
3221
|
+
if (!m)
|
|
3222
|
+
return { raw: '', inner: '', body: content };
|
|
3223
|
+
return { raw: m[0], inner: m[1], body: content.slice(m[0].length) };
|
|
3224
|
+
}
|
|
3188
3225
|
/**
|
|
3189
3226
|
* Parse YAML frontmatter from markdown content using js-yaml
|
|
3190
3227
|
*/
|
|
3191
3228
|
function parseYamlFrontmatter(content) {
|
|
3192
3229
|
const result = {};
|
|
3193
|
-
|
|
3194
|
-
if (!
|
|
3195
|
-
return result;
|
|
3196
|
-
}
|
|
3197
|
-
const endIndex = content.indexOf('---', 3);
|
|
3198
|
-
if (endIndex === -1) {
|
|
3230
|
+
const { inner } = splitFrontmatter(content);
|
|
3231
|
+
if (!inner) {
|
|
3199
3232
|
return result;
|
|
3200
3233
|
}
|
|
3201
|
-
const frontmatterStr = content.substring(3, endIndex).trim();
|
|
3202
3234
|
try {
|
|
3203
|
-
const parsed = yaml.load(
|
|
3235
|
+
const parsed = yaml.load(inner);
|
|
3204
3236
|
if (!parsed || typeof parsed !== 'object') {
|
|
3205
3237
|
return result;
|
|
3206
3238
|
}
|
|
@@ -3248,9 +3280,14 @@ function parseCloudKeywords(keywords) {
|
|
|
3248
3280
|
* Update frontmatter in file content with new metadata values
|
|
3249
3281
|
* Preserves existing frontmatter structure and only updates specified fields
|
|
3250
3282
|
*/
|
|
3251
|
-
function updateFrontmatter(content, updates) {
|
|
3252
|
-
|
|
3253
|
-
|
|
3283
|
+
export function updateFrontmatter(content, updates) {
|
|
3284
|
+
const { inner, body } = splitFrontmatter(content);
|
|
3285
|
+
// If no (valid) frontmatter exists, create one
|
|
3286
|
+
if (!inner) {
|
|
3287
|
+
if (content.startsWith('---')) {
|
|
3288
|
+
// Opens with a fence but has no valid closing fence: don't risk mangling it.
|
|
3289
|
+
return content;
|
|
3290
|
+
}
|
|
3254
3291
|
const frontmatterLines = ['---'];
|
|
3255
3292
|
if (updates.title)
|
|
3256
3293
|
frontmatterLines.push(`title: ${updates.title}`);
|
|
@@ -3265,13 +3302,8 @@ function updateFrontmatter(content, updates) {
|
|
|
3265
3302
|
frontmatterLines.push('---');
|
|
3266
3303
|
return frontmatterLines.join('\n') + '\n' + content;
|
|
3267
3304
|
}
|
|
3268
|
-
|
|
3269
|
-
const
|
|
3270
|
-
if (endIndex === -1) {
|
|
3271
|
-
return content; // Invalid frontmatter, return unchanged
|
|
3272
|
-
}
|
|
3273
|
-
const frontmatter = content.substring(3, endIndex);
|
|
3274
|
-
const bodyContent = content.substring(endIndex + 3);
|
|
3305
|
+
const frontmatter = inner;
|
|
3306
|
+
const bodyContent = body;
|
|
3275
3307
|
const lines = frontmatter.split(/\r?\n/);
|
|
3276
3308
|
const newLines = [];
|
|
3277
3309
|
const updatedKeys = new Set();
|
|
@@ -3334,31 +3366,27 @@ function updateFrontmatter(content, updates) {
|
|
|
3334
3366
|
newLines.push(` - ${kw}`);
|
|
3335
3367
|
}
|
|
3336
3368
|
}
|
|
3337
|
-
return '---\n' + newLines.filter(l => l.trim() !== '').join('\n') + '\n
|
|
3369
|
+
return '---\n' + newLines.filter(l => l.trim() !== '').join('\n') + '\n---\n' + bodyContent;
|
|
3338
3370
|
}
|
|
3339
3371
|
/**
|
|
3340
3372
|
* Parse markdown content into structure: frontmatter, H1 heading, and body
|
|
3341
3373
|
*/
|
|
3342
|
-
function parseMarkdownStructure(content) {
|
|
3343
|
-
|
|
3344
|
-
|
|
3345
|
-
|
|
3346
|
-
|
|
3347
|
-
|
|
3348
|
-
|
|
3349
|
-
|
|
3350
|
-
|
|
3351
|
-
|
|
3352
|
-
}
|
|
3353
|
-
// Extract first H1 heading (# ...)
|
|
3354
|
-
const h1Match = remaining.match(/^(#\s+.+?)(\r?\n|$)/m);
|
|
3374
|
+
export function parseMarkdownStructure(content) {
|
|
3375
|
+
// Extract YAML frontmatter (line-anchored; see splitFrontmatter).
|
|
3376
|
+
const { raw, body: afterFrontmatter } = splitFrontmatter(content);
|
|
3377
|
+
const frontmatter = raw ? raw.replace(/\r?\n$/, '') : ''; // keep fences; reconstruct re-adds the trailing newline
|
|
3378
|
+
const remaining = afterFrontmatter;
|
|
3379
|
+
// Extract the note's H1 heading — ONLY when it is the first non-blank line of the
|
|
3380
|
+
// body. A non-anchored /m search would match a `# ...` line inside a fenced code
|
|
3381
|
+
// block (e.g. a shell comment) and set body to everything after it, silently
|
|
3382
|
+
// dropping the whole note above it. Anchoring to the start keeps all content.
|
|
3383
|
+
const h1Match = remaining.match(/^\s*(#[ \t]+.+?)(\r?\n|$)/);
|
|
3355
3384
|
let h1 = null;
|
|
3356
3385
|
let body = remaining;
|
|
3357
3386
|
if (h1Match) {
|
|
3358
3387
|
h1 = h1Match[1];
|
|
3359
|
-
//
|
|
3360
|
-
|
|
3361
|
-
body = remaining.substring(h1EndIndex);
|
|
3388
|
+
// Everything after the matched leading H1 line becomes the body.
|
|
3389
|
+
body = remaining.substring(h1Match[0].length);
|
|
3362
3390
|
}
|
|
3363
3391
|
return { frontmatter, h1, body };
|
|
3364
3392
|
}
|
|
@@ -3628,7 +3656,7 @@ async function runConflictCascade(opts) {
|
|
|
3628
3656
|
* Merge local content with cloud content
|
|
3629
3657
|
* Strategy: Cloud's H1 wins (Noesis edits), Local's body wins (local edits)
|
|
3630
3658
|
*/
|
|
3631
|
-
function mergeContent(localContent, cloudContent) {
|
|
3659
|
+
export function mergeContent(localContent, cloudContent) {
|
|
3632
3660
|
const localParsed = parseMarkdownStructure(localContent);
|
|
3633
3661
|
const cloudParsed = parseMarkdownStructure(cloudContent);
|
|
3634
3662
|
// Use cloud's H1 if it exists and differs from local, otherwise use local's H1
|