@cachly-dev/mcp-server 0.10.123 → 0.10.125

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 (85) hide show
  1. package/dist/index.d.ts +1 -0
  2. package/dist/index.js +7 -0
  3. package/dist/index.js.map +1 -0
  4. package/dist/src/aussetzer.d.ts +67 -0
  5. package/dist/src/aussetzer.js +82 -0
  6. package/dist/src/aussetzer.js.map +1 -0
  7. package/dist/src/autor-abzeichen.d.ts +41 -0
  8. package/dist/src/autor-abzeichen.js +56 -0
  9. package/dist/src/autor-abzeichen.js.map +1 -0
  10. package/dist/src/autor.d.ts +101 -0
  11. package/dist/src/autor.js +136 -0
  12. package/dist/src/autor.js.map +1 -0
  13. package/dist/src/bauabdruck-daten.generiert.json +5 -0
  14. package/dist/src/bauabdruck.d.ts +38 -0
  15. package/dist/src/bauabdruck.js +93 -0
  16. package/dist/src/bauabdruck.js.map +1 -0
  17. package/dist/src/bedeutung.d.ts +175 -0
  18. package/dist/src/bedeutung.js +254 -0
  19. package/dist/src/bedeutung.js.map +1 -0
  20. package/dist/src/brain-export.d.ts +24 -0
  21. package/dist/src/brain-export.js +31 -1
  22. package/dist/src/brain-export.js.map +1 -1
  23. package/dist/src/deutsch.d.ts +98 -0
  24. package/dist/src/deutsch.js +223 -0
  25. package/dist/src/deutsch.js.map +1 -0
  26. package/dist/src/eingaenge-nachruesten.d.ts +41 -0
  27. package/dist/src/eingaenge-nachruesten.js +290 -0
  28. package/dist/src/eingaenge-nachruesten.js.map +1 -0
  29. package/dist/src/eingaenge.d.ts +135 -0
  30. package/dist/src/eingaenge.js +228 -0
  31. package/dist/src/eingaenge.js.map +1 -0
  32. package/dist/src/geliefert.d.ts +63 -0
  33. package/dist/src/geliefert.js +114 -0
  34. package/dist/src/geliefert.js.map +1 -0
  35. package/dist/src/handlers/advanced.js +7 -3
  36. package/dist/src/handlers/advanced.js.map +1 -1
  37. package/dist/src/handlers/brain.d.ts +19 -0
  38. package/dist/src/handlers/brain.js +492 -48
  39. package/dist/src/handlers/brain.js.map +1 -1
  40. package/dist/src/handlers/instances.d.ts +24 -0
  41. package/dist/src/handlers/instances.js +49 -1
  42. package/dist/src/handlers/instances.js.map +1 -1
  43. package/dist/src/handlers/team.js +105 -0
  44. package/dist/src/handlers/team.js.map +1 -1
  45. package/dist/src/index.d.ts +20 -0
  46. package/dist/src/index.js +282 -36
  47. package/dist/src/index.js.map +1 -1
  48. package/dist/src/lesson-preview.d.ts +81 -0
  49. package/dist/src/lesson-preview.js +133 -0
  50. package/dist/src/lesson-preview.js.map +1 -0
  51. package/dist/src/rangfolge.d.ts +169 -0
  52. package/dist/src/rangfolge.js +214 -0
  53. package/dist/src/rangfolge.js.map +1 -0
  54. package/dist/src/raumkorrektur.d.ts +100 -0
  55. package/dist/src/raumkorrektur.js +197 -0
  56. package/dist/src/raumkorrektur.js.map +1 -0
  57. package/dist/src/recall-tiefe.d.ts +48 -0
  58. package/dist/src/recall-tiefe.js +42 -0
  59. package/dist/src/recall-tiefe.js.map +1 -0
  60. package/dist/src/rerank.d.ts +0 -14
  61. package/dist/src/rerank.js +39 -2
  62. package/dist/src/rerank.js.map +1 -1
  63. package/dist/src/search.d.ts +56 -17
  64. package/dist/src/search.js +335 -73
  65. package/dist/src/search.js.map +1 -1
  66. package/dist/src/seltenheitsbestand.d.ts +53 -0
  67. package/dist/src/seltenheitsbestand.js +92 -0
  68. package/dist/src/seltenheitsbestand.js.map +1 -0
  69. package/dist/src/spuren.d.ts +97 -0
  70. package/dist/src/spuren.js +150 -0
  71. package/dist/src/spuren.js.map +1 -0
  72. package/dist/src/tools.js +1 -1
  73. package/dist/src/tools.js.map +1 -1
  74. package/dist/src/vorspann.d.ts +73 -0
  75. package/dist/src/vorspann.js +146 -0
  76. package/dist/src/vorspann.js.map +1 -0
  77. package/dist/src/werkzeug-nutzung.d.ts +62 -0
  78. package/dist/src/werkzeug-nutzung.js +72 -0
  79. package/dist/src/werkzeug-nutzung.js.map +1 -0
  80. package/dist/src/wertbeitrag.d.ts +57 -0
  81. package/dist/src/wertbeitrag.js +73 -0
  82. package/dist/src/wertbeitrag.js.map +1 -0
  83. package/glama.json +1 -1
  84. package/package.json +12 -4
  85. package/server.json +2 -2
package/dist/src/index.js CHANGED
@@ -1,6 +1,8 @@
1
1
  #!/usr/bin/env node
2
2
  import { jwtExpiryMs, checkJwt, handleApiError, diagnoseAuth, planAuthHeal, readClientCredentialsFromEnv, buildClientCredentialsBody, clientCredentialsTokenUrl } from './auth.js';
3
3
  import { cachlyUrl } from './cachly-url.js';
4
+ import { nutzungsSchluessel } from './werkzeug-nutzung.js';
5
+ import { istLieferung, lieferSchluessel, tokenDerAntwort, hatTreffer, trefferSchluessel } from './geliefert.js';
4
6
  import { notify } from './notifier.js';
5
7
  import { fileURLToPath } from 'node:url';
6
8
  import { createRequire } from 'node:module';
@@ -47,7 +49,7 @@ const _isMain = process.argv[1] != null &&
47
49
  import { Server } from '@modelcontextprotocol/sdk/server/index.js';
48
50
  import { StdioServerTransport } from '@modelcontextprotocol/sdk/server/stdio.js';
49
51
  import { StreamableHTTPServerTransport } from '@modelcontextprotocol/sdk/server/streamableHttp.js';
50
- import { CallToolRequestSchema, ErrorCode, ListToolsRequestSchema, McpError, } from '@modelcontextprotocol/sdk/types.js';
52
+ import { CallToolRequestSchema, ErrorCode, ListPromptsRequestSchema, ListResourceTemplatesRequestSchema, ListResourcesRequestSchema, ListToolsRequestSchema, McpError, } from '@modelcontextprotocol/sdk/types.js';
51
53
  import { Redis } from 'ioredis';
52
54
  // ── Config ────────────────────────────────────────────────────────────────────
53
55
  // Mutable so the `setup`/`init` CLI can honor a `--api-url` flag for self-hosting.
@@ -593,6 +595,28 @@ export { tokenize, splitMultiQuery, levenshtein, recencyBoost, extractTimestamp,
593
595
  // ── Connection pool ───────────────────────────────────────────────────────────
594
596
  /** Reuse Redis connections across tool calls (keyed by instance_id). */
595
597
  const pool = new Map();
598
+ /**
599
+ * Reconnect exactly ONCE, then give up.
600
+ *
601
+ * The old policy was `retryStrategy: () => null` — fail fast, never reconnect.
602
+ * Measured cost of that policy (2026-08-16, one working session): the FIRST
603
+ * brain call after every idle stretch died with "Connection is closed", the
604
+ * retyped second call worked. Five times in one day, every time the same
605
+ * shape. Cause: the pooled socket goes half-open while idle (NAT/LB drops the
606
+ * TCP session silently); the next command writes into a dead pipe, and with
607
+ * reconnects disabled the error goes straight to the user instead of into one
608
+ * quick reconnect.
609
+ *
610
+ * One retry after 200 ms heals exactly that case. Anything beyond one attempt
611
+ * still fails fast — the "no reconnect loops in MCP context" concern the old
612
+ * policy protected stays protected: a genuinely down instance costs one extra
613
+ * 200 ms attempt, not a loop.
614
+ *
615
+ * Exported for the test that pins this behaviour down.
616
+ */
617
+ export function reconnectOnce(times) {
618
+ return times <= 1 ? 200 : null;
619
+ }
596
620
  async function getConnection(instance_id) {
597
621
  if (!instance_id) {
598
622
  // With auto-provisioning, this only happens when the API was briefly
@@ -680,7 +704,11 @@ async function getConnection(instance_id) {
680
704
  lazyConnect: true,
681
705
  enableReadyCheck: true,
682
706
  connectTimeout: 8000,
683
- retryStrategy: () => null, // fail fast, no reconnect loops in MCP context
707
+ retryStrategy: reconnectOnce,
708
+ // One in-flight command survives exactly one reconnect (see reconnectOnce).
709
+ maxRetriesPerRequest: 1,
710
+ // Detect half-open sockets while idle instead of at the next command.
711
+ keepAlive: 15000,
684
712
  });
685
713
  // Drop the client from the pool AND close its socket on error/end, so a
686
714
  // mid-session Redis drop doesn't leak sockets across reconnects. disconnect()
@@ -768,7 +796,7 @@ import { appendLedgerEntry, readLedger, defaultLedgerPath } from './ambient-ledg
768
796
  import { loadAmbientMemory, saveAmbientMemory } from './ambient-memory.js';
769
797
  import { buildAmbientDeps } from './ambient-deps.js';
770
798
  import { resolveApiKey, saveApiKey } from './credentials.js';
771
- import { buildLessonsMarkdown, buildLessonsJsonl } from './brain-export.js';
799
+ import { buildLessonsMarkdown, buildLessonsJsonl, vonRohLektion } from './brain-export.js';
772
800
  import { detectEditor as detectEditorImpl } from './editor.js';
773
801
  import { milestoneSent, markMilestoneSent } from './funnel-milestones.js';
774
802
  import { kostprobeUebrig, kostprobeVerbrauchen, kostprobeHinweis, schrankeNachKostproben, } from './kostprobe.js';
@@ -949,6 +977,49 @@ async function featureGate(name, instanceId) {
949
977
  }
950
978
  return { sperre: schrankeNachKostproben(name, pitch, UPGRADE_URL_FG) };
951
979
  }
980
+ /**
981
+ * Einen Werkzeug-Aufruf zaehlen (GROW-046).
982
+ *
983
+ * Ein Hash je Brain, ein Feld je Werkzeugname. Gelesen wird das in
984
+ * `brain_metrics` — bewusst KEIN eigenes Werkzeug dafuer: ein 123. Werkzeug,
985
+ * um zu messen, dass es zu viele Werkzeuge gibt, waere die Pointe, die
986
+ * niemand will.
987
+ *
988
+ * Jeder Fehler wird geschluckt. Eine Messung, die den gemessenen Vorgang
989
+ * stoert, misst am Ende sich selbst.
990
+ */
991
+ async function zaehleWerkzeug(instanceId, name) {
992
+ try {
993
+ const redis = await getConnection(instanceId);
994
+ await redis.hincrby(nutzungsSchluessel(instanceId), name, 1);
995
+ }
996
+ catch {
997
+ // still — siehe oben
998
+ }
999
+ }
1000
+ /**
1001
+ * Wie viel kam aus dem Gedaechtnis? — gezaehlt an der EINEN Stelle, durch die
1002
+ * jede Antwort geht.
1003
+ *
1004
+ * Drei Zahlen, alle drei Summen: gelieferte Token, Abrufe, Abrufe mit Treffer.
1005
+ * Keine davon braucht eine Annahme. Fire-and-forget wie die Werkzeugzaehlung —
1006
+ * eine Statistik darf eine Antwort nie verzoegern und nie scheitern lassen.
1007
+ */
1008
+ async function zaehleLieferung(instanceId, name, antwort) {
1009
+ if (!istLieferung(name))
1010
+ return;
1011
+ try {
1012
+ const redis = await getConnection(instanceId);
1013
+ const treffer = hatTreffer(antwort);
1014
+ await redis.incrby(lieferSchluessel(instanceId), treffer ? tokenDerAntwort(antwort) : 0);
1015
+ await redis.hincrby(trefferSchluessel(instanceId), 'abrufe', 1);
1016
+ if (treffer)
1017
+ await redis.hincrby(trefferSchluessel(instanceId), 'treffer', 1);
1018
+ }
1019
+ catch {
1020
+ // still — siehe oben
1021
+ }
1022
+ }
952
1023
  async function handleTool(name, args) {
953
1024
  // Guard: if no JWT, return actionable onboarding message instead of HTTP 401
954
1025
  if (!JWT) {
@@ -1034,9 +1105,32 @@ async function handleTool(name, args) {
1034
1105
  kostprobeHinweisText = gate.kostprobe;
1035
1106
  sendFunnelEvent('premium_taste_used', { tool: name, instance_id: args.instance_id ?? '' });
1036
1107
  }
1108
+ /*
1109
+ * GROW-046: Erst messen, dann zusammenlegen.
1110
+ *
1111
+ * Die Glama-Bewertung sagt, cachly habe zu viele Werkzeuge. Der Einwand ist
1112
+ * plausibel — nur konnte ihm niemand mit Zahlen begegnen, weil nirgends
1113
+ * gezaehlt wurde, welches Werkzeug wie oft laeuft. Ohne diese Zahl streicht
1114
+ * man das, was man SELBST selten benutzt, und das ist selten dasselbe wie
1115
+ * das, was die Nutzer selten benutzen.
1116
+ *
1117
+ * Gezaehlt wird NACH dem Tarif-Gate: ein abgewiesener Aufruf ist kein
1118
+ * Gebrauch, sondern ein verhinderter. Wer beides zusammenzaehlt, haelt
1119
+ * gesperrte Werkzeuge faelschlich fuer beliebt.
1120
+ *
1121
+ * Fire-and-forget: eine Zaehlung darf einen Aufruf nie verzoegern und nie
1122
+ * scheitern lassen.
1123
+ */
1124
+ if (typeof args.instance_id === 'string' && args.instance_id) {
1125
+ void zaehleWerkzeug(args.instance_id, name);
1126
+ }
1037
1127
  // Delegate brain tools (learn, recall, session, etc.)
1038
1128
  const brainResult = await handleBrainTool(name, args, getConnection, apiFetch);
1039
1129
  if (brainResult !== null) {
1130
+ // Erst hier steht die Antwort fest — vorher gibt es nichts zu zaehlen.
1131
+ if (typeof args.instance_id === 'string' && args.instance_id) {
1132
+ void zaehleLieferung(args.instance_id, name, String(brainResult ?? ''));
1133
+ }
1040
1134
  if (!_firstCallSuccessSent && JWT) {
1041
1135
  _firstCallSuccessSent = true;
1042
1136
  // Persistent guard: ambient hooks spawn a fresh process per prompt, so the
@@ -1351,8 +1445,90 @@ const CACHLY_MCP_INSTRUCTIONS = `This project has a Cachly AI Brain — persiste
1351
1445
  - BEFORE a risky op (deploy, migration, dependency bump): call brain_predict.
1352
1446
 
1353
1447
  The Brain instance id comes from the CACHLY_BRAIN_INSTANCE_ID environment variable.`;
1354
- const server = new Server({ name: 'cachly-mcp', version: CURRENT_VERSION }, { capabilities: { tools: {} }, instructions: CACHLY_MCP_INSTRUCTIONS });
1355
- server.setRequestHandler(ListToolsRequestSchema, async () => ({ tools: TOOLS }));
1448
+ /**
1449
+ * Baut einen frischen MCP-Server mit allen Handlern.
1450
+ *
1451
+ * ─── WARUM DAS EINE FABRIK IST UND KEINE KONSTANTE ──────────────────────────
1452
+ *
1453
+ * Im HTTP-Betrieb braucht JEDE Anfrage ihren eigenen Server samt Transport.
1454
+ * Bis zum 17.08.2026 gab es genau einen, beim Start erzeugt und fuer alles
1455
+ * wiederverwendet — und ein MCP-Server laesst sich nur EINMAL initialisieren.
1456
+ *
1457
+ * Die Folge war unauffaellig und teuer: Der erste initialize nach dem Start
1458
+ * antwortete mit 200, jeder weitere mit 500 und LEEREM Rumpf. Nachgestellt:
1459
+ *
1460
+ * Aufruf 1: HTTP 200
1461
+ * Aufruf 2: HTTP 500
1462
+ * Aufruf 3: HTTP 500
1463
+ *
1464
+ * Nach jedem Ausrollen war der Endpunkt also fuer genau eine Anfrage gesund.
1465
+ * Glama und Smithery haben ihren einen Treffer gelandet und die Antwort
1466
+ * eingefroren — beide zeigen bis heute 80 Werkzeuge, obwohl es 122 sind. Kein
1467
+ * Log, keine Fehlermeldung, kein roter Alarm. Der Endpunkt galt als "up",
1468
+ * weil /health antwortete.
1469
+ *
1470
+ * Der Aufbau ist billig: ein "new Server" und zwei Handler. TOOLS ist eine
1471
+ * Konstante des Moduls, es wird nichts kopiert.
1472
+ */
1473
+ /**
1474
+ * Was dieser Server kann. Werkzeuge sind der Kern; Ressourcen und Prompts
1475
+ * fuehren wir mit LEEREN Listen.
1476
+ *
1477
+ * ─── WARUM LEER UND NICHT WEGGELASSEN (18.08.2026) ──────────────────────────
1478
+ *
1479
+ * Genau genommen war das Weglassen richtig: Wer eine Faehigkeit nicht
1480
+ * ankuendigt, darf auf `resources/list` mit -32601 "Method not found"
1481
+ * antworten, und das ist die vorgesehene Antwort.
1482
+ *
1483
+ * Nur liest sie niemand so. Im Scan-Protokoll von Smithery stand:
1484
+ *
1485
+ * [scan] Capabilities found: 122 tools. Warning: Failed to list resources:
1486
+ * MCP error -32601: Method not found; Failed to list prompts: ...
1487
+ *
1488
+ * Ein Pruefer, der "Failed" protokolliert, faellt nicht auf die Feinheit
1489
+ * herein, dass unser Nein das korrekte Nein war. Aus einer sauberen Antwort
1490
+ * wird eine Warnzeile im Bericht — und Warnzeilen kosten Noten in den
1491
+ * Verzeichnissen, ohne dass irgendwo etwas kaputt ist.
1492
+ *
1493
+ * Eine leere Liste sagt dasselbe wie der Fehler ("hier gibt es nichts"), aber
1494
+ * in einer Form, die jeder Aufrufer richtig versteht. Das ist keine
1495
+ * Beschoenigung: Wir behaupten nicht, Ressourcen zu haben, wir liefern
1496
+ * nachpruefbar null Stueck.
1497
+ */
1498
+ const CAPABILITIES = { tools: {}, resources: {}, prompts: {} };
1499
+ const listToolsHandler = async () => ({ tools: TOOLS });
1500
+ // Bewusst Funktionsdeklarationen statt const: makeServer() steht weiter oben
1501
+ // als callToolHandler, und Deklarationen werden hochgezogen. So gibt es EINE
1502
+ // Stelle je Handler, nicht eine pro Server-Aufbau.
1503
+ async function listResourcesHandler() {
1504
+ return { resources: [] };
1505
+ }
1506
+ async function listResourceTemplatesHandler() {
1507
+ return { resourceTemplates: [] };
1508
+ }
1509
+ async function listPromptsHandler() {
1510
+ return { prompts: [] };
1511
+ }
1512
+ /** Haengt alle Handler an einen Server. Beide Betriebsarten teilen sich diese
1513
+ * eine Liste — sonst kennt der HTTP-Server bald etwas, das der stdio-Server
1514
+ * nicht kennt, und niemand merkt es. */
1515
+ function registriereHandler(s) {
1516
+ s.setRequestHandler(ListToolsRequestSchema, listToolsHandler);
1517
+ s.setRequestHandler(CallToolRequestSchema, callToolHandler);
1518
+ s.setRequestHandler(ListResourcesRequestSchema, listResourcesHandler);
1519
+ s.setRequestHandler(ListResourceTemplatesRequestSchema, listResourceTemplatesHandler);
1520
+ s.setRequestHandler(ListPromptsRequestSchema, listPromptsHandler);
1521
+ }
1522
+ function makeServer() {
1523
+ const s = new Server({ name: 'cachly-mcp', version: CURRENT_VERSION }, { capabilities: CAPABILITIES, instructions: CACHLY_MCP_INSTRUCTIONS });
1524
+ registriereHandler(s);
1525
+ return s;
1526
+ }
1527
+ const server = new Server({ name: 'cachly-mcp', version: CURRENT_VERSION }, { capabilities: CAPABILITIES, instructions: CACHLY_MCP_INSTRUCTIONS });
1528
+ server.setRequestHandler(ListToolsRequestSchema, listToolsHandler);
1529
+ server.setRequestHandler(ListResourcesRequestSchema, listResourcesHandler);
1530
+ server.setRequestHandler(ListResourceTemplatesRequestSchema, listResourceTemplatesHandler);
1531
+ server.setRequestHandler(ListPromptsRequestSchema, listPromptsHandler);
1356
1532
  // ── Auto-session management ───────────────────────────────────────────────────
1357
1533
  // Transparently starts a Brain session on the first tool call of a process
1358
1534
  // and saves a session_end summary on SIGTERM/exit so users never have to call
@@ -1423,7 +1599,7 @@ async function autoEndSession() {
1423
1599
  }
1424
1600
  catch { /* non-fatal */ }
1425
1601
  }
1426
- server.setRequestHandler(CallToolRequestSchema, async (request) => {
1602
+ const callToolHandler = async (request) => {
1427
1603
  const { name, arguments: args } = request.params;
1428
1604
  // Auto-start brain session on first tool call that has an instance_id.
1429
1605
  // Skip session management tools to avoid recursion.
@@ -1456,7 +1632,8 @@ server.setRequestHandler(CallToolRequestSchema, async (request) => {
1456
1632
  notify('cachly', 'tool_error', { tool: name, error: msg }).catch(() => undefined);
1457
1633
  throw new McpError(ErrorCode.InternalError, msg);
1458
1634
  }
1459
- });
1635
+ };
1636
+ server.setRequestHandler(CallToolRequestSchema, callToolHandler);
1460
1637
  // Graceful shutdown – close all Redis connections + auto-end brain session
1461
1638
  process.on('SIGTERM', async () => {
1462
1639
  await autoEndSession().catch(() => undefined);
@@ -1766,7 +1943,7 @@ if (process.argv[2] === 'digest') {
1766
1943
  process.stdout.write('\n🧠 Fetching your weekly Brain digest...\n\n');
1767
1944
  try {
1768
1945
  // Fetch current stats
1769
- const statsRes = await fetch(`${API_URL}/api/v1/instances/${instanceId}/brain-stats`, {
1946
+ const statsRes = await fetch(`${API_URL}/api/v1/instances/${instanceId}/memory`, {
1770
1947
  headers: { Authorization: `Bearer ${apiKey}` },
1771
1948
  signal: AbortSignal.timeout(8000),
1772
1949
  });
@@ -1775,7 +1952,7 @@ if (process.argv[2] === 'digest') {
1775
1952
  const stats = await statsRes.json();
1776
1953
  const lessons = stats.lesson_count ?? 0;
1777
1954
  const recalls = stats.total_recall_count ?? 0;
1778
- const score = Math.round((stats.quality_score ?? 0) * 100);
1955
+ const score = Math.round(stats.iq_boost_pct ?? 0);
1779
1956
  const topLessons = (stats.top_lessons ?? []).slice(0, 5);
1780
1957
  const teamAuthors = stats.team_authors ?? [];
1781
1958
  const tokensSaved = recalls * 1200;
@@ -1865,23 +2042,60 @@ if (process.argv[2] === 'export') {
1865
2042
  }
1866
2043
  process.stdout.write('\n📤 Exporting your Brain...\n\n');
1867
2044
  try {
1868
- const memRes = await fetch(`${API_URL}/api/v1/instances/${instanceId}/memory`, {
2045
+ // ── Warum /export und nicht /memory ──────────────────────────────────────
2046
+ //
2047
+ // Bis zum 19.08.2026 stand hier /memory. Der Endpunkt ist die
2048
+ // Dashboard-Zusammenfassung: oberste 50 Lektionen, what_worked auf 120
2049
+ // Zeichen gekuerzt. Gemessen am 19.08.2026 an einem Brain mit 493
2050
+ // Lektionen: der Export lieferte 50 Stueck mit mitten im Wort
2051
+ // abgeschnittenen Saetzen — 10 Prozent, unlesbar.
2052
+ //
2053
+ // Bei einem Produkt, das "your memory, not the vendor's" verspricht, ist
2054
+ // das kein Schoenheitsfehler. /export liefert alles, ungekuerzt.
2055
+ let lessons = [];
2056
+ let unvollstaendig = '';
2057
+ const expRes = await fetch(`${API_URL}/api/v1/instances/${instanceId}/export`, {
1869
2058
  headers: { Authorization: `Bearer ${apiKey}` },
1870
- signal: AbortSignal.timeout(8000),
2059
+ signal: AbortSignal.timeout(120000), // alles lesen dauert laenger als eine Zusammenfassung
1871
2060
  });
1872
- if (!memRes.ok)
1873
- throw new Error(`memory HTTP ${memRes.status}`);
1874
- const mem = await memRes.json();
1875
- const lessons = (mem.top_lessons ?? []).map((l) => ({
1876
- topic: l.topic,
1877
- outcome: l.outcome,
1878
- severity: l.severity,
1879
- tags: l.tags,
1880
- whatWorked: l.what_worked,
1881
- ts: l.ts,
1882
- author: l.author,
1883
- recallCount: l.recall_count,
1884
- }));
2061
+ if (expRes.ok) {
2062
+ const dump = await expRes.json();
2063
+ lessons = (dump.lessons ?? [])
2064
+ .map(vonRohLektion)
2065
+ .filter((l) => l !== null);
2066
+ // Der Server sagt selbst, ob er alles hatte. Diese Antwort wird
2067
+ // weitergereicht statt geschluckt — ein abgeschnittener Export, der wie
2068
+ // ein vollstaendiger aussieht, ist schlimmer als ein Fehler.
2069
+ if (dump.complete === false)
2070
+ unvollstaendig = dump.note ?? 'the server reported an incomplete read';
2071
+ }
2072
+ else if (expRes.status === 404) {
2073
+ // Aeltere Server kennen /export noch nicht. Dann gibt es die
2074
+ // Zusammenfassung — aber der Nutzer erfaehrt, dass er sie bekommt.
2075
+ const memRes = await fetch(`${API_URL}/api/v1/instances/${instanceId}/memory`, {
2076
+ headers: { Authorization: `Bearer ${apiKey}` },
2077
+ signal: AbortSignal.timeout(8000),
2078
+ });
2079
+ if (!memRes.ok)
2080
+ throw new Error(`memory HTTP ${memRes.status}`);
2081
+ const mem = await memRes.json();
2082
+ lessons = (mem.top_lessons ?? []).map((l) => ({
2083
+ topic: l.topic,
2084
+ outcome: l.outcome,
2085
+ severity: l.severity,
2086
+ tags: l.tags,
2087
+ whatWorked: l.what_worked,
2088
+ ts: l.ts,
2089
+ author: l.author,
2090
+ recallCount: l.recall_count,
2091
+ }));
2092
+ unvollstaendig =
2093
+ 'this server has no /export endpoint yet, so this is the DASHBOARD SUMMARY: ' +
2094
+ 'at most 50 lessons, each shortened to 120 characters. Update the server for a full export.';
2095
+ }
2096
+ else {
2097
+ throw new Error(`export HTTP ${expRes.status}`);
2098
+ }
1885
2099
  if (lessons.length === 0) {
1886
2100
  console.log(' No lessons in this Brain yet — nothing to export.\n');
1887
2101
  process.exit(0);
@@ -1898,7 +2112,14 @@ if (process.argv[2] === 'export') {
1898
2112
  await writeFile(jsonlPath, buildLessonsJsonl(lessons), 'utf-8');
1899
2113
  written.push(`${lessons.length} lesson${lessons.length === 1 ? '' : 's'} → ${jsonlPath}`);
1900
2114
  }
1901
- console.log('✅ Export complete:');
2115
+ if (unvollstaendig) {
2116
+ console.log('⚠️ Export INCOMPLETE — this is not your whole Brain:');
2117
+ console.log(` ${unvollstaendig}`);
2118
+ console.log('');
2119
+ }
2120
+ else {
2121
+ console.log('✅ Export complete:');
2122
+ }
1902
2123
  for (const line of written)
1903
2124
  console.log(` ${line}`);
1904
2125
  console.log('');
@@ -2432,7 +2653,7 @@ if (process.argv[2] === 'share') {
2432
2653
  }
2433
2654
  process.stdout.write('\n🧠 Fetching your Brain stats...\n');
2434
2655
  try {
2435
- const statsRes = await fetch(`${API_URL}/api/v1/instances/${instanceId}/brain-stats`, {
2656
+ const statsRes = await fetch(`${API_URL}/api/v1/instances/${instanceId}/memory`, {
2436
2657
  headers: { Authorization: `Bearer ${apiKey}` },
2437
2658
  signal: AbortSignal.timeout(8000),
2438
2659
  });
@@ -2441,7 +2662,7 @@ if (process.argv[2] === 'share') {
2441
2662
  const stats = await statsRes.json();
2442
2663
  const lessons = stats.lesson_count ?? 0;
2443
2664
  const recalls = stats.total_recall_count ?? 0;
2444
- const score = Math.round((stats.quality_score ?? 0) * 100);
2665
+ const score = Math.round(stats.iq_boost_pct ?? 0);
2445
2666
  const tokensSaved = recalls * 1200;
2446
2667
  const costSaved = (tokensSaved * 0.000003).toFixed(2);
2447
2668
  const level = lessons === 0 ? 'Intern 🌱' :
@@ -2498,7 +2719,7 @@ if (process.argv[2] === 'publish') {
2498
2719
  process.stdout.write('\n🧠 Publishing Brain snapshot...\n');
2499
2720
  try {
2500
2721
  // Fetch stats for the card
2501
- const statsRes = await fetch(`${API_URL}/api/v1/instances/${instanceId}/brain-stats`, {
2722
+ const statsRes = await fetch(`${API_URL}/api/v1/instances/${instanceId}/memory`, {
2502
2723
  headers: { Authorization: `Bearer ${apiKey}` },
2503
2724
  signal: AbortSignal.timeout(8000),
2504
2725
  });
@@ -2765,7 +2986,7 @@ if (process.argv[2] === 'status') {
2765
2986
  }
2766
2987
  try {
2767
2988
  const [statsRes, referralRes] = await Promise.all([
2768
- fetch(`${API_URL}/api/v1/instances/${instanceId}/brain-stats`, {
2989
+ fetch(`${API_URL}/api/v1/instances/${instanceId}/memory`, {
2769
2990
  headers: { Authorization: `Bearer ${apiKey}` },
2770
2991
  signal: AbortSignal.timeout(8000),
2771
2992
  }),
@@ -2782,7 +3003,7 @@ if (process.argv[2] === 'status') {
2782
3003
  : null;
2783
3004
  const lessons = stats.lesson_count ?? 0;
2784
3005
  const recalls = stats.total_recall_count ?? 0;
2785
- const score = Math.round((stats.quality_score ?? 0) * 100);
3006
+ const score = Math.round(stats.iq_boost_pct ?? 0);
2786
3007
  const team = stats.team_authors ?? [];
2787
3008
  const refCount = referral?.referral_count ?? 0;
2788
3009
  const refUrl = referral?.url ?? '';
@@ -3500,7 +3721,7 @@ if (process.argv[2] === 'autosetup' || process.argv[2] === 'setup' || _isAutopil
3500
3721
  // Fetch brain health from the API to show the user what their agent will see.
3501
3722
  process.stdout.write('\n⏳ Fetching your Brain health preview...');
3502
3723
  try {
3503
- const brainRes = await fetch(`${API_URL}/api/v1/instances/${instance.id}/brain-stats`, {
3724
+ const brainRes = await fetch(`${API_URL}/api/v1/instances/${instance.id}/memory`, {
3504
3725
  headers: { Authorization: `Bearer ${token}` },
3505
3726
  signal: AbortSignal.timeout(6000),
3506
3727
  });
@@ -3508,7 +3729,7 @@ if (process.argv[2] === 'autosetup' || process.argv[2] === 'setup' || _isAutopil
3508
3729
  const brainData = await brainRes.json();
3509
3730
  const lessons = brainData.lesson_count ?? 0;
3510
3731
  const contexts = brainData.context_count ?? 0;
3511
- const score = brainData.quality_score ?? 0;
3732
+ const score = Math.round(brainData.iq_boost_pct ?? 0);
3512
3733
  const level = lessons === 0 ? 'Intern 🌱' :
3513
3734
  lessons < 10 ? 'Junior Dev 🔧' :
3514
3735
  lessons < 30 ? 'Mid Dev ⚡' :
@@ -3519,7 +3740,7 @@ if (process.argv[2] === 'autosetup' || process.argv[2] === 'setup' || _isAutopil
3519
3740
  console.log(`│ 🧠 Brain Health Report │`);
3520
3741
  console.log('├──────────────────────────────────────────────────────┤');
3521
3742
  console.log(`│ Lessons stored : ${String(lessons).padEnd(6)} │ Level: ${level.padEnd(20)}│`);
3522
- console.log(`│ Context entries : ${String(contexts).padEnd(6)} │ Quality score: ${String(Math.round(score * 100)).padEnd(3)}% │`);
3743
+ console.log(`│ Context entries : ${String(contexts).padEnd(6)} │ Quality score: ${String(score).padEnd(3)}% │`);
3523
3744
  if (lessons === 0) {
3524
3745
  console.log('├──────────────────────────────────────────────────────┤');
3525
3746
  console.log('│ Your Brain is empty and ready to learn. │');
@@ -4045,8 +4266,6 @@ if (httpPort) {
4045
4266
  // ── HTTP mode (Streamable HTTP transport) ───────────────────────────────
4046
4267
  // Used for Smithery URL deployment: PORT=3000 node dist/src/index.js
4047
4268
  const { createServer } = await import('node:http');
4048
- const transport = new StreamableHTTPServerTransport({ sessionIdGenerator: undefined });
4049
- await server.connect(transport);
4050
4269
  const httpServer = createServer((req, res) => {
4051
4270
  const url = new URL(req.url ?? '/', `http://localhost:${httpPort}`);
4052
4271
  if (url.pathname === '/health') {
@@ -4062,7 +4281,34 @@ if (httpPort) {
4062
4281
  return;
4063
4282
  }
4064
4283
  if (url.pathname === '/mcp' || url.pathname === '/') {
4065
- transport.handleRequest(req, res);
4284
+ // JEDE Anfrage bekommt einen eigenen Server samt Transport.
4285
+ //
4286
+ // Vorher gab es genau einen, beim Start erzeugt. Ein MCP-Server laesst
4287
+ // sich aber nur EINMAL initialisieren — der erste Aufruf nach dem Start
4288
+ // ging durch, jeder weitere endete mit 500 und leerem Rumpf. Siehe die
4289
+ // Begruendung bei makeServer().
4290
+ void (async () => {
4291
+ const s = makeServer();
4292
+ const t = new StreamableHTTPServerTransport({ sessionIdGenerator: undefined });
4293
+ // Aufraeumen, sobald die Antwort durch ist — sonst haelt jede Anfrage
4294
+ // ihren Server fest und der Speicher waechst mit jedem Aufruf.
4295
+ res.on('close', () => {
4296
+ t.close().catch(() => undefined);
4297
+ s.close().catch(() => undefined);
4298
+ });
4299
+ try {
4300
+ await s.connect(t);
4301
+ await t.handleRequest(req, res);
4302
+ }
4303
+ catch (err) {
4304
+ // Ein Fehler ohne Text war genau das Problem, das hier behoben wird.
4305
+ process.stderr.write(`cachly-mcp HTTP: ${err.message}\n`);
4306
+ if (!res.headersSent) {
4307
+ res.writeHead(500, { 'Content-Type': 'application/json' });
4308
+ res.end(JSON.stringify({ error: err.message }));
4309
+ }
4310
+ }
4311
+ })();
4066
4312
  return;
4067
4313
  }
4068
4314
  res.writeHead(404);