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 { font-size: 10px; text-transform: uppercase; opacity: 0.6; }
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">${e.name}</span>
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??"mtime_desc");#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!=="mtime_desc"&&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`
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 .value=${this.#y.value} @change=${s=>this.#me(s)}>
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>
@@ -33,7 +33,7 @@
33
33
  z-index: 2147483000;
34
34
  }
35
35
  </style>
36
- <script type="module" crossorigin src="/assets/index-D-Xr5fx5.js"></script>
36
+ <script type="module" crossorigin src="/assets/index-C9GF1GPZ.js"></script>
37
37
  </head>
38
38
  <body>
39
39
  <mem-bucket-app></mem-bucket-app>
@@ -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 ? matchSearch(db, q) : null;
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
- const sort = req.query.sort ?? 'mtime_desc';
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
- where += ` AND (folder || '${SKILL_KEY_SEP}' || id) IN (${[...restrictToIds].map(() => '?').join(', ')})`;
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
- /** Runs the FTS5 query once and buckets matching ids by source table. */
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(skillKey(row.ref_folder, row.ref_id));
258
+ skills.add(key);
236
259
  else
237
- memory_docs.add(row.ref_id);
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.0",
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": {