@noesis-brain/mcp-server 2.1.0 → 2.1.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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@noesis-brain/mcp-server",
3
- "version": "2.1.0",
3
+ "version": "2.1.1",
4
4
  "description": "Official MCP server for Noesis — the markdown notes manager for Claude Code. Search, sync, and manage your .md knowledge base via 19 AI-powered tools.",
5
5
  "type": "module",
6
6
  "main": "dist/index.js",
@@ -88,6 +88,13 @@ const stripAnsi = (s) => String(s == null ? '' : s).replace(ANSI_RE, '');
88
88
  const GIT_CRLF_WARN_RE = /warning: in the working copy of '[^']*', LF will be replaced by CRLF the next time Git touches it\.?\s*/g;
89
89
  const stripCmdNoise = (s) => String(s == null ? '' : s).replace(GIT_CRLF_WARN_RE, '');
90
90
 
91
+ // Some Windows console tools (`wsl --status`, certain `reg`/`netsh` invocations)
92
+ // write UTF-16LE to a piped stdout; captured as UTF-8 that leaves NUL bytes
93
+ // interleaved through the text. Postgres rejects NUL in text columns outright,
94
+ // and render() joins the whole note into one string before the cloud push, so
95
+ // a single stray NUL anywhere fails the ENTIRE push, not just that one entry.
96
+ const stripNulBytes = (s) => String(s == null ? '' : s).replace(/\u0000/g, '');
97
+
91
98
  // Drop Claude Code image placeholders ("[Image #1]", "[Image: source: <path>]")
92
99
  // that carry no readable signal in a text capture.
93
100
  const stripImageRefs = (s) =>
@@ -315,8 +322,8 @@ function frontmatter(meta) {
315
322
 
316
323
  function resultText(res) {
317
324
  const c = res?.content;
318
- if (typeof c === 'string') return stripCmdNoise(stripAnsi(c));
319
- if (Array.isArray(c)) return stripCmdNoise(stripAnsi(c.filter((b) => b && b.type === 'text').map((b) => b.text).join(' ')));
325
+ if (typeof c === 'string') return stripNulBytes(stripCmdNoise(stripAnsi(c)));
326
+ if (Array.isArray(c)) return stripNulBytes(stripCmdNoise(stripAnsi(c.filter((b) => b && b.type === 'text').map((b) => b.text).join(' '))));
320
327
  return '';
321
328
  }
322
329
 
@@ -330,6 +337,46 @@ function blockquote(text) {
330
337
  return text.split('\n').map((l) => (l.length ? `> ${l}` : '>')).join('\n');
331
338
  }
332
339
 
340
+ // Returns the exact CommonMark closing fence needed to balance `text` if it ends
341
+ // inside an open fenced code block (``` or ~~~), or '' if none is open. Mirrors
342
+ // the fence-parity rules in frontend/src/utils/codeFences.ts.
343
+ function unterminatedFenceCloser(text) {
344
+ let openChar = '';
345
+ let openLen = 0;
346
+ for (const raw of text.split('\n')) {
347
+ const trimmed = raw.trim();
348
+ const m = /^(`{3,}|~{3,})(.*)$/.exec(trimmed);
349
+ if (!m) continue;
350
+ const char = m[1][0];
351
+ const len = m[1].length;
352
+ const info = m[2].trim();
353
+ if (!openChar) {
354
+ openChar = char;
355
+ openLen = len;
356
+ } else if (char === openChar && len >= openLen && info === '') {
357
+ openChar = '';
358
+ openLen = 0;
359
+ }
360
+ }
361
+ return openChar ? openChar.repeat(openLen) : '';
362
+ }
363
+
364
+ // Truncates `text` to at most `maxLen` characters, closing any fenced code block
365
+ // left dangling by the cut. An unclosed ``` fence would otherwise mark the rest
366
+ // of the note as code (per CommonMark), silently swallowing every later heading
367
+ // from both the TOC panel and the rendered body until EOF.
368
+ //
369
+ // The fence check runs even when THIS function doesn't truncate: Claude Code's
370
+ // own transcript can already contain a tool result that IT truncated upstream
371
+ // (its own "… (truncated)" marker, mid-fence, before this script ever sees the
372
+ // text) -- observed in the wild at ~11968 chars, under this function's 12000
373
+ // cap. Closing only on our own cut would miss that case entirely.
374
+ function truncateSafely(text, maxLen) {
375
+ const cut = text.length > maxLen ? `${text.slice(0, maxLen)}\n\n… (truncated)` : text;
376
+ const closer = unterminatedFenceCloser(cut);
377
+ return closer ? `${cut}\n${closer}` : cut;
378
+ }
379
+
333
380
  function isSystemInjection(text) {
334
381
  const t = text.trimStart();
335
382
  return t.startsWith('<system-reminder') || t.startsWith('[SYSTEM NOTIFICATION') ||
@@ -415,7 +462,7 @@ function renderPlanBlock(toolUse, resultMap) {
415
462
  const body = txt
416
463
  .replace(/<\/details>/gi, '&lt;/details&gt;') // never let an inner tag close our wrapper
417
464
  .replace(/^(#{1,6})\s+(.*)$/gm, '**$2**'); // demote headings so they don't pollute the note outline
418
- const capped = body.length > 12000 ? `${body.slice(0, 12000)}\n\n… (truncated)` : body;
465
+ const capped = truncateSafely(body, 12000);
419
466
  lines.push('', '<details>', '<summary>Plan / approval</summary>', '', capped, '', '</details>', '');
420
467
  } else {
421
468
  lines.push(' - ↳ _(awaiting approval)_');
@@ -428,8 +475,7 @@ function renderUserPrompt(turn, text, entry, queued = false) {
428
475
  const imageOnly = !clean;
429
476
  const heading = imageOnly ? '(image)' : (firstWords(clean, 9) || '(empty)');
430
477
  const ts = tsStamp(entry.timestamp);
431
- const body = imageOnly ? '_(image attachment)_'
432
- : (clean.length > 3000 ? `${clean.slice(0, 3000)}\n\n… (truncated)` : clean);
478
+ const body = imageOnly ? '_(image attachment)_' : truncateSafely(clean, 3000);
433
479
  const parts = [`### ${turn}. ${heading}`];
434
480
  const meta = [ts, queued ? '_(queued while working)_' : ''].filter(Boolean).join(' · ');
435
481
  if (meta) parts.push(`*${meta}*`);