@mnemoverse/mcp-memory-server 0.12.1 → 0.13.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/README.md +12 -5
- package/dist/limits.d.ts +17 -0
- package/dist/limits.js +17 -0
- package/dist/limits.js.map +1 -1
- package/dist/requests.d.ts +22 -0
- package/dist/requests.js +23 -0
- package/dist/requests.js.map +1 -1
- package/dist/teaching.d.ts +15 -1
- package/dist/teaching.js +15 -1
- package/dist/teaching.js.map +1 -1
- package/dist/tools.d.ts +2 -2
- package/dist/tools.js +360 -89
- package/dist/tools.js.map +1 -1
- package/package.json +7 -3
- package/tools.json +140 -0
package/dist/tools.js
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* The ten memory tools, as one function any MCP server can register.
|
|
2
|
+
* The eleven tools (ten memory tools and vault_list), as one function any MCP server can register.
|
|
3
3
|
*
|
|
4
4
|
* ADR-025 (mnemoverse-core): this package defines the MCP surface, and every
|
|
5
5
|
* server that exposes Mnemoverse memory over MCP registers the SAME tools from
|
|
@@ -12,14 +12,14 @@
|
|
|
12
12
|
* or the environment.
|
|
13
13
|
*/
|
|
14
14
|
import { z } from "zod";
|
|
15
|
-
import { CURSOR_RE, formatReadItem, formatRecentPage, rawAuthorName, safeInline, structuredItem, } from "./render.js";
|
|
15
|
+
import { CURSOR_RE, formatDateTag, formatReadItem, formatRecentPage, rawAuthorName, safeInline, structuredItem, } from "./render.js";
|
|
16
16
|
// `NO_MATCH_MESSAGE` is no longer imported here: this file used to pick between
|
|
17
17
|
// it and buildReadEmptyResponse by testing a note string for truthiness, which
|
|
18
18
|
// is how "we could not check" came to be spelled like "the store is there".
|
|
19
19
|
// Assembling the whole answer belongs in one place — src/teaching.ts.
|
|
20
20
|
import { buildReadEmptyResponse } from "./teaching.js";
|
|
21
21
|
import { classifyRooms, futureSinceNote, probeReadScope, readScopeNote, scopeLabel, } from "./scope.js";
|
|
22
|
-
import { readRequestBody, recentRequestBody, writeRequestBody, searchedScope, } from "./requests.js";
|
|
22
|
+
import { graphRequestBody, readRequestBody, recentRequestBody, writeRequestBody, searchedScope, } from "./requests.js";
|
|
23
23
|
import { exactLiteral, formatDomainList, MAX_DOMAIN_LITERAL, MAX_DOMAIN_TAG_LITERAL, roomNamePhrase, structuredText, withDomainEscapeLegend, withEscapeLegendAt, } from "./names.js";
|
|
24
24
|
import { ApiError, rewordFailure } from "./errors.js";
|
|
25
25
|
// Field limits, generated from core's contract (src/limits.ts, ADR-025).
|
|
@@ -285,7 +285,9 @@ const PROBE_TIMEOUT_MS = 4000;
|
|
|
285
285
|
* (test/read-structured.test.ts pins it unchanged).
|
|
286
286
|
*/
|
|
287
287
|
const MEMORY_ITEM_OUTPUT = {
|
|
288
|
-
memory_id: z
|
|
288
|
+
memory_id: z
|
|
289
|
+
.string()
|
|
290
|
+
.describe("Identifier needed to rate or manage this saved memory."),
|
|
289
291
|
content: z.string().describe("Stored memory content."),
|
|
290
292
|
domain: z.string().describe("User-defined memory namespace or domain."),
|
|
291
293
|
created_at: z
|
|
@@ -298,14 +300,16 @@ const MEMORY_ITEM_OUTPUT = {
|
|
|
298
300
|
.describe("Sanitized AGENT identity of the writer (never the human principal) — attribution in shared rooms."),
|
|
299
301
|
};
|
|
300
302
|
/**
|
|
301
|
-
* Register the
|
|
303
|
+
* Register the eleven tools on `server`. Call once per server instance.
|
|
302
304
|
* `deps.apiFetch` is the only way the tools reach the API.
|
|
303
305
|
*/
|
|
304
306
|
export function registerMemoryTools(server, deps) {
|
|
305
307
|
const { wording, writeAuthor } = deps;
|
|
306
308
|
// STEP4-2: `wording` reaches the error text here, on every rejection, not
|
|
307
309
|
// in the consumer's constructors. See wordedApiFetch.
|
|
308
|
-
const apiFetch = wording === undefined
|
|
310
|
+
const apiFetch = wording === undefined
|
|
311
|
+
? deps.apiFetch
|
|
312
|
+
: wordedApiFetch(deps.apiFetch, wording);
|
|
309
313
|
// Read once, defensively: `wording` crosses a public package boundary a
|
|
310
314
|
// caller controls only at compile time (STEP4-2, owner 2026-09-24). A
|
|
311
315
|
// strict-equality check rather than a truthiness check, so any value other
|
|
@@ -320,10 +324,10 @@ export function registerMemoryTools(server, deps) {
|
|
|
320
324
|
// ANNOTATIONS, decided once for every server that registers these tools
|
|
321
325
|
// (owner, 2026-09-21; the stdio server and the hosted connector had answered
|
|
322
326
|
// both opposite ways):
|
|
323
|
-
// - openWorldHint is false on all
|
|
327
|
+
// - openWorldHint is false on all eleven. Every tool works on the user's own
|
|
324
328
|
// memory store and reaches nothing else; that the store sits behind an API
|
|
325
329
|
// does not make it an open world.
|
|
326
|
-
// - destructiveHint is true only for a tool that deletes. None of these
|
|
330
|
+
// - destructiveHint is true only for a tool that deletes. None of these eleven
|
|
327
331
|
// does. Rating a memory moves its ranking signals and never alters or
|
|
328
332
|
// erases what was saved (see memory_feedback).
|
|
329
333
|
/**
|
|
@@ -349,7 +353,11 @@ export function registerMemoryTools(server, deps) {
|
|
|
349
353
|
* wire, so the diagnosis can never describe a different store than the request.
|
|
350
354
|
*/
|
|
351
355
|
function probeScope(searched) {
|
|
352
|
-
return probeReadScope(searched, () => apiFetch("/memory/stats", {
|
|
356
|
+
return probeReadScope(searched, () => apiFetch("/memory/stats", {
|
|
357
|
+
signal: AbortSignal.timeout(PROBE_TIMEOUT_MS),
|
|
358
|
+
}), () => apiFetch("/memory/rooms", {
|
|
359
|
+
signal: AbortSignal.timeout(PROBE_TIMEOUT_MS),
|
|
360
|
+
}), safeInline);
|
|
353
361
|
}
|
|
354
362
|
// --- Tool: memory_write ---
|
|
355
363
|
server.registerTool("memory_write", {
|
|
@@ -402,7 +410,7 @@ export function registerMemoryTools(server, deps) {
|
|
|
402
410
|
reason: z
|
|
403
411
|
.string()
|
|
404
412
|
.optional()
|
|
405
|
-
.describe(
|
|
413
|
+
.describe('The memory service\'s own explanation of this outcome, quoted as sent — when stored is false this is the ONLY statement of WHY, e.g. "Below importance threshold (0.047 < 0.1)". Ordinary text is preserved exactly; only control, bidi, zero-width, and repeated-whitespace characters are normalized before display, and the value is capped at 400 characters. Absent when the service sent no explanation, or when nothing remains after that normalization.'),
|
|
406
414
|
importance: z
|
|
407
415
|
.number()
|
|
408
416
|
.optional()
|
|
@@ -453,7 +461,9 @@ export function registerMemoryTools(server, deps) {
|
|
|
453
461
|
// {@link WriteAuthor}), and this package cannot know the sixth field a
|
|
454
462
|
// future core release adds any better than core's own validator does.
|
|
455
463
|
const authorRaw = writeAuthor?.();
|
|
456
|
-
const author = typeof authorRaw === "object" && authorRaw !== null
|
|
464
|
+
const author = typeof authorRaw === "object" && authorRaw !== null
|
|
465
|
+
? authorRaw
|
|
466
|
+
: undefined;
|
|
457
467
|
const r = await apiFetch("/memory/write", {
|
|
458
468
|
method: "POST",
|
|
459
469
|
body: JSON.stringify(writeRequestBody({ content, concepts, domain }, author)),
|
|
@@ -501,7 +511,8 @@ export function registerMemoryTools(server, deps) {
|
|
|
501
511
|
// exactly instead (src/names.ts). If it will not fit, say THAT rather than
|
|
502
512
|
// drop the clause: omitting it would report a server that gave no reason.
|
|
503
513
|
const reasonQuote = r?.reason
|
|
504
|
-
? (exactLiteral(r.reason, 400)?.literal ??
|
|
514
|
+
? (exactLiteral(r.reason, 400)?.literal ??
|
|
515
|
+
"(too long to quote exactly)")
|
|
505
516
|
: "";
|
|
506
517
|
// Structured twins of the two text-only values above, for
|
|
507
518
|
// `structuredContent`: the raw number instead of the two-decimal
|
|
@@ -518,7 +529,9 @@ export function registerMemoryTools(server, deps) {
|
|
|
518
529
|
: undefined;
|
|
519
530
|
const optionalStructured = {
|
|
520
531
|
...(reasonStructured === undefined ? {} : { reason: reasonStructured }),
|
|
521
|
-
...(importanceStructured === undefined
|
|
532
|
+
...(importanceStructured === undefined
|
|
533
|
+
? {}
|
|
534
|
+
: { importance: importanceStructured }),
|
|
522
535
|
};
|
|
523
536
|
// Narrowed to `true` by the guard above, so this is now the server's stated
|
|
524
537
|
// verdict rather than "the body was not falsy".
|
|
@@ -683,7 +696,7 @@ export function registerMemoryTools(server, deps) {
|
|
|
683
696
|
idempotentHint: true,
|
|
684
697
|
openWorldHint: false,
|
|
685
698
|
},
|
|
686
|
-
}, async ({ query, top_k, domain, order_by, since, until, exclude_author }) => {
|
|
699
|
+
}, async ({ query, top_k, domain, order_by, since, until, exclude_author, }) => {
|
|
687
700
|
// ONE value, used for the request AND for every decision about it.
|
|
688
701
|
//
|
|
689
702
|
// A previous draft sent the raw string but decided the wording from a
|
|
@@ -704,7 +717,15 @@ export function registerMemoryTools(server, deps) {
|
|
|
704
717
|
const searched = searchedScope(domain);
|
|
705
718
|
const r = await apiFetch("/memory/read", {
|
|
706
719
|
method: "POST",
|
|
707
|
-
body: JSON.stringify(readRequestBody({
|
|
720
|
+
body: JSON.stringify(readRequestBody({
|
|
721
|
+
query,
|
|
722
|
+
top_k,
|
|
723
|
+
domain,
|
|
724
|
+
order_by,
|
|
725
|
+
since,
|
|
726
|
+
until,
|
|
727
|
+
exclude_author,
|
|
728
|
+
})),
|
|
708
729
|
});
|
|
709
730
|
// `items` must be a REAL array before anything below may speak. Core's
|
|
710
731
|
// read response always carries one on a 200, so a body without it is not
|
|
@@ -1076,7 +1097,9 @@ export function registerMemoryTools(server, deps) {
|
|
|
1076
1097
|
// to the service either, so paging stops here and the page says
|
|
1077
1098
|
// the token could not be displayed (Copilot, #159).
|
|
1078
1099
|
position =
|
|
1079
|
-
typeof next === "string" && next && CURSOR_RE.test(next)
|
|
1100
|
+
typeof next === "string" && next && CURSOR_RE.test(next)
|
|
1101
|
+
? next
|
|
1102
|
+
: undefined;
|
|
1080
1103
|
// No cursor: the feed ended, and the page says so. No entries: the
|
|
1081
1104
|
// server is not advancing, so continuing would spend requests on the
|
|
1082
1105
|
// same nothing. Ceiling reached: the caller's count is spent.
|
|
@@ -1232,7 +1255,8 @@ export function registerMemoryTools(server, deps) {
|
|
|
1232
1255
|
// would pass the regex by coercion and then fail the schema.
|
|
1233
1256
|
...(acceptedCursor == null
|
|
1234
1257
|
? { next_cursor: null }
|
|
1235
|
-
: typeof acceptedCursor === "string" &&
|
|
1258
|
+
: typeof acceptedCursor === "string" &&
|
|
1259
|
+
CURSOR_RE.test(acceptedCursor)
|
|
1236
1260
|
? { next_cursor: acceptedCursor }
|
|
1237
1261
|
: {}),
|
|
1238
1262
|
});
|
|
@@ -1273,23 +1297,17 @@ export function registerMemoryTools(server, deps) {
|
|
|
1273
1297
|
"Report whether memories returned by memory_read were actually helpful. This is a learning signal, not a log: positive feedback raises a memory's ranking so it surfaces faster next time (across all of the user's tools), negative feedback lowers it so other memories out-rank it — nothing is erased and nothing decays with time. Call it right after you act on (or reject) recalled memories, passing the ids from the memory_read results as memory_ids. For memories read from a shared room, also pass that room's address as domain; your own memories need no domain. A read-only room member cannot rate the room's memories.",
|
|
1274
1298
|
// `memory_ids` is the name (2026-09-21): the tool rates memories, which
|
|
1275
1299
|
// is what every result is (an atom is the engine's word for its smallest
|
|
1276
|
-
// unit), and the hosted connector already names the parameter so.
|
|
1277
|
-
//
|
|
1278
|
-
//
|
|
1279
|
-
//
|
|
1300
|
+
// unit), and the hosted connector already names the parameter so. The old
|
|
1301
|
+
// name, atom_ids, was accepted alongside it from 0.11 and removed in
|
|
1302
|
+
// 0.13 as announced; with one name left, memory_ids is schema-required.
|
|
1303
|
+
// It carries no format or count cap: the engine
|
|
1280
1304
|
// validates the ids and sets no maximum, and ADR-025 keeps such checks
|
|
1281
1305
|
// with the engine rather than copying them here.
|
|
1282
1306
|
inputSchema: {
|
|
1283
1307
|
memory_ids: z
|
|
1284
1308
|
.array(z.string())
|
|
1285
1309
|
.min(1)
|
|
1286
|
-
.
|
|
1287
|
-
.describe("Required: IDs of the memories to rate, the `id:` line of each memory_read result. (Optional in this schema only while the deprecated atom_ids is still accepted in its place.)"),
|
|
1288
|
-
atom_ids: z
|
|
1289
|
-
.array(z.string())
|
|
1290
|
-
.min(1)
|
|
1291
|
-
.optional()
|
|
1292
|
-
.describe("Deprecated since 0.11, removed in 0.13: atom_ids is the old name of memory_ids, still accepted on its own until then. Pass memory_ids instead."),
|
|
1310
|
+
.describe("IDs of the memories to rate, the `id:` line of each memory_read result."),
|
|
1293
1311
|
outcome: z
|
|
1294
1312
|
.number()
|
|
1295
1313
|
.min(CORE_LIMITS.feedbackOutcome.minimum)
|
|
@@ -1354,34 +1372,8 @@ export function registerMemoryTools(server, deps) {
|
|
|
1354
1372
|
idempotentHint: false,
|
|
1355
1373
|
openWorldHint: false,
|
|
1356
1374
|
},
|
|
1357
|
-
}, async ({ memory_ids,
|
|
1358
|
-
|
|
1359
|
-
// it is refused rather than resolved by a silent preference.
|
|
1360
|
-
if (memory_ids !== undefined && legacyIds !== undefined) {
|
|
1361
|
-
return {
|
|
1362
|
-
isError: true,
|
|
1363
|
-
content: [
|
|
1364
|
-
{
|
|
1365
|
-
type: "text",
|
|
1366
|
-
text: "Pass the ids as memory_ids only. atom_ids is its old name, still " +
|
|
1367
|
-
"accepted on its own, but both at once is ambiguous. Nothing was rated.",
|
|
1368
|
-
},
|
|
1369
|
-
],
|
|
1370
|
-
};
|
|
1371
|
-
}
|
|
1372
|
-
const atom_ids = memory_ids ?? legacyIds;
|
|
1373
|
-
if (atom_ids === undefined) {
|
|
1374
|
-
return {
|
|
1375
|
-
isError: true,
|
|
1376
|
-
content: [
|
|
1377
|
-
{
|
|
1378
|
-
type: "text",
|
|
1379
|
-
text: "memory_feedback needs memory_ids: the ids from the memory_read results " +
|
|
1380
|
-
"you are rating. Nothing was rated.",
|
|
1381
|
-
},
|
|
1382
|
-
],
|
|
1383
|
-
};
|
|
1384
|
-
}
|
|
1375
|
+
}, async ({ memory_ids, outcome, domain }) => {
|
|
1376
|
+
const atom_ids = memory_ids;
|
|
1385
1377
|
// `atom_ids` below is the ENGINE's field name for the same list; the
|
|
1386
1378
|
// wire contract is unchanged. `domain` goes through `searchedScope`, as
|
|
1387
1379
|
// on memory_read and memory_list_recent: an empty string counts as no
|
|
@@ -1391,7 +1383,9 @@ export function registerMemoryTools(server, deps) {
|
|
|
1391
1383
|
const scope = searchedScope(domain);
|
|
1392
1384
|
const r = await apiFetch("/memory/feedback", {
|
|
1393
1385
|
method: "POST",
|
|
1394
|
-
body: JSON.stringify(scope === undefined
|
|
1386
|
+
body: JSON.stringify(scope === undefined
|
|
1387
|
+
? { atom_ids, outcome }
|
|
1388
|
+
: { atom_ids, outcome, domain: scope }),
|
|
1395
1389
|
});
|
|
1396
1390
|
// A FIELD THE SERVER DID NOT SEND IS UNKNOWN, NOT ZERO — the rule
|
|
1397
1391
|
// memory_stats already applies with `num()`, broken here by
|
|
@@ -1416,7 +1410,9 @@ export function registerMemoryTools(server, deps) {
|
|
|
1416
1410
|
// isSafeInteger, not isInteger: the output schema is z.number().int(),
|
|
1417
1411
|
// and zod 4 rejects an integer above 2^53 - 1, so such a count would
|
|
1418
1412
|
// turn the whole reply into an SDK validation error (review, 2026-09-23).
|
|
1419
|
-
typeof reported === "number" &&
|
|
1413
|
+
typeof reported === "number" &&
|
|
1414
|
+
Number.isSafeInteger(reported) &&
|
|
1415
|
+
reported >= 0
|
|
1420
1416
|
? reported
|
|
1421
1417
|
: undefined;
|
|
1422
1418
|
// Structured twin of the text's average-valence clause built further
|
|
@@ -1491,7 +1487,8 @@ export function registerMemoryTools(server, deps) {
|
|
|
1491
1487
|
text: `${sent} The service accepted the call but did not report how many ` +
|
|
1492
1488
|
`memories it updated, so whether any changed is unknown from here. ` +
|
|
1493
1489
|
`That is not evidence of a failure — do not re-send the same rating ` +
|
|
1494
|
-
`on the strength of it.` +
|
|
1490
|
+
`on the strength of it.` +
|
|
1491
|
+
pickADirection,
|
|
1495
1492
|
},
|
|
1496
1493
|
],
|
|
1497
1494
|
};
|
|
@@ -1512,8 +1509,12 @@ export function registerMemoryTools(server, deps) {
|
|
|
1512
1509
|
return structured("No feedback was recorded — none of those ids matched a memory " +
|
|
1513
1510
|
`${feedbackScope(scope)}. ${feedbackMissCauses(scope)}`, {
|
|
1514
1511
|
updated_count: 0,
|
|
1515
|
-
...(avgValenceStructured === undefined
|
|
1516
|
-
|
|
1512
|
+
...(avgValenceStructured === undefined
|
|
1513
|
+
? {}
|
|
1514
|
+
: { avg_valence: avgValenceStructured }),
|
|
1515
|
+
...(coactivationEdges === undefined
|
|
1516
|
+
? {}
|
|
1517
|
+
: { coactivation_edges: coactivationEdges }),
|
|
1517
1518
|
});
|
|
1518
1519
|
}
|
|
1519
1520
|
// WHOSE NUMBER THIS IS (#68). `updated_count` is the count of memories the
|
|
@@ -1531,11 +1532,11 @@ export function registerMemoryTools(server, deps) {
|
|
|
1531
1532
|
const effect = outcome > 0
|
|
1532
1533
|
? " — they should surface sooner next time."
|
|
1533
1534
|
: outcome < 0
|
|
1534
|
-
// No fade. 0.9.1 (#95) withdrew "lets it fade" as false — nothing
|
|
1535
|
-
|
|
1536
|
-
|
|
1537
|
-
|
|
1538
|
-
|
|
1535
|
+
? // No fade. 0.9.1 (#95) withdrew "lets it fade" as false — nothing
|
|
1536
|
+
// time-decays, nothing is auto-deleted, and deletion has been
|
|
1537
|
+
// administrative-only since 0.9.0 — and this line kept promising it
|
|
1538
|
+
// after the release that deleted the claim from the README.
|
|
1539
|
+
" — they should rank lower next time. Out-ranked, not erased: nothing is deleted and nothing decays with time."
|
|
1539
1540
|
: ".";
|
|
1540
1541
|
// WHAT THE COUNT IS NOT: a guarantee that every id landed. `atom_ids.length`
|
|
1541
1542
|
// was never compared with it, so five ids and `updated_count: 2` printed
|
|
@@ -1590,8 +1591,12 @@ export function registerMemoryTools(server, deps) {
|
|
|
1590
1591
|
// resumed it afterwards.
|
|
1591
1592
|
return structured(`${sent} The service reports ${noun} updated${effect}${valence}${mismatch}${pickADirection}`, {
|
|
1592
1593
|
updated_count: count,
|
|
1593
|
-
...(avgValenceStructured === undefined
|
|
1594
|
-
|
|
1594
|
+
...(avgValenceStructured === undefined
|
|
1595
|
+
? {}
|
|
1596
|
+
: { avg_valence: avgValenceStructured }),
|
|
1597
|
+
...(coactivationEdges === undefined
|
|
1598
|
+
? {}
|
|
1599
|
+
: { coactivation_edges: coactivationEdges }),
|
|
1595
1600
|
});
|
|
1596
1601
|
});
|
|
1597
1602
|
// --- Tool: memory_stats ---
|
|
@@ -1678,8 +1683,8 @@ export function registerMemoryTools(server, deps) {
|
|
|
1678
1683
|
// "0" is the same class of lie as an empty search claiming emptiness:
|
|
1679
1684
|
// "Associations: 0" reads as "this memory has learned nothing", which is
|
|
1680
1685
|
// a strong and possibly false statement about the product itself.
|
|
1681
|
-
const num = (v) =>
|
|
1682
|
-
const dec = (v) =>
|
|
1686
|
+
const num = (v) => typeof v === "number" ? String(v) : "unknown";
|
|
1687
|
+
const dec = (v) => typeof v === "number" ? v.toFixed(2) : "unknown";
|
|
1683
1688
|
// THE surface this tool's own description sends the reader to, to
|
|
1684
1689
|
// confirm the exact domain name before writing to it — so it has to be
|
|
1685
1690
|
// able to answer that. (Until 2026-08-20 memory_delete_domain also sent
|
|
@@ -1733,7 +1738,9 @@ export function registerMemoryTools(server, deps) {
|
|
|
1733
1738
|
// whatever num()/dec() print for the same malformed value, and that
|
|
1734
1739
|
// text/data divergence is disclosed in the CHANGELOG, as S5 disclosed
|
|
1735
1740
|
// its cursor semantics.
|
|
1736
|
-
const safeIntOrUndefined = (v) => typeof v === "number" && Number.isSafeInteger(v) && v >= 0
|
|
1741
|
+
const safeIntOrUndefined = (v) => typeof v === "number" && Number.isSafeInteger(v) && v >= 0
|
|
1742
|
+
? v
|
|
1743
|
+
: undefined;
|
|
1737
1744
|
const finiteOrUndefined = (v) => typeof v === "number" && Number.isFinite(v) ? v : undefined;
|
|
1738
1745
|
const episodesStructured = safeIntOrUndefined(r?.episodes);
|
|
1739
1746
|
const prototypesStructured = safeIntOrUndefined(r?.prototypes);
|
|
@@ -1777,12 +1784,18 @@ export function registerMemoryTools(server, deps) {
|
|
|
1777
1784
|
withDomainEscapeLegend(capResult(text, "The domain list was truncated — some domain names are not shown."), ...(Array.isArray(r?.domains) ? r.domains : [])), {
|
|
1778
1785
|
memory_count: memoryCount,
|
|
1779
1786
|
domains: domainsStructured,
|
|
1780
|
-
...(episodesStructured === undefined
|
|
1781
|
-
|
|
1787
|
+
...(episodesStructured === undefined
|
|
1788
|
+
? {}
|
|
1789
|
+
: { episodes: episodesStructured }),
|
|
1790
|
+
...(prototypesStructured === undefined
|
|
1791
|
+
? {}
|
|
1792
|
+
: { prototypes: prototypesStructured }),
|
|
1782
1793
|
...(hebbianEdgesStructured === undefined
|
|
1783
1794
|
? {}
|
|
1784
1795
|
: { hebbian_edges: hebbianEdgesStructured }),
|
|
1785
|
-
...(avgValenceStructured === undefined
|
|
1796
|
+
...(avgValenceStructured === undefined
|
|
1797
|
+
? {}
|
|
1798
|
+
: { avg_valence: avgValenceStructured }),
|
|
1786
1799
|
...(avgImportanceStructured === undefined
|
|
1787
1800
|
? {}
|
|
1788
1801
|
: { avg_importance: avgImportanceStructured }),
|
|
@@ -1948,7 +1961,10 @@ export function registerMemoryTools(server, deps) {
|
|
|
1948
1961
|
},
|
|
1949
1962
|
outputSchema: {
|
|
1950
1963
|
share_message: z.string().describe("Ready-to-forward invite text."),
|
|
1951
|
-
join_url: z
|
|
1964
|
+
join_url: z
|
|
1965
|
+
.string()
|
|
1966
|
+
.optional()
|
|
1967
|
+
.describe("Landing URL the invitee can open to join."),
|
|
1952
1968
|
code: z
|
|
1953
1969
|
.string()
|
|
1954
1970
|
.optional()
|
|
@@ -1958,7 +1974,11 @@ export function registerMemoryTools(server, deps) {
|
|
|
1958
1974
|
.string()
|
|
1959
1975
|
.optional()
|
|
1960
1976
|
.describe("The room's domain address (xroom:<id>)."),
|
|
1961
|
-
expires_at: z
|
|
1977
|
+
expires_at: z
|
|
1978
|
+
.string()
|
|
1979
|
+
.nullable()
|
|
1980
|
+
.optional()
|
|
1981
|
+
.describe("ISO 8601 expiry, or null."),
|
|
1962
1982
|
},
|
|
1963
1983
|
annotations: {
|
|
1964
1984
|
title: "Invite to room",
|
|
@@ -1995,7 +2015,10 @@ export function registerMemoryTools(server, deps) {
|
|
|
1995
2015
|
// structuredContent.share_message to pair it with.
|
|
1996
2016
|
const shareMessageStructured = structuredText(rawMessage, 800);
|
|
1997
2017
|
if (shareMessageStructured === undefined) {
|
|
1998
|
-
return {
|
|
2018
|
+
return {
|
|
2019
|
+
content: [{ type: "text", text }],
|
|
2020
|
+
isError: true,
|
|
2021
|
+
};
|
|
1999
2022
|
}
|
|
2000
2023
|
// `join_url` through safeInline with the connector's own cap of 400,
|
|
2001
2024
|
// as the connector does (mnemoverse-mcp-remote, memory_invite_to_room):
|
|
@@ -2021,21 +2044,33 @@ export function registerMemoryTools(server, deps) {
|
|
|
2021
2044
|
// structured consumer lands on the instant this client used rather than
|
|
2022
2045
|
// reading the naive string as local time (CodeRabbit, review round 2).
|
|
2023
2046
|
// `null` when core sent null; absent when the value does not parse.
|
|
2024
|
-
const expiresAtStructured = r?.expires_at === null
|
|
2047
|
+
const expiresAtStructured = r?.expires_at === null
|
|
2048
|
+
? null
|
|
2049
|
+
: (utcInstant(r?.expires_at) ?? undefined);
|
|
2025
2050
|
return structured(text, {
|
|
2026
2051
|
share_message: shareMessageStructured,
|
|
2027
|
-
...(joinUrlStructured === undefined
|
|
2052
|
+
...(joinUrlStructured === undefined
|
|
2053
|
+
? {}
|
|
2054
|
+
: { join_url: joinUrlStructured }),
|
|
2028
2055
|
...(codeStructured === undefined ? {} : { code: codeStructured }),
|
|
2029
2056
|
...(scopeStructured === undefined ? {} : { scope: scopeStructured }),
|
|
2030
|
-
...(roomAddressStructured === undefined
|
|
2031
|
-
|
|
2057
|
+
...(roomAddressStructured === undefined
|
|
2058
|
+
? {}
|
|
2059
|
+
: { room_address: roomAddressStructured }),
|
|
2060
|
+
...(expiresAtStructured === undefined
|
|
2061
|
+
? {}
|
|
2062
|
+
: { expires_at: expiresAtStructured }),
|
|
2032
2063
|
});
|
|
2033
2064
|
});
|
|
2034
2065
|
// --- Tool: memory_join_room ---
|
|
2035
2066
|
server.registerTool("memory_join_room", {
|
|
2036
2067
|
description: "Join a shared memory room using an invite code (starts with 'mnvr_'). Use when the user pastes an invite code or says something like 'join room with code ...'. After joining, use the returned address as the `domain` on memory_read to read the shared room — the result tells you what you may do with it: memory_write to that address is only allowed when your membership scope is read_write; a read-only membership has that write refused; and when the server does not report a scope, whether memory_write would succeed is stated as unknown rather than promised either way.",
|
|
2037
2068
|
inputSchema: {
|
|
2038
|
-
code: z
|
|
2069
|
+
code: z
|
|
2070
|
+
.string()
|
|
2071
|
+
.min(1)
|
|
2072
|
+
.max(200)
|
|
2073
|
+
.describe("The invite code (mnvr_...)."),
|
|
2039
2074
|
},
|
|
2040
2075
|
outputSchema: {
|
|
2041
2076
|
room_id: z.string().describe("The room's id (room_...)."),
|
|
@@ -2043,7 +2078,10 @@ export function registerMemoryTools(server, deps) {
|
|
|
2043
2078
|
.string()
|
|
2044
2079
|
.describe("Domain address (xroom:<id>); pass as `domain` on read/write."),
|
|
2045
2080
|
name: z.string().optional().describe("The room name."),
|
|
2046
|
-
scope: z
|
|
2081
|
+
scope: z
|
|
2082
|
+
.string()
|
|
2083
|
+
.optional()
|
|
2084
|
+
.describe("Your role in the room ('read' | 'read_write')."),
|
|
2047
2085
|
already_member: z
|
|
2048
2086
|
.boolean()
|
|
2049
2087
|
.optional()
|
|
@@ -2173,7 +2211,9 @@ export function registerMemoryTools(server, deps) {
|
|
|
2173
2211
|
.describe("Domain address (xroom:<id>); pass as `domain` on read/write."),
|
|
2174
2212
|
role: z.string().describe("'owner' or 'member'."),
|
|
2175
2213
|
scope: z.string().optional().describe("'read' or 'read_write'."),
|
|
2176
|
-
archived: z
|
|
2214
|
+
archived: z
|
|
2215
|
+
.boolean()
|
|
2216
|
+
.describe("True if archived (owned rooms only)."),
|
|
2177
2217
|
})),
|
|
2178
2218
|
},
|
|
2179
2219
|
annotations: {
|
|
@@ -2290,7 +2330,8 @@ export function registerMemoryTools(server, deps) {
|
|
|
2290
2330
|
droppedRooms += 1;
|
|
2291
2331
|
continue;
|
|
2292
2332
|
}
|
|
2293
|
-
const nameStructured = typeof r?.name === "string" &&
|
|
2333
|
+
const nameStructured = typeof r?.name === "string" &&
|
|
2334
|
+
exactLiteral(r.name, MAX_DOMAIN_LITERAL)
|
|
2294
2335
|
? structuredText(r.name, MAX_DOMAIN_LITERAL)
|
|
2295
2336
|
: undefined;
|
|
2296
2337
|
roomsStructured.push({
|
|
@@ -2368,7 +2409,9 @@ export function registerMemoryTools(server, deps) {
|
|
|
2368
2409
|
alias: z
|
|
2369
2410
|
.string()
|
|
2370
2411
|
.describe("The secret's alias — the reference you use, never the value."),
|
|
2371
|
-
context: z
|
|
2412
|
+
context: z
|
|
2413
|
+
.string()
|
|
2414
|
+
.describe("The secret's purpose/context — never the value."),
|
|
2372
2415
|
concepts: z.array(z.string()).describe("Concept tags."),
|
|
2373
2416
|
})),
|
|
2374
2417
|
},
|
|
@@ -2391,7 +2434,9 @@ export function registerMemoryTools(server, deps) {
|
|
|
2391
2434
|
return unreadableAnswerReply("The secret list", "a list of your Vault secrets", "none are stored");
|
|
2392
2435
|
}
|
|
2393
2436
|
if (list.length === 0) {
|
|
2394
|
-
return structured("No secrets are stored in your Vault yet.", {
|
|
2437
|
+
return structured("No secrets are stored in your Vault yet.", {
|
|
2438
|
+
secrets: [],
|
|
2439
|
+
});
|
|
2395
2440
|
}
|
|
2396
2441
|
const lines = list.map((s) => {
|
|
2397
2442
|
const alias = safeInline(s?.alias) || "(no alias)";
|
|
@@ -2418,12 +2463,17 @@ export function registerMemoryTools(server, deps) {
|
|
|
2418
2463
|
const concepts = Array.isArray(conceptsRaw)
|
|
2419
2464
|
? conceptsRaw.map((c) => structuredText(c, VAULT_CONCEPT_CAP))
|
|
2420
2465
|
: undefined;
|
|
2421
|
-
const conceptsOk = concepts !== undefined &&
|
|
2466
|
+
const conceptsOk = concepts !== undefined &&
|
|
2467
|
+
concepts.every((c) => c !== undefined);
|
|
2422
2468
|
if (alias === undefined || context === undefined || !conceptsOk) {
|
|
2423
2469
|
droppedSecrets += 1;
|
|
2424
2470
|
continue;
|
|
2425
2471
|
}
|
|
2426
|
-
secretsStructured.push({
|
|
2472
|
+
secretsStructured.push({
|
|
2473
|
+
alias,
|
|
2474
|
+
context,
|
|
2475
|
+
concepts: concepts,
|
|
2476
|
+
});
|
|
2427
2477
|
}
|
|
2428
2478
|
if (droppedSecrets > 0) {
|
|
2429
2479
|
console.error(`Mnemoverse: vault_list dropped ${droppedSecrets} malformed secret ` +
|
|
@@ -2433,5 +2483,226 @@ export function registerMemoryTools(server, deps) {
|
|
|
2433
2483
|
}
|
|
2434
2484
|
return structured(finalText, { secrets: secretsStructured });
|
|
2435
2485
|
});
|
|
2486
|
+
// --- Tool: memory_graph ---
|
|
2487
|
+
server.registerTool("memory_graph", {
|
|
2488
|
+
description: "Reads the association edges around given concepts: which concepts the memory has linked together, with each link's weight, outcome valence and co-activation count. Use to inspect what a memory store has learned or to explain why a read expanded to a concept. Read-only.",
|
|
2489
|
+
inputSchema: {
|
|
2490
|
+
// Bounds come from CORE_LIMITS (src/limits.ts, ADR-025) EXCEPT the
|
|
2491
|
+
// per-seed 200-character cap: GraphRequestSchema.seeds.items carries
|
|
2492
|
+
// no maxLength in the published JSON Schema at all (core enforces it
|
|
2493
|
+
// with a Pydantic validator that has no JSON-Schema-expressible
|
|
2494
|
+
// form, unlike writeContent/domain/etc.), so there is no numeric
|
|
2495
|
+
// field for scripts/generate-limits.mjs to fetch — that literal
|
|
2496
|
+
// stays inline, same as recentCursor's is generated but this one
|
|
2497
|
+
// cannot be.
|
|
2498
|
+
seeds: z
|
|
2499
|
+
.array(z
|
|
2500
|
+
.string()
|
|
2501
|
+
// CODE-POINT length, not `.max(200)` (Copilot review, round 2):
|
|
2502
|
+
// zod's `.max()` on a string counts UTF-16 CODE UNITS, and
|
|
2503
|
+
// core's 200-char/VARCHAR(200) bound counts Unicode CHARACTERS
|
|
2504
|
+
// — 200 astral characters (many emoji, some CJK-extension
|
|
2505
|
+
// ideographs) are 400 UTF-16 units and would be rejected here
|
|
2506
|
+
// while remaining a valid seed core would accept. `[...s]`
|
|
2507
|
+
// iterates by code point, the same technique `structuredText`
|
|
2508
|
+
// and `capResult`'s hint already use (src/names.ts, src/tools.ts).
|
|
2509
|
+
.refine((s) => [...s].length <= 200, {
|
|
2510
|
+
message: "must be at most 200 characters",
|
|
2511
|
+
})
|
|
2512
|
+
.refine((s) => s.trim().length > 0, {
|
|
2513
|
+
message: "must not be blank",
|
|
2514
|
+
}))
|
|
2515
|
+
.min(CORE_LIMITS.graphSeeds.minItems)
|
|
2516
|
+
.max(CORE_LIMITS.graphSeeds.maxItems)
|
|
2517
|
+
.describe("Concepts to center the graph on (1-20, each ≤200 chars, non-blank) — e.g. ['deploy', 'staging']. An unrecognised concept simply contributes no edges; it is not an error."),
|
|
2518
|
+
depth: z
|
|
2519
|
+
.number()
|
|
2520
|
+
.int()
|
|
2521
|
+
.min(CORE_LIMITS.graphDepth.minimum)
|
|
2522
|
+
.max(CORE_LIMITS.graphDepth.maximum)
|
|
2523
|
+
.optional()
|
|
2524
|
+
.describe("Hops to expand from the seeds (1-3, default 1). At depth 2 or 3, if min_weight is omitted the engine floors edge weight at 0.05 at EVERY hop — including the first — so a hub concept cannot fan out across the whole store before limit applies; pass min_weight explicitly (0 included) to see every edge anyway."),
|
|
2525
|
+
domain: z
|
|
2526
|
+
.string()
|
|
2527
|
+
.optional()
|
|
2528
|
+
.describe("Read a shared room's graph instead of your own: pass that room's address (e.g. 'xroom:room_01ABC'). Find room addresses with memory_list_rooms. Unlike memory_read, any OTHER value has no effect here — the association store has no domain column, so a plain domain name behaves exactly like omitting this field."),
|
|
2529
|
+
min_weight: z
|
|
2530
|
+
.number()
|
|
2531
|
+
.finite()
|
|
2532
|
+
.min(CORE_LIMITS.graphMinWeight.minimum)
|
|
2533
|
+
.optional()
|
|
2534
|
+
.describe("Only include edges at or above this weight (≥ 0). Omit for no floor at depth 1; at depth 2/3 the engine applies its own 0.05 floor when this is omitted (see depth) — pass 0 to see every edge at every depth."),
|
|
2535
|
+
limit: z
|
|
2536
|
+
.number()
|
|
2537
|
+
.int()
|
|
2538
|
+
.min(CORE_LIMITS.graphLimit.minimum)
|
|
2539
|
+
.max(CORE_LIMITS.graphLimit.maximum)
|
|
2540
|
+
.optional()
|
|
2541
|
+
.describe("Max edges to return (1-500, default 100 — mirrors memory_read's top_k bounds)."),
|
|
2542
|
+
},
|
|
2543
|
+
// Field names and descriptions follow core's GraphNodeSchema/GraphEdgeSchema
|
|
2544
|
+
// (https://core.mnemoverse.com/openapi.json) directly — this is a new
|
|
2545
|
+
// endpoint with no back-compat surface, so there is no prior wire shape to
|
|
2546
|
+
// reconcile the way memory_read's MEMORY_ITEM_OUTPUT does.
|
|
2547
|
+
outputSchema: {
|
|
2548
|
+
nodes: z
|
|
2549
|
+
.array(z.object({
|
|
2550
|
+
concept: z.string().describe("The concept name."),
|
|
2551
|
+
degree: z
|
|
2552
|
+
.number()
|
|
2553
|
+
.int()
|
|
2554
|
+
.describe("Edges in THIS response touching this concept — not its total degree across the whole store, which limit/truncated may cut short."),
|
|
2555
|
+
}))
|
|
2556
|
+
.describe("Concepts touched by edges below. A seed with no surviving edge (unrecognised concept, or every edge fell below the weight floor) is not listed."),
|
|
2557
|
+
edges: z
|
|
2558
|
+
.array(z.object({
|
|
2559
|
+
source: z
|
|
2560
|
+
.string()
|
|
2561
|
+
.describe("One side of the edge (storage order, not learn order)."),
|
|
2562
|
+
target: z
|
|
2563
|
+
.string()
|
|
2564
|
+
.describe("The other side of the edge (storage order, not learn order)."),
|
|
2565
|
+
weight: z.number().describe("Co-activation strength, 0 and up."),
|
|
2566
|
+
valence: z.number().describe("Outcome polarity, -1 to 1."),
|
|
2567
|
+
count: z.number().int().describe("Co-activation count."),
|
|
2568
|
+
updated_at: z
|
|
2569
|
+
.string()
|
|
2570
|
+
.describe("UTC instant this edge was last reinforced."),
|
|
2571
|
+
}))
|
|
2572
|
+
.describe("Association edges found within the requested depth."),
|
|
2573
|
+
truncated: z
|
|
2574
|
+
.boolean()
|
|
2575
|
+
.describe("True when a per-hop server cap or limit cut the walk short — the store may hold more edges than are reported here."),
|
|
2576
|
+
min_weight_applied: z
|
|
2577
|
+
.number()
|
|
2578
|
+
.describe("The weight floor actually used at every hop: your min_weight when you set one (0 included); otherwise 0.05 from depth 2, or 0 at depth 1."),
|
|
2579
|
+
},
|
|
2580
|
+
annotations: {
|
|
2581
|
+
title: "Association graph",
|
|
2582
|
+
readOnlyHint: true,
|
|
2583
|
+
destructiveHint: false,
|
|
2584
|
+
idempotentHint: true,
|
|
2585
|
+
openWorldHint: false,
|
|
2586
|
+
},
|
|
2587
|
+
}, async ({ seeds, depth, domain, min_weight, limit }) => {
|
|
2588
|
+
const r = await apiFetch("/memory/graph", {
|
|
2589
|
+
method: "POST",
|
|
2590
|
+
body: JSON.stringify(graphRequestBody({ seeds, depth, domain, min_weight, limit })),
|
|
2591
|
+
});
|
|
2592
|
+
// core's GraphResponseSchema sends all four of these on every 200
|
|
2593
|
+
// (nodes, edges, truncated, min_weight_applied are all required) — the
|
|
2594
|
+
// same "unreadable 2xx" class memory_read's item guard catches one
|
|
2595
|
+
// level up, here for the whole response shape and for each edge/node.
|
|
2596
|
+
// All-or-nothing, like memory_write's atom_id guard: there is no honest
|
|
2597
|
+
// partial rendering of a graph this client could not fully validate.
|
|
2598
|
+
const nodesRaw = r?.nodes;
|
|
2599
|
+
const edgesRaw = r?.edges;
|
|
2600
|
+
const truncatedRaw = r?.truncated;
|
|
2601
|
+
const minWeightAppliedRaw = r?.min_weight_applied;
|
|
2602
|
+
const shapeOk = Array.isArray(nodesRaw) &&
|
|
2603
|
+
Array.isArray(edgesRaw) &&
|
|
2604
|
+
typeof truncatedRaw === "boolean" &&
|
|
2605
|
+
typeof minWeightAppliedRaw === "number" &&
|
|
2606
|
+
Number.isFinite(minWeightAppliedRaw) &&
|
|
2607
|
+
// `degree`/`count` are safe-integer checks, not just finite ones:
|
|
2608
|
+
// the outputSchema declares both `z.number().int()` (CodeRabbit
|
|
2609
|
+
// review, round 1), and zod 4 rejects a non-integer AND an integer
|
|
2610
|
+
// above 2^53 - 1 there — the same reasoning memory_feedback's
|
|
2611
|
+
// `updated_count` guard already documents. `weight`/`valence` stay
|
|
2612
|
+
// finite-only: their outputSchema is plain `z.number()`.
|
|
2613
|
+
nodesRaw.every((n) => typeof n?.concept === "string" &&
|
|
2614
|
+
typeof n?.degree === "number" &&
|
|
2615
|
+
Number.isSafeInteger(n.degree)) &&
|
|
2616
|
+
edgesRaw.every((e) => typeof e?.source === "string" &&
|
|
2617
|
+
typeof e?.target === "string" &&
|
|
2618
|
+
typeof e?.weight === "number" &&
|
|
2619
|
+
Number.isFinite(e.weight) &&
|
|
2620
|
+
typeof e?.valence === "number" &&
|
|
2621
|
+
Number.isFinite(e.valence) &&
|
|
2622
|
+
typeof e?.count === "number" &&
|
|
2623
|
+
Number.isSafeInteger(e.count) &&
|
|
2624
|
+
typeof e?.updated_at === "string");
|
|
2625
|
+
if (!shapeOk) {
|
|
2626
|
+
return unreadableAnswerReply("The graph result", "a set of association edges", "these concepts have no associations");
|
|
2627
|
+
}
|
|
2628
|
+
const nodes = nodesRaw;
|
|
2629
|
+
const edges = edgesRaw;
|
|
2630
|
+
const truncated = truncatedRaw;
|
|
2631
|
+
const minWeightApplied = minWeightAppliedRaw;
|
|
2632
|
+
const hops = depth ?? 1;
|
|
2633
|
+
// Computed BEFORE the empty-edges branch and shared with the non-empty
|
|
2634
|
+
// one below (Copilot review, round 2): the early return used to skip
|
|
2635
|
+
// both notes entirely, so a truncated, all-below-floor answer (0 edges
|
|
2636
|
+
// survived the cut) told a text-only reader only "no association edges
|
|
2637
|
+
// found" — indistinguishable from a genuinely quiet concept.
|
|
2638
|
+
const truncatedNote = truncated
|
|
2639
|
+
? "\n\n(truncated — the store may hold more edges than this call " +
|
|
2640
|
+
"reached; core does not promise these are sorted by weight before " +
|
|
2641
|
+
"the cut, so a higher min_weight, not a lower limit, is what " +
|
|
2642
|
+
"reliably narrows to the strongest ones)"
|
|
2643
|
+
: "";
|
|
2644
|
+
const floorAttribution = min_weight === undefined
|
|
2645
|
+
? "the engine's own floor at this depth"
|
|
2646
|
+
: "the min_weight you passed";
|
|
2647
|
+
const floorNote = minWeightApplied > 0
|
|
2648
|
+
? `\n\n(edges below weight ${minWeightApplied} were excluded — ${floorAttribution})`
|
|
2649
|
+
: "";
|
|
2650
|
+
if (edges.length === 0) {
|
|
2651
|
+
return structured(`No association edges found for ${seeds.length === 1 ? "this seed" : "these seeds"} ` +
|
|
2652
|
+
`within ${hops} hop${hops === 1 ? "" : "s"}` +
|
|
2653
|
+
(minWeightApplied > 0
|
|
2654
|
+
? ` at or above weight ${minWeightApplied}`
|
|
2655
|
+
: "") +
|
|
2656
|
+
`.` +
|
|
2657
|
+
truncatedNote +
|
|
2658
|
+
floorNote, { nodes, edges, truncated, min_weight_applied: minWeightApplied });
|
|
2659
|
+
}
|
|
2660
|
+
// Sorted by weight, strongest first (the brief for this tool, and the
|
|
2661
|
+
// one ranking a reader can act on — core does not promise the wire
|
|
2662
|
+
// order is anything in particular). structuredContent.edges (below)
|
|
2663
|
+
// deliberately keeps the SERVER's order — only this rendered `lines`
|
|
2664
|
+
// array is reordered (llms.txt says so explicitly, Copilot round 2).
|
|
2665
|
+
const sorted = [...edges].sort((a, b) => b.weight - a.weight);
|
|
2666
|
+
// `-0.00` guard, same fix memory_feedback's avg_valence carries
|
|
2667
|
+
// (CodeRabbit on #146): valence can be negative and round to zero.
|
|
2668
|
+
const fmtSigned = (n) => n.toFixed(2).replace(/^-0\.00$/, "0.00");
|
|
2669
|
+
// Concept names are NOT this client's own text (CN-032): in a shared
|
|
2670
|
+
// room they are whatever concept another member's memory_write
|
|
2671
|
+
// supplied, so a newline or instruction-shaped string in `source`/
|
|
2672
|
+
// `target` must not be interpolated raw into text a DIFFERENT
|
|
2673
|
+
// principal's model then reads (Copilot review, round 1). Printed as
|
|
2674
|
+
// an exact JSON literal (src/names.ts), the same treatment `@domain`
|
|
2675
|
+
// and `[by "name"]` already get — not `safeInline`, which maps every
|
|
2676
|
+
// non-ASCII `\w`-excluded character to a space and would silently
|
|
2677
|
+
// erase a Cyrillic/CJK concept name the way it once erased author
|
|
2678
|
+
// names (issue #66). `structuredContent` still carries the raw value
|
|
2679
|
+
// (below): only the rendered TEXT needs the escape treatment.
|
|
2680
|
+
const nameLiteral = (s) => exactLiteral(s, MAX_DOMAIN_LITERAL)?.literal ??
|
|
2681
|
+
"(name cannot be printed exactly)";
|
|
2682
|
+
const lines = sorted.map((e, i) => `${i + 1}. ${nameLiteral(e.source)} — ${nameLiteral(e.target)} ` +
|
|
2683
|
+
`(weight: ${e.weight.toFixed(2)}, valence: ${fmtSigned(e.valence)}, ` +
|
|
2684
|
+
`count: ${e.count})${formatDateTag(e.updated_at)}`);
|
|
2685
|
+
const header = `${edges.length} association edge${edges.length === 1 ? "" : "s"} found` +
|
|
2686
|
+
(minWeightApplied > 0 ? ` (weight ≥ ${minWeightApplied})` : "") +
|
|
2687
|
+
":";
|
|
2688
|
+
// The two status notes go BEFORE the edge lines, not after them: the
|
|
2689
|
+
// cap truncates from the end, so a page long enough to be cut would
|
|
2690
|
+
// otherwise lose exactly the lines that say it is incomplete and what
|
|
2691
|
+
// floor applied (CodeRabbit on #174, after the 0.12.1 merge). Legend
|
|
2692
|
+
// appended AFTER capResult, like memory_read's, for the same reason.
|
|
2693
|
+
const notes = truncatedNote + floorNote;
|
|
2694
|
+
const preface = notes ? header + notes + "\n" : header;
|
|
2695
|
+
const text = withDomainEscapeLegend(capResult([preface, ...lines].join("\n"), "Lower `limit` or raise `min_weight` to see fewer edges."), ...edges.flatMap((e) => [e.source, e.target]));
|
|
2696
|
+
// structuredContent carries the validated nodes/edges EXACTLY, uncapped
|
|
2697
|
+
// and unescaped (OD-11, the same rule memory_read's items get): a
|
|
2698
|
+
// client reading structured data reads these as the graph, not as a
|
|
2699
|
+
// rendering of it.
|
|
2700
|
+
return structured(text, {
|
|
2701
|
+
nodes,
|
|
2702
|
+
edges,
|
|
2703
|
+
truncated,
|
|
2704
|
+
min_weight_applied: minWeightApplied,
|
|
2705
|
+
});
|
|
2706
|
+
});
|
|
2436
2707
|
}
|
|
2437
2708
|
//# sourceMappingURL=tools.js.map
|