mcp-memory-bucket 0.9.0 → 0.9.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
|
@@ -58,7 +58,16 @@
|
|
|
58
58
|
.desc { font-size: 12px; opacity: 0.8; margin-top: 3px; }
|
|
59
59
|
.tags { margin-top: 4px; display: flex; gap: 4px; flex-wrap: wrap; }
|
|
60
60
|
.tag { font-size: 10px; border: 1px solid var(--border-strong); border-radius: 999px; padding: 1px 6px; }
|
|
61
|
-
.type-badge {
|
|
61
|
+
.type-badge {
|
|
62
|
+
font-size: 10px; text-transform: uppercase; font-weight: 700; flex: 0 0 auto;
|
|
63
|
+
border-radius: 4px; padding: 1px 5px; display: inline-flex; align-items: center; gap: 3px;
|
|
64
|
+
}
|
|
65
|
+
.type-badge.skill {
|
|
66
|
+
border: 1px solid var(--purple); color: var(--purple-fg); background: var(--purple-tint);
|
|
67
|
+
}
|
|
68
|
+
.type-badge.memory {
|
|
69
|
+
border: 1px solid var(--accent); color: var(--accent); background: var(--accent-tint);
|
|
70
|
+
}
|
|
62
71
|
.folder-badge {
|
|
63
72
|
font-size: 10px; text-transform: uppercase; opacity: 0.9; border: 1px solid var(--purple);
|
|
64
73
|
color: var(--purple-fg); background: var(--purple-tint); border-radius: 4px; padding: 0 4px;
|
|
@@ -100,12 +109,16 @@
|
|
|
100
109
|
/>
|
|
101
110
|
<div class="row-body">
|
|
102
111
|
<div class="top">
|
|
103
|
-
<span class="name"
|
|
112
|
+
<span class="name">
|
|
113
|
+
<span class="type-badge ${e._table==="skills"?"skill":"memory"}"
|
|
114
|
+
>${e._table==="skills"?"⚡ skill":"📝 memory"}</span
|
|
115
|
+
>
|
|
116
|
+
${e.name}
|
|
117
|
+
</span>
|
|
104
118
|
<span class="meta">${bs(e.created_at)} · ${e.owner??"—"} · ${e.status}</span>
|
|
105
119
|
</div>
|
|
106
120
|
<div class="desc">${e.description}</div>
|
|
107
121
|
<div class="tags">
|
|
108
|
-
<span class="type-badge">${e._table==="skills"?"skill":"memory"}</span>
|
|
109
122
|
${this.showFolder&&e.folder?h`<span class="folder-badge">📁 ${e.folder}</span>`:""}
|
|
110
123
|
${e.deprecated?h`<span class="deprecated-badge">deprecated</span>`:""}
|
|
111
124
|
${e.paused?h`<span class="paused-badge">paused</span>`:""}
|
|
@@ -1745,7 +1758,7 @@ This removes the doc's key, tags, and status — it drops out of key-based looku
|
|
|
1745
1758
|
cursor: pointer;
|
|
1746
1759
|
}
|
|
1747
1760
|
.bulk-bar button.danger { border-color: var(--danger); color: var(--danger); }
|
|
1748
|
-
`}#e=ji();#t=new u(this.#e.query??"");#s=new u(this.#e.typeFilter??"all");#n=new u(this.#e.activeTags??[]);#r=new u(this.#e.activeFolders??[]);#g=new u([]);#a=new u(Di);#i=new u(null);#l=new u(Fi);#u=new u(!1);#h=new u("");#d=new u(!1);#o=new u(Mi());#f=new u("");#b=new u(!1);#y=new u(this.#e.sort??"
|
|
1761
|
+
`}#e=ji();#t=new u(this.#e.query??"");#s=new u(this.#e.typeFilter??"all");#n=new u(this.#e.activeTags??[]);#r=new u(this.#e.activeFolders??[]);#g=new u([]);#a=new u(Di);#i=new u(null);#l=new u(Fi);#u=new u(!1);#h=new u("");#d=new u(!1);#o=new u(Mi());#f=new u("");#b=new u(!1);#y=new u(this.#e.sort??"");#x=new u(this.#e.deprecatedFilter??"all");#k=new u(this.#e.pausedFilter??"all");#v=new u(this.#e.dateFrom??"");#m=new u(this.#e.dateTo??"");#p=new u(new Set);#$=new u(Ui());#w=new u(!1);#_=new u("entries");#z=new u([]);#A=new u(null);#R=new u(null);#T=new u(!1);#P=e=>this.#N(e);#L=()=>this.#q();constructor(){super(),new le(this),pt(this.#$.value)}#M(e){this.#$.set(e),localStorage.setItem(Ft,e),pt(e)}#I(){const e=fe[(fe.indexOf(this.#$.value)+1)%fe.length];this.#M(e)}async#O(){if(!this.#w.value){this.#w.set(!0);try{await fetch("/api/rebuild-cache",{method:"POST"}),await Promise.all([this.#C(),this.#S(),this.#c()])}finally{this.#w.set(!1)}}}#D(e){e.preventDefault(),this.#b.set(!0),document.addEventListener("pointermove",this.#P),document.addEventListener("pointerup",this.#L)}#N(e){const t=this.renderRoot.querySelector(".body-region");if(!t)return;const s=t.getBoundingClientRect(),i=(e.clientX-s.left)/s.width*100;this.#o.set(Math.min(Dt,Math.max(Ot,i)))}#q(){this.#b.set(!1),document.removeEventListener("pointermove",this.#P),document.removeEventListener("pointerup",this.#L),localStorage.setItem(It,String(this.#o.value))}#U=new fs(()=>{const e=new URLSearchParams;this.#s.value!=="all"&&e.set("type",this.#s.value),this.#t.value.trim()&&e.set("q",this.#t.value.trim());for(const t of this.#n.value)e.append("tag",t);for(const t of this.#r.value)e.append("folder",t);return this.#y.value&&e.set("sort",this.#y.value),this.#x.value==="hide"?e.set("deprecated","0"):this.#x.value==="only"&&e.set("deprecated","1"),this.#k.value==="hide"?e.set("paused","0"):this.#k.value==="only"&&e.set("paused","1"),this.#v.value&&e.set("date_from",this.#v.value),this.#m.value&&e.set("date_to",this.#m.value),e.toString()});#E=()=>this.#Q();#F=e=>this.#ie(e);#K=e=>this.#re(e);#Z=()=>this.#V();#W;connectedCallback(){super.connectedCallback(),this.#c(),this.#S(),this.#C().then(()=>this.#Q()),window.addEventListener("focus",this.#E),document.addEventListener("visibilitychange",this.#E),document.addEventListener("folderfoo-file-open",this.#F),document.addEventListener("folderfoo-folder-changed",this.#K),window.addEventListener("folderfoo-auth-change",this.#Z),this.#ne(),this.#V(),Ii(),this.#W=Lt.subscribe(()=>this.#C())}async#ie(e){const t=e.detail?.name;if(!t)return;const s=Li(t);if(s.owner){this.#f.set(`"${s.name}" is from a shared folder — opening shared files here isn't supported yet.`);return}const{folderfooMode:i,folderfooHost:n}=await W();if(!(i==="off"||!n))try{const o=await fetch("/api/folderfoo/resolve-open",{method:"POST",headers:{"Content-Type":"application/json"},body:JSON.stringify({server:n,tenantId:ne,folderPath:s.folderPath,name:s.name})}),a=await o.json();if(!o.ok){this.#f.set(a.error??`couldn't open "${s.name}"`);return}this.#f.set(""),this.#i.set({table:a.table,id:a.id})}catch(o){this.#f.set(o.message)}}async#re(e){const{path:t,action:s,newPath:i}=e.detail??{};if(!(t===void 0||!s)){if((s==="rename"||s==="move")&&i){const{folderfooHost:n}=await W();if(!n)return;try{await fetch("/api/folders/renamed",{method:"POST",headers:{"Content-Type":"application/json"},body:JSON.stringify({server:n,oldPath:t,newPath:i})})}catch{}}this.#C()}}async#ne(){if(this.shadowRoot?.querySelector("folderfoo-profile-circle"))return;const{folderfooMode:e,folderfooHost:t}=await W();e==="off"||!t||ut(()=>import(`${t}/elements/folderfoo-profile-circle.js`),[]).then(async()=>{await this.updateComplete;const s=document.createElement("folderfoo-profile-circle");s.setAttribute("app-name","Memory Bucket"),s.setAttribute("tenant-id",ne),s.style.zoom="0.875",this.shadowRoot?.querySelector(".folderfoo-slot")?.appendChild(s)}).catch(s=>{console.warn("folderfoo-profile-circle unavailable:",s.message)})}disconnectedCallback(){super.disconnectedCallback(),window.removeEventListener("focus",this.#E),document.removeEventListener("visibilitychange",this.#E),document.removeEventListener("folderfoo-file-open",this.#F),document.removeEventListener("folderfoo-folder-changed",this.#K),window.removeEventListener("folderfoo-auth-change",this.#Z),this.#W?.()}async#V(){let e=null;try{e=localStorage.getItem("folderfoo_token")}catch{return}if(!e){fetch("/api/folderfoo/logout",{method:"POST"}).catch(()=>{});return}const{folderfooMode:t,folderfooHost:s}=await W();t==="off"||!s||fetch("/api/folderfoo/login",{method:"POST",headers:{"Content-Type":"application/json"},body:JSON.stringify({server:s,token:e})}).catch(()=>{})}#j=!1;async#Q(){if(document.visibilityState==="visible"){if(this.#X().some(e=>e.remote)&&!this.#j){this.#j=!0;try{await fetch("/api/remote-folders/resync-all",{method:"POST"})}catch{}finally{this.#j=!1}}this.#c(),this.#S(),this.#C(),this.renderRoot.querySelector("detail-panel")?.refresh?.()}}async#c(){const e=this.#U.value;history.replaceState(null,"",e?`#${e}`:location.pathname+location.search);const t=await fetch(`/api/entries?${e}`);this.#g.set(await t.json()),this.renderRoot.querySelector("result-list")?.scrollTo(0,0)}async#S(){const e=this.#s.value==="all"?"":`?type=${this.#s.value}`,t=await fetch(`/api/facets${e}`);this.#a.set(await t.json())}async#C(){const t=await(await fetch("/api/folders")).json();this.#l.set(t),this.#d.set(!0);const s=new Set([...t.skill,...t.memory].map(n=>n.name)),i=this.#r.value.filter(n=>s.has(n));i.length!==this.#r.value.length&&(this.#r.set(i),this.#c())}#oe(e){this.#_.set(e),e==="channels"&&this.#J()}async#J(){const e=await fetch("/api/channels");this.#z.set(await e.json()),this.#A.value&&this.#Y(this.#A.value)}async#Y(e){this.#A.set(e),this.#T.set(!0);try{const t=await fetch(`/api/channels/${encodeURIComponent(e)}`);this.#R.set(t.ok?await t.json():null)}finally{this.#T.set(!1)}}#ae(e){const t=this.#r.value;this.#r.set(t.includes(e)?t.filter(s=>s!==e):[...t,e]),this.#c()}async#le(e,t){if(t.stopPropagation(),window.prompt(`Type "${e.name}" to remove this ${e.kind} folder (files on disk are untouched):`)===e.name){this.#h.set(e.name);try{await fetch(`/api/folders/${e.kind}/${encodeURIComponent(e.name)}`,{method:"DELETE"}),this.#r.set(this.#r.value.filter(i=>i!==e.name)),await Promise.all([this.#C(),this.#S(),this.#c()])}finally{this.#h.set("")}}}#G(){this.#u.set(!1),this.#C(),this.#S(),this.#c()}#X(){return[...this.#l.value.skill,...this.#l.value.memory].sort((e,t)=>Number(e.remote)-Number(t.remote))}#ce(e){this.#s.set(e),this.#n.set([]),this.#S(),this.#c()}#he(e){const t=this.#n.value;this.#n.set(t.includes(e)?t.filter(s=>s!==e):[...t,e]),this.#c()}#de(e){this.#n.value.includes(e)||this.#n.set([...this.#n.value,e]),this.#c()}#pe(e){this.#r.value.includes(e)||this.#r.set([...this.#r.value,e]),this.#c()}#ue(e){this.#v.set(e),this.#m.set(e),this.#c()}#fe(e){this.#t.set(e.target.value),this.#c()}#ee(e){this.#t.set(e),this.#c()}#ge(){this.#t.set(""),this.#c()}#be(e){this.#i.set({table:e._table,id:e.id})}#me(e){this.#y.set(e.target.value),this.#c()}#xe(e){this.#x.set(e.target.value),this.#c()}#ve(e){this.#k.set(e.target.value),this.#c()}#ke(e){this.#v.set(e.target.value),this.#c()}#ye(e){this.#m.set(e.target.value),this.#c()}#$e(){this.#v.set(""),this.#m.set(""),this.#c()}#we(e){const t=new Set(this.#p.value);t.has(e.id)?t.delete(e.id):t.add(e.id),this.#p.set(t)}#_e(){return new Map(this.#g.value.map(e=>[e.id,e]))}#H(){const e=new Map,t=this.#_e();for(const s of this.#p.value){const i=t.get(s);if(!i)continue;const n=e.get(i._table)??[];n.push(s),e.set(i._table,n)}return e}async#te(e){const t=this.#H();await Promise.all([...t.entries()].map(([s,i])=>fetch(`/api/entries/${s}/bulk/deprecated`,{method:"POST",headers:{"Content-Type":"application/json"},body:JSON.stringify({ids:i,deprecated:e})}))),this.#p.set(new Set),await this.#c()}async#se(e){const t=this.#H();await Promise.all([...t.entries()].map(([s,i])=>fetch(`/api/entries/${s}/bulk/paused`,{method:"POST",headers:{"Content-Type":"application/json"},body:JSON.stringify({ids:i,paused:e})}))),this.#p.set(new Set),await this.#c()}async#Se(){if(!window.confirm(`Delete ${this.#p.value.size} doc(s)? This can't be undone.`))return;const e=this.#H();await Promise.all([...e.entries()].map(([t,s])=>fetch(`/api/entries/${t}/bulk/delete`,{method:"POST",headers:{"Content-Type":"application/json"},body:JSON.stringify({ids:s})}))),this.#p.set(new Set),await this.#c()}#Ce(){const e=this.#$.value;return h`
|
|
1749
1762
|
<button
|
|
1750
1763
|
class="theme-toggle"
|
|
1751
1764
|
title=${`Theme: ${qi[e]} (click to change)`}
|
|
@@ -1900,7 +1913,11 @@ This removes the doc's key, tags, and status — it drops out of key-based looku
|
|
|
1900
1913
|
`:""}
|
|
1901
1914
|
</div>
|
|
1902
1915
|
<span class="filter-label">Sort:</span>
|
|
1903
|
-
<select
|
|
1916
|
+
<select
|
|
1917
|
+
.value=${this.#y.value||(this.#t.value.trim()?"relevance":"mtime_desc")}
|
|
1918
|
+
@change=${s=>this.#me(s)}
|
|
1919
|
+
>
|
|
1920
|
+
${this.#t.value.trim()?h`<option value="relevance">Relevance</option>`:""}
|
|
1904
1921
|
<option value="mtime_desc">Recently touched</option>
|
|
1905
1922
|
<option value="mtime_asc">Least recently touched</option>
|
|
1906
1923
|
<option value="created_at_asc">Oldest first</option>
|
package/dist/client/index.html
CHANGED
package/dist/src/web/routes.js
CHANGED
|
@@ -47,7 +47,9 @@ function queryEntries(db, memoryRepo, req) {
|
|
|
47
47
|
const keyMatch = q ? memoryRepo.suggestKeys(q, 1).find((m) => stripKey(m.key) === stripKey(q)) : undefined;
|
|
48
48
|
// Always compute FTS matches when q is set, even on a keyMatch — skills still need this to stay
|
|
49
49
|
// filtered by q (they have no key concept, so keyMatch only bypasses filtering for memory_docs below).
|
|
50
|
-
const matchedIds = q
|
|
50
|
+
const matchedIds = q
|
|
51
|
+
? matchSearch(db, q)
|
|
52
|
+
: null;
|
|
51
53
|
if (q && !keyMatch && matchedIds && matchedIds.skills.size === 0 && matchedIds.memory_docs.size === 0) {
|
|
52
54
|
return [];
|
|
53
55
|
}
|
|
@@ -62,8 +64,20 @@ function queryEntries(db, memoryRepo, req) {
|
|
|
62
64
|
if (type === 'memory' || type === 'all') {
|
|
63
65
|
results.push(...queryTable(db, 'memory_docs', { tags, statuses, owners: [], docTypes, keyTypes, folders, deprecated, paused, keys: keyMatch ? [keyMatch.key] : [] }, keyMatch ? undefined : intersectIds(matchedIds?.memory_docs, dateIds?.memory_docs)));
|
|
64
66
|
}
|
|
65
|
-
|
|
67
|
+
// Default sort is relevance (bm25) when a search query is active — otherwise a good match with
|
|
68
|
+
// an old mtime (e.g. "grilling") can sink below unrelated recently-touched entries and look like
|
|
69
|
+
// it's missing. Falls back to mtime_desc when there's no query to rank by. An explicit `sort`
|
|
70
|
+
// param always wins over this default.
|
|
71
|
+
const sort = req.query.sort ?? (q ? 'relevance' : 'mtime_desc');
|
|
72
|
+
const rankOf = (r) => matchedIds?.rank.get(r._table === 'skills' ? skillKey(r.folder, r.id) : r.id);
|
|
66
73
|
results.sort((a, b) => {
|
|
74
|
+
if (sort === 'relevance') {
|
|
75
|
+
const ra = rankOf(a);
|
|
76
|
+
const rb = rankOf(b);
|
|
77
|
+
if (ra !== undefined && rb !== undefined && ra !== rb)
|
|
78
|
+
return ra - rb; // bm25: lower is better
|
|
79
|
+
return b.mtime_ms - a.mtime_ms; // tie or no-rank (e.g. keyMatch-only memory_docs): fall back to mtime
|
|
80
|
+
}
|
|
67
81
|
if (sort === 'mtime_asc')
|
|
68
82
|
return a.mtime_ms - b.mtime_ms;
|
|
69
83
|
if (sort === 'name_asc')
|
|
@@ -126,7 +140,10 @@ function queryTable(db, table, filters, restrictToIds) {
|
|
|
126
140
|
// `id IN (...)` could match the wrong folder's same-named skill once names are folder-scoped.
|
|
127
141
|
// memory_docs restricts on plain id, unchanged (its ids never collide across folders).
|
|
128
142
|
if (table === 'skills') {
|
|
129
|
-
|
|
143
|
+
// SKILL_KEY_SEP is a NUL byte, which can't be embedded in a single-quoted SQL string literal
|
|
144
|
+
// (SQLite's tokenizer rejects it) — bind it as a parameter instead of interpolating it into the query text.
|
|
145
|
+
where += ` AND (folder || ? || id) IN (${[...restrictToIds].map(() => '?').join(', ')})`;
|
|
146
|
+
params.push(SKILL_KEY_SEP);
|
|
130
147
|
}
|
|
131
148
|
else {
|
|
132
149
|
where += ` AND id IN (${[...restrictToIds].map(() => '?').join(', ')})`;
|
|
@@ -216,27 +233,34 @@ function matchDateRange(db, from, to) {
|
|
|
216
233
|
}
|
|
217
234
|
return { skills, memory_docs };
|
|
218
235
|
}
|
|
219
|
-
/**
|
|
236
|
+
/**
|
|
237
|
+
* Runs the FTS5 query once, buckets matching ids by source table, and records each match's bm25
|
|
238
|
+
* rank (lower = more relevant) keyed the same way restrictToIds is (skillKey for skills, plain id
|
|
239
|
+
* for memory_docs) — used to sort search results by relevance instead of mtime by default.
|
|
240
|
+
*/
|
|
220
241
|
function matchSearch(db, q) {
|
|
221
242
|
const skills = new Set();
|
|
222
243
|
const memory_docs = new Set();
|
|
244
|
+
const rank = new Map();
|
|
223
245
|
let rows;
|
|
224
246
|
try {
|
|
225
247
|
rows = db
|
|
226
|
-
.prepare(`SELECT ref_table, ref_id, ref_folder FROM search_index WHERE search_index MATCH ? ORDER BY rank`)
|
|
248
|
+
.prepare(`SELECT ref_table, ref_id, ref_folder, bm25(search_index) AS score FROM search_index WHERE search_index MATCH ? ORDER BY rank`)
|
|
227
249
|
.all(sanitizeFtsQuery(q));
|
|
228
250
|
}
|
|
229
251
|
catch {
|
|
230
252
|
// Bad FTS5 query syntax (e.g. a bare quote) — treat as no matches rather than 500ing.
|
|
231
|
-
return { skills, memory_docs };
|
|
253
|
+
return { skills, memory_docs, rank };
|
|
232
254
|
}
|
|
233
255
|
for (const row of rows) {
|
|
256
|
+
const key = row.ref_table === 'skills' ? skillKey(row.ref_folder, row.ref_id) : row.ref_id;
|
|
234
257
|
if (row.ref_table === 'skills')
|
|
235
|
-
skills.add(
|
|
258
|
+
skills.add(key);
|
|
236
259
|
else
|
|
237
|
-
memory_docs.add(
|
|
260
|
+
memory_docs.add(key);
|
|
261
|
+
rank.set(key, row.score); // bm25 is lower-is-better; stored as-is, compared ascending below
|
|
238
262
|
}
|
|
239
|
-
return { skills, memory_docs };
|
|
263
|
+
return { skills, memory_docs, rank };
|
|
240
264
|
}
|
|
241
265
|
function buildFacets(db, type) {
|
|
242
266
|
const tags = new Set();
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "mcp-memory-bucket",
|
|
3
|
-
"version": "0.9.
|
|
3
|
+
"version": "0.9.1",
|
|
4
4
|
"type": "module",
|
|
5
5
|
"description": "MCP server exposing skill_* (reusable coding patterns) and memory_* (point-in-time working context) tools over a markdown+frontmatter source, cached into SQLite at runtime.",
|
|
6
6
|
"repository": {
|