@squirrelnotes.app/mcp-embeddings 2.1.1-test.341 → 2.1.1-test.343
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 +14 -3
- package/dist/cli.js +255 -30
- package/npm-shrinkwrap.json +2 -2
- package/package.json +2 -2
package/README.md
CHANGED
|
@@ -8,7 +8,8 @@ flat white with oat milk"*.
|
|
|
8
8
|
It is the same server as [`@squirrelnotes.app/mcp`](https://www.npmjs.com/package/@squirrelnotes.app/mcp):
|
|
9
9
|
the same tools, the same inputs and the same output shape. It is built from the
|
|
10
10
|
same source and published from the same commit, at the same version. The
|
|
11
|
-
only
|
|
11
|
+
only differences are the order `search_memory` returns, its `ranking` (`hybrid`) and each hit's
|
|
12
|
+
`semanticScore`, which the base package sends as `null`. If you do not install it,
|
|
12
13
|
nothing changes for you.
|
|
13
14
|
|
|
14
15
|
## What stays on your machine
|
|
@@ -96,8 +97,18 @@ The vector file can be deleted at any time; it is rebuilt when needed.
|
|
|
96
97
|
ranks above one in only one.
|
|
97
98
|
- A semantic match must reach a minimum similarity of 0.35. Below that, a
|
|
98
99
|
memory is returned only if its words match.
|
|
99
|
-
- `score` is on a 0–1 scale here, and unbounded BM25 in the base package.
|
|
100
|
-
|
|
100
|
+
- `score` is on a 0–1 scale here, and unbounded BM25 in the base package. It
|
|
101
|
+
orders one response only: a memory found by one order alone scores about 0.5
|
|
102
|
+
whatever its relevance. The keys are the same in both.
|
|
103
|
+
- To judge a hit, use the absolute scores: `semanticScore` (the cosine
|
|
104
|
+
similarity, reported for every hit, also those below 0.35 found by words) and
|
|
105
|
+
`lexicalScore` (the share of the query's content words, stopwords aside).
|
|
106
|
+
As a guide, from the recall gate's data (ADR-0444): below `semanticScore`
|
|
107
|
+
0.45 and `lexicalScore` 0.67 a hit is rarely the answer (about 1 in 20); at
|
|
108
|
+
`semanticScore` ≥ 0.45 about half are, and at `lexicalScore` ≥ 0.67 about
|
|
109
|
+
9 in 10. These were measured on the delivered (rounded) scores. `semanticScore` is a cosine, −1 to 1
|
|
110
|
+
(in practice about −0.1 to 0.9). `ranking` is `hybrid` when the semantic
|
|
111
|
+
order ran, `lexical` while the model loads or after it fails.
|
|
101
112
|
- Words outside ASCII letters and digits never match lexically, in either
|
|
102
113
|
package. Embeddings can find some of these memories (see *Recall*).
|
|
103
114
|
- A query with no ASCII letters or digits always comes back with a `note`.
|
package/dist/cli.js
CHANGED
|
@@ -22613,8 +22613,15 @@ function intersectSets(a, b) {
|
|
|
22613
22613
|
function searchableText(p) {
|
|
22614
22614
|
return normalizeText([p.text, p.entity, ...p.tags].join(" "));
|
|
22615
22615
|
}
|
|
22616
|
+
function hasSearchableText(query) {
|
|
22617
|
+
return normalizeText(query).length > 0;
|
|
22618
|
+
}
|
|
22616
22619
|
function queryTerms(query) {
|
|
22617
|
-
|
|
22620
|
+
const tokens = normalizeText(query).split(" ").filter(Boolean);
|
|
22621
|
+
const isStop = (w, i) => STOPWORDS.has(w) || NT_STEMS.has(w) && tokens[i + 1] === "t";
|
|
22622
|
+
const content = [...new Set(tokens.filter((w, i) => !isStop(w, i)))];
|
|
22623
|
+
const words = [...new Set(tokens)];
|
|
22624
|
+
return content.length > 0 ? content : words;
|
|
22618
22625
|
}
|
|
22619
22626
|
function recencyWeight(category, lastValidatedAt, now2) {
|
|
22620
22627
|
const at = Date.parse(lastValidatedAt);
|
|
@@ -22700,7 +22707,7 @@ function termCoverage(text, p) {
|
|
|
22700
22707
|
}
|
|
22701
22708
|
return total / terms.length;
|
|
22702
22709
|
}
|
|
22703
|
-
var DECAY_HALF_LIFE_DAYS, BM25_K1, BM25_B, PREFIX_WEIGHT, DAY_MS;
|
|
22710
|
+
var DECAY_HALF_LIFE_DAYS, BM25_K1, BM25_B, PREFIX_WEIGHT, DAY_MS, STOPWORDS, NT_STEMS;
|
|
22704
22711
|
var init_memory_search = __esm({
|
|
22705
22712
|
"../mcp/src/memory-search.ts"() {
|
|
22706
22713
|
"use strict";
|
|
@@ -22714,6 +22721,158 @@ var init_memory_search = __esm({
|
|
|
22714
22721
|
BM25_B = 0.75;
|
|
22715
22722
|
PREFIX_WEIGHT = 0.5;
|
|
22716
22723
|
DAY_MS = 864e5;
|
|
22724
|
+
STOPWORDS = /* @__PURE__ */ new Set([
|
|
22725
|
+
"a",
|
|
22726
|
+
"about",
|
|
22727
|
+
"above",
|
|
22728
|
+
"after",
|
|
22729
|
+
"again",
|
|
22730
|
+
"all",
|
|
22731
|
+
"am",
|
|
22732
|
+
"an",
|
|
22733
|
+
"and",
|
|
22734
|
+
"any",
|
|
22735
|
+
"are",
|
|
22736
|
+
"as",
|
|
22737
|
+
"at",
|
|
22738
|
+
"be",
|
|
22739
|
+
"because",
|
|
22740
|
+
"been",
|
|
22741
|
+
"before",
|
|
22742
|
+
"being",
|
|
22743
|
+
"below",
|
|
22744
|
+
"between",
|
|
22745
|
+
"both",
|
|
22746
|
+
"but",
|
|
22747
|
+
"by",
|
|
22748
|
+
"can",
|
|
22749
|
+
"could",
|
|
22750
|
+
"did",
|
|
22751
|
+
"do",
|
|
22752
|
+
"does",
|
|
22753
|
+
"doing",
|
|
22754
|
+
"during",
|
|
22755
|
+
"each",
|
|
22756
|
+
"few",
|
|
22757
|
+
"for",
|
|
22758
|
+
"from",
|
|
22759
|
+
"further",
|
|
22760
|
+
"had",
|
|
22761
|
+
"has",
|
|
22762
|
+
"have",
|
|
22763
|
+
"having",
|
|
22764
|
+
"he",
|
|
22765
|
+
"her",
|
|
22766
|
+
"here",
|
|
22767
|
+
"hers",
|
|
22768
|
+
"herself",
|
|
22769
|
+
"him",
|
|
22770
|
+
"himself",
|
|
22771
|
+
"his",
|
|
22772
|
+
"how",
|
|
22773
|
+
"i",
|
|
22774
|
+
"if",
|
|
22775
|
+
"in",
|
|
22776
|
+
"into",
|
|
22777
|
+
"is",
|
|
22778
|
+
"it",
|
|
22779
|
+
"its",
|
|
22780
|
+
"itself",
|
|
22781
|
+
"just",
|
|
22782
|
+
"me",
|
|
22783
|
+
"more",
|
|
22784
|
+
"most",
|
|
22785
|
+
"my",
|
|
22786
|
+
"myself",
|
|
22787
|
+
"no",
|
|
22788
|
+
"nor",
|
|
22789
|
+
"not",
|
|
22790
|
+
"of",
|
|
22791
|
+
"off",
|
|
22792
|
+
"on",
|
|
22793
|
+
"once",
|
|
22794
|
+
"only",
|
|
22795
|
+
"or",
|
|
22796
|
+
"other",
|
|
22797
|
+
"our",
|
|
22798
|
+
"ours",
|
|
22799
|
+
"ourselves",
|
|
22800
|
+
"out",
|
|
22801
|
+
"over",
|
|
22802
|
+
"own",
|
|
22803
|
+
"same",
|
|
22804
|
+
"she",
|
|
22805
|
+
"should",
|
|
22806
|
+
"so",
|
|
22807
|
+
"some",
|
|
22808
|
+
"such",
|
|
22809
|
+
"than",
|
|
22810
|
+
"that",
|
|
22811
|
+
"the",
|
|
22812
|
+
"their",
|
|
22813
|
+
"theirs",
|
|
22814
|
+
"them",
|
|
22815
|
+
"themselves",
|
|
22816
|
+
"then",
|
|
22817
|
+
"there",
|
|
22818
|
+
"these",
|
|
22819
|
+
"they",
|
|
22820
|
+
"this",
|
|
22821
|
+
"those",
|
|
22822
|
+
"through",
|
|
22823
|
+
"to",
|
|
22824
|
+
"too",
|
|
22825
|
+
"under",
|
|
22826
|
+
"until",
|
|
22827
|
+
"up",
|
|
22828
|
+
"very",
|
|
22829
|
+
"was",
|
|
22830
|
+
"we",
|
|
22831
|
+
"were",
|
|
22832
|
+
"what",
|
|
22833
|
+
"when",
|
|
22834
|
+
"where",
|
|
22835
|
+
"which",
|
|
22836
|
+
"while",
|
|
22837
|
+
"who",
|
|
22838
|
+
"whom",
|
|
22839
|
+
"whose",
|
|
22840
|
+
"why",
|
|
22841
|
+
"will",
|
|
22842
|
+
"with",
|
|
22843
|
+
"would",
|
|
22844
|
+
"you",
|
|
22845
|
+
"your",
|
|
22846
|
+
"yours",
|
|
22847
|
+
"yourself",
|
|
22848
|
+
"yourselves",
|
|
22849
|
+
// What contractions leave once normalizeText turns the apostrophe into a
|
|
22850
|
+
// space ("what's" → "what s", "doesn't" → "doesn t"). A one-letter term
|
|
22851
|
+
// would prefix-match every word starting with that letter. "don", "won" and
|
|
22852
|
+
// "haven" are also words or names, so they are not here: queryTerms drops
|
|
22853
|
+
// them only before a "t" (NT_STEMS).
|
|
22854
|
+
"s",
|
|
22855
|
+
"t",
|
|
22856
|
+
"d",
|
|
22857
|
+
"ll",
|
|
22858
|
+
"m",
|
|
22859
|
+
"re",
|
|
22860
|
+
"ve",
|
|
22861
|
+
"aren",
|
|
22862
|
+
"couldn",
|
|
22863
|
+
"didn",
|
|
22864
|
+
"doesn",
|
|
22865
|
+
"hadn",
|
|
22866
|
+
"hasn",
|
|
22867
|
+
"isn",
|
|
22868
|
+
"mustn",
|
|
22869
|
+
"needn",
|
|
22870
|
+
"shouldn",
|
|
22871
|
+
"wasn",
|
|
22872
|
+
"weren",
|
|
22873
|
+
"wouldn"
|
|
22874
|
+
]);
|
|
22875
|
+
NT_STEMS = /* @__PURE__ */ new Set(["don", "won", "haven"]);
|
|
22717
22876
|
}
|
|
22718
22877
|
});
|
|
22719
22878
|
|
|
@@ -22730,6 +22889,9 @@ function blendRanks(lexical, dense) {
|
|
|
22730
22889
|
for (const [id, s] of scores) scores.set(id, s / max);
|
|
22731
22890
|
return scores;
|
|
22732
22891
|
}
|
|
22892
|
+
function aboveFloor(similarities, minSimilarity) {
|
|
22893
|
+
return similarities.filter((m) => m.similarity >= minSimilarity);
|
|
22894
|
+
}
|
|
22733
22895
|
var RRF_K;
|
|
22734
22896
|
var init_memory_rank = __esm({
|
|
22735
22897
|
"../mcp/src/memory-rank.ts"() {
|
|
@@ -22785,24 +22947,66 @@ async function listAllMemories(client, key) {
|
|
|
22785
22947
|
return { memories, unverifiable };
|
|
22786
22948
|
}
|
|
22787
22949
|
async function rankForSearch(query, all, eligible, semantic, key, now2) {
|
|
22950
|
+
const byId = new Map(eligible.map((m) => [m.id, m]));
|
|
22951
|
+
const hit = (id, score, similarity2) => ({
|
|
22952
|
+
id,
|
|
22953
|
+
score,
|
|
22954
|
+
lexicalScore: termCoverage(query, byId.get(id).payload),
|
|
22955
|
+
semanticScore: similarity2 ? similarity2.get(id) ?? null : null
|
|
22956
|
+
});
|
|
22788
22957
|
const lexical = scoreMemories(query, eligible, now2);
|
|
22789
|
-
|
|
22790
|
-
|
|
22958
|
+
const lexicalOnly = () => ({ ranked: lexical.map((x) => hit(x.id, x.score, null)), fused: false });
|
|
22959
|
+
if (!semantic) return lexicalOnly();
|
|
22960
|
+
const scored = await semantic.score(
|
|
22791
22961
|
query,
|
|
22792
22962
|
all.map((m) => ({ id: m.id, revision: m.revision, text: memoryEmbeddingText(m.payload) })),
|
|
22793
22963
|
new Set(eligible.map((m) => m.id)),
|
|
22794
22964
|
key
|
|
22795
22965
|
);
|
|
22796
|
-
if (!
|
|
22797
|
-
const
|
|
22966
|
+
if (!scored) return lexicalOnly();
|
|
22967
|
+
const similarity = new Map(scored.similarities.map((m) => [m.id, m.similarity]));
|
|
22798
22968
|
const decay = (id) => {
|
|
22799
22969
|
const p = byId.get(id).payload;
|
|
22800
22970
|
return recencyWeight(p.category, p.lastValidatedAt, now2);
|
|
22801
22971
|
};
|
|
22802
|
-
const dense =
|
|
22803
|
-
const ranked = [...blendRanks(lexical.map((x) => x.id), dense)].map(([id, score]) => ({ id, score, updatedAt: byId.get(id).payload.updatedAt })).sort((a, b) => b.score - a.score || b.updatedAt.localeCompare(a.updatedAt) || (a.id < b.id ? -1 : 1)).map(({ id, score }) => (
|
|
22972
|
+
const dense = aboveFloor(scored.similarities, scored.minSimilarity).map(({ id, similarity: sim }) => ({ id, weighed: sim * decay(id) })).sort((a, b) => b.weighed - a.weighed || (a.id < b.id ? -1 : 1)).map((x) => x.id);
|
|
22973
|
+
const ranked = [...blendRanks(lexical.map((x) => x.id), dense)].map(([id, score]) => ({ id, score, updatedAt: byId.get(id).payload.updatedAt })).sort((a, b) => b.score - a.score || b.updatedAt.localeCompare(a.updatedAt) || (a.id < b.id ? -1 : 1)).map(({ id, score }) => hit(id, score, similarity));
|
|
22804
22974
|
return { ranked, fused: true };
|
|
22805
22975
|
}
|
|
22976
|
+
function listByRecency(eligible) {
|
|
22977
|
+
return [...eligible].sort(compareRecency);
|
|
22978
|
+
}
|
|
22979
|
+
function compareRecency(a, b) {
|
|
22980
|
+
return b.payload.updatedAt.localeCompare(a.payload.updatedAt) || (a.id < b.id ? -1 : a.id > b.id ? 1 : 0);
|
|
22981
|
+
}
|
|
22982
|
+
function encodeListCursor(m) {
|
|
22983
|
+
const position = { u: m.payload.updatedAt, i: m.id };
|
|
22984
|
+
return Buffer.from(JSON.stringify(position)).toString("base64url");
|
|
22985
|
+
}
|
|
22986
|
+
function decodeListCursor(cursor) {
|
|
22987
|
+
try {
|
|
22988
|
+
const parsed = JSON.parse(Buffer.from(cursor, "base64url").toString("utf8"));
|
|
22989
|
+
if (typeof parsed !== "object" || parsed === null) return null;
|
|
22990
|
+
const { u, i } = parsed;
|
|
22991
|
+
return typeof u === "string" && typeof i === "string" && u.length <= 64 && i.length <= 64 ? { u, i } : null;
|
|
22992
|
+
} catch {
|
|
22993
|
+
return null;
|
|
22994
|
+
}
|
|
22995
|
+
}
|
|
22996
|
+
function listPage(eligible, after, limit) {
|
|
22997
|
+
const ordered = listByRecency(eligible);
|
|
22998
|
+
const start = after === null ? ordered : ordered.filter((m) => compareRecency(m, { id: after.i, payload: { updatedAt: after.u } }) > 0);
|
|
22999
|
+
const page = start.slice(0, limit);
|
|
23000
|
+
const last = page[page.length - 1];
|
|
23001
|
+
return { page, nextCursor: start.length > limit && last ? encodeListCursor(last) : null };
|
|
23002
|
+
}
|
|
23003
|
+
function hitScores(h) {
|
|
23004
|
+
return {
|
|
23005
|
+
score: round(h.score, 2),
|
|
23006
|
+
lexicalScore: round(h.lexicalScore, 2),
|
|
23007
|
+
semanticScore: h.semanticScore === null ? null : round(h.semanticScore, 3)
|
|
23008
|
+
};
|
|
23009
|
+
}
|
|
22806
23010
|
function view(m, score) {
|
|
22807
23011
|
const p = m.payload;
|
|
22808
23012
|
return {
|
|
@@ -22823,7 +23027,7 @@ function view(m, score) {
|
|
|
22823
23027
|
reviewedAt: p.reviewedAt,
|
|
22824
23028
|
flaggedAt: p.flaggedAt ?? null,
|
|
22825
23029
|
updatedAt: p.updatedAt,
|
|
22826
|
-
...score
|
|
23030
|
+
...typeof score === "number" ? { score: round(score, 2) } : score ?? {}
|
|
22827
23031
|
};
|
|
22828
23032
|
}
|
|
22829
23033
|
function chunkNote(title, markdown, category) {
|
|
@@ -23060,33 +23264,45 @@ function registerMemoryTools(server, context, options = {}) {
|
|
|
23060
23264
|
};
|
|
23061
23265
|
server.tool(
|
|
23062
23266
|
"search_memory",
|
|
23063
|
-
|
|
23267
|
+
SEARCH_MEMORY_DESCRIPTION,
|
|
23064
23268
|
{
|
|
23065
|
-
query: external_exports.string().
|
|
23269
|
+
query: external_exports.string().max(500).optional().describe("Words to look for. Optional when category or entity is given: then the matching memories are listed, newest first"),
|
|
23066
23270
|
category: external_exports.enum(MEMORY_CATEGORIES).optional().describe("Only this category"),
|
|
23067
23271
|
entity: external_exports.string().max(MAX_ENTITY).optional().describe("Only memories about this entity (case-insensitive)"),
|
|
23068
23272
|
includeSuperseded: external_exports.boolean().default(false),
|
|
23069
23273
|
includeInvalidated: external_exports.boolean().default(false),
|
|
23070
|
-
limit: external_exports.number().int().min(1).max(20).default(5)
|
|
23274
|
+
limit: external_exports.number().int().min(1).max(20).default(5),
|
|
23275
|
+
cursor: external_exports.string().max(200).optional().describe("A listing only (no query): the nextCursor of the previous page, to get the next one")
|
|
23071
23276
|
},
|
|
23072
|
-
async ({ query, category, entity, includeSuperseded, includeInvalidated, limit }) => {
|
|
23277
|
+
async ({ query, category, entity, includeSuperseded, includeInvalidated, limit, cursor }) => {
|
|
23278
|
+
const wantEntity = entity?.trim().toLowerCase() || void 0;
|
|
23279
|
+
const wantQuery = query?.trim() ? query : void 0;
|
|
23280
|
+
if (wantQuery === void 0 && !category && !wantEntity) return refuse("search_memory", NO_QUERY_OR_FILTER);
|
|
23281
|
+
if (cursor !== void 0 && wantQuery !== void 0) return refuse("search_memory", CURSOR_WITH_QUERY);
|
|
23282
|
+
const after = cursor === void 0 ? null : decodeListCursor(cursor);
|
|
23283
|
+
if (cursor !== void 0 && after === null) return refuse("search_memory", BAD_CURSOR);
|
|
23073
23284
|
try {
|
|
23074
23285
|
const ctx = await unlock();
|
|
23075
23286
|
const { client, key } = ctx;
|
|
23076
23287
|
const { memories, unverifiable } = await loadAll(client, key);
|
|
23077
|
-
const wantEntity = entity?.trim().toLowerCase();
|
|
23078
23288
|
const eligible = memories.filter(
|
|
23079
23289
|
({ payload: p }) => p.state === "active" || p.state === "superseded" && includeSuperseded || p.state === "invalidated" && includeInvalidated
|
|
23080
23290
|
).filter(({ payload: p }) => !category || p.category === category).filter(({ payload: p }) => !wantEntity || p.entity.trim().toLowerCase() === wantEntity);
|
|
23291
|
+
if (wantQuery === void 0) {
|
|
23292
|
+
const { page, nextCursor } = listPage(eligible, after, limit);
|
|
23293
|
+
const results2 = page.map((m) => view(m, NO_SCORES));
|
|
23294
|
+
mcpLog(`memory: search_memory listed=${eligible.length} results=${results2.length} unverifiable=${unverifiable}`);
|
|
23295
|
+
const ranking2 = "none";
|
|
23296
|
+
return ok({ ranking: ranking2, results: results2, total: eligible.length, nextCursor, searched: memories.length, unverifiable });
|
|
23297
|
+
}
|
|
23081
23298
|
const byId = new Map(eligible.map((m) => [m.id, m]));
|
|
23082
|
-
const { ranked, fused } = await rankForSearch(
|
|
23083
|
-
const results = ranked.slice(0, limit).map((x) => view(byId.get(x.id), x
|
|
23299
|
+
const { ranked, fused } = await rankForSearch(wantQuery, memories, eligible, options.semantic ?? null, key, Date.now());
|
|
23300
|
+
const results = ranked.slice(0, limit).map((x) => view(byId.get(x.id), hitScores(x)));
|
|
23301
|
+
const ranking = fused ? "hybrid" : "lexical";
|
|
23084
23302
|
mcpLog(`memory: search_memory searched=${memories.length} results=${results.length} unverifiable=${unverifiable}`);
|
|
23085
|
-
|
|
23086
|
-
|
|
23087
|
-
|
|
23088
|
-
}
|
|
23089
|
-
return ok({ results, searched: memories.length, unverifiable });
|
|
23303
|
+
const body = { ranking, results, total: ranked.length, nextCursor: null, searched: memories.length, unverifiable };
|
|
23304
|
+
if (!hasSearchableText(wantQuery)) return ok({ ...body, note: fused ? MEANING_ONLY_NOTE : NO_SEARCHABLE_TERMS });
|
|
23305
|
+
return ok(body);
|
|
23090
23306
|
} catch (err) {
|
|
23091
23307
|
return fail("search_memory", err);
|
|
23092
23308
|
}
|
|
@@ -23317,7 +23533,7 @@ async function supersedeLanded(client, key, newId, oldId) {
|
|
|
23317
23533
|
return false;
|
|
23318
23534
|
}
|
|
23319
23535
|
}
|
|
23320
|
-
var MEMORY_CATEGORIES, MEMORY_STATES, VIAS, MAX_TEXT, MAX_ENTITY, MAX_REASON, MAX_TAGS, MAX_TAG, MAX_CLIENT_NAME, uuidArg, MAX_IMPORT, CREATED_REVISION, MAX_ENCRYPTED_PAYLOAD_LENGTH, NoteVerificationError, MemoryTooLongError, isStr, isStrOrNull, oneOf, NEAR_DUPLICATE_SCORE, NEAR_DUPLICATE_LIMIT, STORAGE_MESSAGES, KeyContextError, isRetryable, USER_DATA_TEXT, NEEDS_REVIEW_TEXT, PROFILE_TOKEN_BUDGET, CHARS_PER_TOKEN, PROFILE_URI, NO_SEARCHABLE_TERMS, MEANING_ONLY_NOTE, NO_PINNED_MESSAGE, MEMORY_INSTRUCTIONS, NEEDS_REVIEW_HELP, now, MEMORY_CACHE_TTL_MS;
|
|
23536
|
+
var MEMORY_CATEGORIES, MEMORY_STATES, VIAS, MAX_TEXT, MAX_ENTITY, MAX_REASON, MAX_TAGS, MAX_TAG, MAX_CLIENT_NAME, uuidArg, MAX_IMPORT, CREATED_REVISION, MAX_ENCRYPTED_PAYLOAD_LENGTH, NoteVerificationError, MemoryTooLongError, isStr, isStrOrNull, oneOf, NEAR_DUPLICATE_SCORE, NEAR_DUPLICATE_LIMIT, round, NO_SCORES, STORAGE_MESSAGES, KeyContextError, isRetryable, USER_DATA_TEXT, NEEDS_REVIEW_TEXT, PROFILE_TOKEN_BUDGET, CHARS_PER_TOKEN, PROFILE_URI, NO_SEARCHABLE_TERMS, MEANING_ONLY_NOTE, CURSOR_WITH_QUERY, BAD_CURSOR, NO_QUERY_OR_FILTER, NO_PINNED_MESSAGE, MEMORY_INSTRUCTIONS, NEEDS_REVIEW_HELP, SEARCH_MEMORY_DESCRIPTION, now, MEMORY_CACHE_TTL_MS;
|
|
23321
23537
|
var init_memory = __esm({
|
|
23322
23538
|
"../mcp/src/memory.ts"() {
|
|
23323
23539
|
"use strict";
|
|
@@ -23357,6 +23573,8 @@ var init_memory = __esm({
|
|
|
23357
23573
|
oneOf = (xs, x) => isStr(x) && xs.includes(x);
|
|
23358
23574
|
NEAR_DUPLICATE_SCORE = 0.6;
|
|
23359
23575
|
NEAR_DUPLICATE_LIMIT = 3;
|
|
23576
|
+
round = (x, places) => Math.round(x * 10 ** places) / 10 ** places;
|
|
23577
|
+
NO_SCORES = { score: null, lexicalScore: null, semanticScore: null };
|
|
23360
23578
|
STORAGE_MESSAGES = ["Memory limit reached", "Memory storage limit reached"];
|
|
23361
23579
|
KeyContextError = class extends Error {
|
|
23362
23580
|
constructor(reason) {
|
|
@@ -23374,9 +23592,13 @@ var init_memory = __esm({
|
|
|
23374
23592
|
PROFILE_URI = "memory://profile";
|
|
23375
23593
|
NO_SEARCHABLE_TERMS = "The query has no searchable characters: memory search matches ASCII letters and digits only, so accented or non-Latin text cannot be searched.";
|
|
23376
23594
|
MEANING_ONLY_NOTE = "The query has no ASCII letters or digits, which word matching ignores: these results were found only by meaning, through local embeddings.";
|
|
23595
|
+
CURSOR_WITH_QUERY = "A cursor pages a listing (a category or entity with no query); a search does not page. Leave out the cursor, or the query.";
|
|
23596
|
+
BAD_CURSOR = "That cursor is not one search_memory returned. Use the nextCursor of the previous page, or leave it out to start again.";
|
|
23597
|
+
NO_QUERY_OR_FILTER = "Nothing to search for: give a query, or a category or entity to list the memories that match it.";
|
|
23377
23598
|
NO_PINNED_MESSAGE = "No pinned memories yet. Only the user pins memories, in the Squirrel Notes app (Memory); agents cannot pin. Pinned memories appear here. Use search_memory to look for anything else.";
|
|
23378
23599
|
MEMORY_INSTRUCTIONS = "Squirrel Notes keeps the user's encrypted notes and long-term memories. At the start of a session, call get_memory_profile (or read the resource memory://profile) to load the memories the user has pinned as always relevant (only the user pins, in the app). Use search_memory before asking the user something they may already have told you, and remember_memory, supersede_memory and invalidate_memory to keep memories current. " + NEEDS_REVIEW_TEXT;
|
|
23379
23600
|
NEEDS_REVIEW_HELP = "`needsReview: true` means the memory was written by an agent and the user has not confirmed it yet; it is still in effect. " + USER_DATA_TEXT;
|
|
23601
|
+
SEARCH_MEMORY_DESCRIPTION = `Search the user's long-term memories (short facts and preferences saved by agents or the user). Ranks active memories by their relevance to the query: matching words in the text, entity and tags, and, when the user has installed local embeddings, similar meaning. Common English words (stopwords such as "what", "does", "he", "is") are ignored by word matching unless the query has nothing else. Word matching uses ASCII letters and digits only (as in the app's search): other letters, such as accented or non-Latin ones, are ignored by it, so without local embeddings a query or memory written only in them cannot be found. The response says which ranking ran: "hybrid" (words and meaning), "lexical" (words only) or "none" (a listing). Each hit has \`score\`, which orders this response only and cannot be compared across searches, and two scores that can: \`lexicalScore\` (0\u20131, the share of the query's words, stopwords aside, that the memory contains; a query word that only begins a memory word counts half) and \`semanticScore\` (the cosine similarity of meaning, null unless the ranking is hybrid). As a guide, measured on a synthetic test set: a hit with semanticScore below 0.45 and lexicalScore below 0.67 is rarely the answer (about 1 in 20); at semanticScore 0.45 or more about half are, and at lexicalScore 0.67 or more (two thirds of the query's words, as delivered) about 9 in 10 are. semanticScore is a cosine, from -1 to 1 (in practice about -0.1 to 0.9). Without a query, a category or entity filter lists the matching memories, newest first, \`limit\` (at most 20) at a time: pass the response's \`nextCursor\` as \`cursor\` for the next page (null on the last). \`total\` is how many match in all; a memory edited during a walk may be missed, so compare against \`total\` or start again. ${NEEDS_REVIEW_HELP} Superseded and invalidated memories are left out unless asked for.`;
|
|
23380
23602
|
now = () => (/* @__PURE__ */ new Date()).toISOString();
|
|
23381
23603
|
MEMORY_CACHE_TTL_MS = 6e4;
|
|
23382
23604
|
}
|
|
@@ -24610,6 +24832,7 @@ var memory_semantic_exports = {};
|
|
|
24610
24832
|
__export(memory_semantic_exports, {
|
|
24611
24833
|
RRF_K: () => RRF_K,
|
|
24612
24834
|
SemanticMemoryIndex: () => SemanticMemoryIndex,
|
|
24835
|
+
aboveFloor: () => aboveFloor,
|
|
24613
24836
|
blendRanks: () => blendRanks,
|
|
24614
24837
|
memoryEmbeddingText: () => memoryEmbeddingText
|
|
24615
24838
|
});
|
|
@@ -24648,15 +24871,18 @@ var init_memory_semantic = __esm({
|
|
|
24648
24871
|
batchSize;
|
|
24649
24872
|
settled;
|
|
24650
24873
|
/**
|
|
24651
|
-
*
|
|
24652
|
-
*
|
|
24653
|
-
* lexically".
|
|
24874
|
+
* Every candidate that has a vector, with its cosine similarity to `query`,
|
|
24875
|
+
* best first, and the model's minimum alongside, from one embedding of the
|
|
24876
|
+
* query; or null, meaning "rank lexically". search_memory fuses those at or
|
|
24877
|
+
* above the minimum (`aboveFloor`), weighed by recency, and reports each
|
|
24878
|
+
* hit's similarity as `semanticScore` (ADR-0444, amended). A candidate not
|
|
24879
|
+
* embedded within the time budget is absent.
|
|
24654
24880
|
*
|
|
24655
24881
|
* `all` is every memory the list returned: it decides which vectors are kept
|
|
24656
24882
|
* on file. `candidates` is the subset search_memory has already filtered
|
|
24657
24883
|
* (state, category, entity).
|
|
24658
24884
|
*/
|
|
24659
|
-
async
|
|
24885
|
+
async score(query, all, candidates2, dek) {
|
|
24660
24886
|
const epoch = this.epoch;
|
|
24661
24887
|
const state = await this.settledState();
|
|
24662
24888
|
if (state.kind !== "ready") return null;
|
|
@@ -24669,12 +24895,11 @@ var init_memory_semantic = __esm({
|
|
|
24669
24895
|
const scored = [];
|
|
24670
24896
|
for (const id of candidates2) {
|
|
24671
24897
|
const e = vectors.get(id);
|
|
24672
|
-
if (
|
|
24673
|
-
const similarity = dot(q, e.vector);
|
|
24674
|
-
if (similarity >= embedder.minSimilarity) scored.push({ id, similarity });
|
|
24898
|
+
if (e) scored.push({ id, similarity: dot(q, e.vector) });
|
|
24675
24899
|
}
|
|
24676
24900
|
await this.writeChain;
|
|
24677
|
-
|
|
24901
|
+
scored.sort((a, b) => b.similarity - a.similarity || (a.id < b.id ? -1 : 1));
|
|
24902
|
+
return { similarities: scored, minSimilarity: embedder.minSimilarity };
|
|
24678
24903
|
} catch {
|
|
24679
24904
|
this.state = { kind: "off" };
|
|
24680
24905
|
this.options.log("memory: semantic search failed in the embedding runtime; using lexical search for the rest of this session");
|
package/npm-shrinkwrap.json
CHANGED
|
@@ -1,12 +1,12 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@squirrelnotes.app/mcp-embeddings",
|
|
3
|
-
"version": "2.1.1-test.
|
|
3
|
+
"version": "2.1.1-test.343",
|
|
4
4
|
"lockfileVersion": 3,
|
|
5
5
|
"requires": true,
|
|
6
6
|
"packages": {
|
|
7
7
|
"": {
|
|
8
8
|
"name": "@squirrelnotes.app/mcp-embeddings",
|
|
9
|
-
"version": "2.1.1-test.
|
|
9
|
+
"version": "2.1.1-test.343",
|
|
10
10
|
"license": "MIT",
|
|
11
11
|
"dependencies": {
|
|
12
12
|
"onnxruntime-node": "1.30.0"
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@squirrelnotes.app/mcp-embeddings",
|
|
3
|
-
"version": "2.1.1-test.
|
|
3
|
+
"version": "2.1.1-test.343",
|
|
4
4
|
"description": "Opt-in local semantic memory search for the Squirrel Notes MCP (stdio). The embedding model runs on your machine; no memory text is sent anywhere to be embedded.",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"repository": {
|
|
@@ -45,7 +45,7 @@
|
|
|
45
45
|
"onnxruntime-node": "1.30.0"
|
|
46
46
|
},
|
|
47
47
|
"devDependencies": {
|
|
48
|
-
"@modelcontextprotocol/sdk": "1.
|
|
48
|
+
"@modelcontextprotocol/sdk": "1.31.0",
|
|
49
49
|
"@types/jest": "^30.0.0",
|
|
50
50
|
"@types/node": "^20.0.0",
|
|
51
51
|
"esbuild": "^0.28.1",
|