mcp-memory-bucket 0.1.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/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 Anatoli Radulov
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
package/README.md ADDED
@@ -0,0 +1,76 @@
1
+ # mcp-memory-bucket
2
+
3
+ MCP server exposing `skill_*` (reusable coding patterns, stored as
4
+ [agentskills.io](https://agentskills.io)-standard `SKILL.md` folders) and
5
+ `memory_*` (point-in-time working context — plans, specs, SQL, session
6
+ summaries) tools over markdown+frontmatter files, cached into SQLite at
7
+ runtime.
8
+
9
+ See [AGENTS.md](./AGENTS.md) for the frontmatter schemas, tool reference,
10
+ and how an agent should use this — the same content is also published as
11
+ the `memory-bucket-authoring` skill
12
+ (`src/skills/builtin/memory-bucket-authoring/SKILL.md`), built in so it's
13
+ always available regardless of `--memory-dir`/cwd, fetchable via
14
+ `skill_get("memory-bucket-authoring")` from any MCP session connected to
15
+ this server. See
16
+ [skill-bucket-v0-plan.md](../../skill-bucket-v0-plan.md) at the workspace
17
+ root for the full design plan.
18
+
19
+ ## Run
20
+
21
+ ```sh
22
+ npm install
23
+ npm run build
24
+ npm start # or: npm run dev for auto-restart on source changes
25
+ ```
26
+
27
+ Starts a stateless StreamableHTTP MCP server at `http://localhost:8767/mcp`
28
+ (override with `PORT`). This is a long-lived process, not a one-shot CLI —
29
+ the SQLite cache is kept current by a file watcher for as long as the
30
+ server runs.
31
+
32
+ The same process also serves a read-only browser UI at
33
+ `http://localhost:8767/` for searching/filtering skills and memory docs by
34
+ tag, status, owner, and fulltext (SQLite FTS5) — a way to review what's in
35
+ the index without going through an agent. It has no write access; all
36
+ edits still go through the `skill_*`/`memory_*` tools or the files
37
+ directly. From an MCP session connected to this server, call
38
+ `bucket_open_ui` to get the URL. The UI is a Lit + `avosignals` app built
39
+ with Vite (`src/client/`, bundled to `dist/client/`) — `npm run build`
40
+ builds it (along with the server); `npm start` does **not** rebuild it, so
41
+ run `npm run build` again after changing anything under `src/client/`.
42
+ `npm run dev` rebuilds the client on change alongside the server, for active
43
+ UI development.
44
+
45
+ ### Configuration
46
+
47
+ By default the server uses the current working directory as the base for
48
+ memory/skill sources. Override that with one of:
49
+
50
+ ```json
51
+ {
52
+ "skill_sources": ["./skills"],
53
+ "memory_sources": ["./docs"]
54
+ }
55
+ ```
56
+
57
+ Paths are resolved relative to the working directory. Defaults match the
58
+ example above if no `skill_sources`/`memory_sources` key is present.
59
+
60
+ - the `MEMORY_BUCKET_DIR` environment variable, or the `--memory-dir <path>`
61
+ CLI flag — either overrides the base directory that the (still-defaultable)
62
+ `skill_sources`/`memory_sources` are resolved against.
63
+
64
+ ```sh
65
+ npm start -- --memory-dir /path/to/other/dir
66
+ # or: MEMORY_BUCKET_DIR=/path/to/other/dir npm start
67
+ ```
68
+
69
+ Note the `--` before `--memory-dir` — without it, npm swallows the flag
70
+ itself instead of passing it through to the script.
71
+
72
+ ## Test
73
+
74
+ ```sh
75
+ npm test
76
+ ```
@@ -0,0 +1,2 @@
1
+ #!/usr/bin/env node
2
+ import '../dist/src/server.js';
@@ -0,0 +1,150 @@
1
+ (function(){const t=document.createElement("link").relList;if(t&&t.supports&&t.supports("modulepreload"))return;for(const i of document.querySelectorAll('link[rel="modulepreload"]'))s(i);new MutationObserver(i=>{for(const o of i)if(o.type==="childList")for(const n of o.addedNodes)n.tagName==="LINK"&&n.rel==="modulepreload"&&s(n)}).observe(document,{childList:!0,subtree:!0});function e(i){const o={};return i.integrity&&(o.integrity=i.integrity),i.referrerPolicy&&(o.referrerPolicy=i.referrerPolicy),i.crossOrigin==="use-credentials"?o.credentials="include":i.crossOrigin==="anonymous"?o.credentials="omit":o.credentials="same-origin",o}function s(i){if(i.ep)return;i.ep=!0;const o=e(i);fetch(i.href,o)}})();/**
2
+ * @license
3
+ * Copyright 2019 Google LLC
4
+ * SPDX-License-Identifier: BSD-3-Clause
5
+ */const T=globalThis,L=T.ShadowRoot&&(T.ShadyCSS===void 0||T.ShadyCSS.nativeShadow)&&"adoptedStyleSheets"in Document.prototype&&"replace"in CSSStyleSheet.prototype,D=Symbol(),F=new WeakMap;let rt=class{constructor(t,e,s){if(this._$cssResult$=!0,s!==D)throw Error("CSSResult is not constructable. Use `unsafeCSS` or `css` instead.");this.cssText=t,this.t=e}get styleSheet(){let t=this.o;const e=this.t;if(L&&t===void 0){const s=e!==void 0&&e.length===1;s&&(t=F.get(e)),t===void 0&&((this.o=t=new CSSStyleSheet).replaceSync(this.cssText),s&&F.set(e,t))}return t}toString(){return this.cssText}};const pt=r=>new rt(typeof r=="string"?r:r+"",void 0,D),I=(r,...t)=>{const e=r.length===1?r[0]:t.reduce((s,i,o)=>s+(n=>{if(n._$cssResult$===!0)return n.cssText;if(typeof n=="number")return n;throw Error("Value passed to 'css' function must be a 'css' function result: "+n+". Use 'unsafeCSS' to pass non-literal values, but take care to ensure page security.")})(i)+r[o+1],r[0]);return new rt(e,r,D)},dt=(r,t)=>{if(L)r.adoptedStyleSheets=t.map(e=>e instanceof CSSStyleSheet?e:e.styleSheet);else for(const e of t){const s=document.createElement("style"),i=T.litNonce;i!==void 0&&s.setAttribute("nonce",i),s.textContent=e.cssText,r.appendChild(s)}},K=L?r=>r:r=>r instanceof CSSStyleSheet?(t=>{let e="";for(const s of t.cssRules)e+=s.cssText;return pt(e)})(r):r;/**
6
+ * @license
7
+ * Copyright 2017 Google LLC
8
+ * SPDX-License-Identifier: BSD-3-Clause
9
+ */const{is:ut,defineProperty:ft,getOwnPropertyDescriptor:$t,getOwnPropertyNames:gt,getOwnPropertySymbols:_t,getPrototypeOf:mt}=Object,N=globalThis,Z=N.trustedTypes,yt=Z?Z.emptyScript:"",vt=N.reactiveElementPolyfillSupport,E=(r,t)=>r,j={toAttribute(r,t){switch(t){case Boolean:r=r?yt:null;break;case Object:case Array:r=r==null?r:JSON.stringify(r)}return r},fromAttribute(r,t){let e=r;switch(t){case Boolean:e=r!==null;break;case Number:e=r===null?null:Number(r);break;case Object:case Array:try{e=JSON.parse(r)}catch{e=null}}return e}},ot=(r,t)=>!ut(r,t),J={attribute:!0,type:String,converter:j,reflect:!1,useDefault:!1,hasChanged:ot};Symbol.metadata??=Symbol("metadata"),N.litPropertyMetadata??=new WeakMap;let A=class extends HTMLElement{static addInitializer(t){this._$Ei(),(this.l??=[]).push(t)}static get observedAttributes(){return this.finalize(),this._$Eh&&[...this._$Eh.keys()]}static createProperty(t,e=J){if(e.state&&(e.attribute=!1),this._$Ei(),this.prototype.hasOwnProperty(t)&&((e=Object.create(e)).wrapped=!0),this.elementProperties.set(t,e),!e.noAccessor){const s=Symbol(),i=this.getPropertyDescriptor(t,s,e);i!==void 0&&ft(this.prototype,t,i)}}static getPropertyDescriptor(t,e,s){const{get:i,set:o}=$t(this.prototype,t)??{get(){return this[e]},set(n){this[e]=n}};return{get:i,set(n){const h=i?.call(this);o?.call(this,n),this.requestUpdate(t,h,s)},configurable:!0,enumerable:!0}}static getPropertyOptions(t){return this.elementProperties.get(t)??J}static _$Ei(){if(this.hasOwnProperty(E("elementProperties")))return;const t=mt(this);t.finalize(),t.l!==void 0&&(this.l=[...t.l]),this.elementProperties=new Map(t.elementProperties)}static finalize(){if(this.hasOwnProperty(E("finalized")))return;if(this.finalized=!0,this._$Ei(),this.hasOwnProperty(E("properties"))){const e=this.properties,s=[...gt(e),..._t(e)];for(const i of s)this.createProperty(i,e[i])}const t=this[Symbol.metadata];if(t!==null){const e=litPropertyMetadata.get(t);if(e!==void 0)for(const[s,i]of e)this.elementProperties.set(s,i)}this._$Eh=new Map;for(const[e,s]of this.elementProperties){const i=this._$Eu(e,s);i!==void 0&&this._$Eh.set(i,e)}this.elementStyles=this.finalizeStyles(this.styles)}static finalizeStyles(t){const e=[];if(Array.isArray(t)){const s=new Set(t.flat(1/0).reverse());for(const i of s)e.unshift(K(i))}else t!==void 0&&e.push(K(t));return e}static _$Eu(t,e){const s=e.attribute;return s===!1?void 0:typeof s=="string"?s:typeof t=="string"?t.toLowerCase():void 0}constructor(){super(),this._$Ep=void 0,this.isUpdatePending=!1,this.hasUpdated=!1,this._$Em=null,this._$Ev()}_$Ev(){this._$ES=new Promise(t=>this.enableUpdating=t),this._$AL=new Map,this._$E_(),this.requestUpdate(),this.constructor.l?.forEach(t=>t(this))}addController(t){(this._$EO??=new Set).add(t),this.renderRoot!==void 0&&this.isConnected&&t.hostConnected?.()}removeController(t){this._$EO?.delete(t)}_$E_(){const t=new Map,e=this.constructor.elementProperties;for(const s of e.keys())this.hasOwnProperty(s)&&(t.set(s,this[s]),delete this[s]);t.size>0&&(this._$Ep=t)}createRenderRoot(){const t=this.shadowRoot??this.attachShadow(this.constructor.shadowRootOptions);return dt(t,this.constructor.elementStyles),t}connectedCallback(){this.renderRoot??=this.createRenderRoot(),this.enableUpdating(!0),this._$EO?.forEach(t=>t.hostConnected?.())}enableUpdating(t){}disconnectedCallback(){this._$EO?.forEach(t=>t.hostDisconnected?.())}attributeChangedCallback(t,e,s){this._$AK(t,s)}_$ET(t,e){const s=this.constructor.elementProperties.get(t),i=this.constructor._$Eu(t,s);if(i!==void 0&&s.reflect===!0){const o=(s.converter?.toAttribute!==void 0?s.converter:j).toAttribute(e,s.type);this._$Em=t,o==null?this.removeAttribute(i):this.setAttribute(i,o),this._$Em=null}}_$AK(t,e){const s=this.constructor,i=s._$Eh.get(t);if(i!==void 0&&this._$Em!==i){const o=s.getPropertyOptions(i),n=typeof o.converter=="function"?{fromAttribute:o.converter}:o.converter?.fromAttribute!==void 0?o.converter:j;this._$Em=i;const h=n.fromAttribute(e,o.type);this[i]=h??this._$Ej?.get(i)??h,this._$Em=null}}requestUpdate(t,e,s,i=!1,o){if(t!==void 0){const n=this.constructor;if(i===!1&&(o=this[t]),s??=n.getPropertyOptions(t),!((s.hasChanged??ot)(o,e)||s.useDefault&&s.reflect&&o===this._$Ej?.get(t)&&!this.hasAttribute(n._$Eu(t,s))))return;this.C(t,e,s)}this.isUpdatePending===!1&&(this._$ES=this._$EP())}C(t,e,{useDefault:s,reflect:i,wrapped:o},n){s&&!(this._$Ej??=new Map).has(t)&&(this._$Ej.set(t,n??e??this[t]),o!==!0||n!==void 0)||(this._$AL.has(t)||(this.hasUpdated||s||(e=void 0),this._$AL.set(t,e)),i===!0&&this._$Em!==t&&(this._$Eq??=new Set).add(t))}async _$EP(){this.isUpdatePending=!0;try{await this._$ES}catch(e){Promise.reject(e)}const t=this.scheduleUpdate();return t!=null&&await t,!this.isUpdatePending}scheduleUpdate(){return this.performUpdate()}performUpdate(){if(!this.isUpdatePending)return;if(!this.hasUpdated){if(this.renderRoot??=this.createRenderRoot(),this._$Ep){for(const[i,o]of this._$Ep)this[i]=o;this._$Ep=void 0}const s=this.constructor.elementProperties;if(s.size>0)for(const[i,o]of s){const{wrapped:n}=o,h=this[i];n!==!0||this._$AL.has(i)||h===void 0||this.C(i,void 0,o,h)}}let t=!1;const e=this._$AL;try{t=this.shouldUpdate(e),t?(this.willUpdate(e),this._$EO?.forEach(s=>s.hostUpdate?.()),this.update(e)):this._$EM()}catch(s){throw t=!1,this._$EM(),s}t&&this._$AE(e)}willUpdate(t){}_$AE(t){this._$EO?.forEach(e=>e.hostUpdated?.()),this.hasUpdated||(this.hasUpdated=!0,this.firstUpdated(t)),this.updated(t)}_$EM(){this._$AL=new Map,this.isUpdatePending=!1}get updateComplete(){return this.getUpdateComplete()}getUpdateComplete(){return this._$ES}shouldUpdate(t){return!0}update(t){this._$Eq&&=this._$Eq.forEach(e=>this._$ET(e,this[e])),this._$EM()}updated(t){}firstUpdated(t){}};A.elementStyles=[],A.shadowRootOptions={mode:"open"},A[E("elementProperties")]=new Map,A[E("finalized")]=new Map,vt?.({ReactiveElement:A}),(N.reactiveElementVersions??=[]).push("2.1.2");/**
10
+ * @license
11
+ * Copyright 2017 Google LLC
12
+ * SPDX-License-Identifier: BSD-3-Clause
13
+ */const B=globalThis,Q=r=>r,M=B.trustedTypes,Y=M?M.createPolicy("lit-html",{createHTML:r=>r}):void 0,nt="$lit$",_=`lit$${Math.random().toFixed(9).slice(2)}$`,at="?"+_,bt=`<${at}>`,b=document,C=()=>b.createComment(""),P=r=>r===null||typeof r!="object"&&typeof r!="function",q=Array.isArray,At=r=>q(r)||typeof r?.[Symbol.iterator]=="function",R=`[
14
+ \f\r]`,S=/<(?:(!--|\/[^a-zA-Z])|(\/?[a-zA-Z][^>\s]*)|(\/?$))/g,G=/-->/g,X=/>/g,m=RegExp(`>|${R}(?:([^\\s"'>=/]+)(${R}*=${R}*(?:[^
15
+ \f\r"'\`<>=]|("|')|))|$)`,"g"),tt=/'/g,et=/"/g,lt=/^(?:script|style|textarea|title)$/i,wt=r=>(t,...e)=>({_$litType$:r,strings:t,values:e}),f=wt(1),w=Symbol.for("lit-noChange"),c=Symbol.for("lit-nothing"),st=new WeakMap,y=b.createTreeWalker(b,129);function ht(r,t){if(!q(r)||!r.hasOwnProperty("raw"))throw Error("invalid template strings array");return Y!==void 0?Y.createHTML(t):t}const xt=(r,t)=>{const e=r.length-1,s=[];let i,o=t===2?"<svg>":t===3?"<math>":"",n=S;for(let h=0;h<e;h++){const a=r[h];let p,d,l=-1,$=0;for(;$<a.length&&(n.lastIndex=$,d=n.exec(a),d!==null);)$=n.lastIndex,n===S?d[1]==="!--"?n=G:d[1]!==void 0?n=X:d[2]!==void 0?(lt.test(d[2])&&(i=RegExp("</"+d[2],"g")),n=m):d[3]!==void 0&&(n=m):n===m?d[0]===">"?(n=i??S,l=-1):d[1]===void 0?l=-2:(l=n.lastIndex-d[2].length,p=d[1],n=d[3]===void 0?m:d[3]==='"'?et:tt):n===et||n===tt?n=m:n===G||n===X?n=S:(n=m,i=void 0);const g=n===m&&r[h+1].startsWith("/>")?" ":"";o+=n===S?a+bt:l>=0?(s.push(p),a.slice(0,l)+nt+a.slice(l)+_+g):a+_+(l===-2?h:g)}return[ht(r,o+(r[e]||"<?>")+(t===2?"</svg>":t===3?"</math>":"")),s]};class k{constructor({strings:t,_$litType$:e},s){let i;this.parts=[];let o=0,n=0;const h=t.length-1,a=this.parts,[p,d]=xt(t,e);if(this.el=k.createElement(p,s),y.currentNode=this.el.content,e===2||e===3){const l=this.el.content.firstChild;l.replaceWith(...l.childNodes)}for(;(i=y.nextNode())!==null&&a.length<h;){if(i.nodeType===1){if(i.hasAttributes())for(const l of i.getAttributeNames())if(l.endsWith(nt)){const $=d[n++],g=i.getAttribute(l).split(_),O=/([.?@])?(.*)/.exec($);a.push({type:1,index:o,name:O[2],strings:g,ctor:O[1]==="."?Et:O[1]==="?"?Ct:O[1]==="@"?Pt:H}),i.removeAttribute(l)}else l.startsWith(_)&&(a.push({type:6,index:o}),i.removeAttribute(l));if(lt.test(i.tagName)){const l=i.textContent.split(_),$=l.length-1;if($>0){i.textContent=M?M.emptyScript:"";for(let g=0;g<$;g++)i.append(l[g],C()),y.nextNode(),a.push({type:2,index:++o});i.append(l[$],C())}}}else if(i.nodeType===8)if(i.data===at)a.push({type:2,index:o});else{let l=-1;for(;(l=i.data.indexOf(_,l+1))!==-1;)a.push({type:7,index:o}),l+=_.length-1}o++}}static createElement(t,e){const s=b.createElement("template");return s.innerHTML=t,s}}function x(r,t,e=r,s){if(t===w)return t;let i=s!==void 0?e._$Co?.[s]:e._$Cl;const o=P(t)?void 0:t._$litDirective$;return i?.constructor!==o&&(i?._$AO?.(!1),o===void 0?i=void 0:(i=new o(r),i._$AT(r,e,s)),s!==void 0?(e._$Co??=[])[s]=i:e._$Cl=i),i!==void 0&&(t=x(r,i._$AS(r,t.values),i,s)),t}class St{constructor(t,e){this._$AV=[],this._$AN=void 0,this._$AD=t,this._$AM=e}get parentNode(){return this._$AM.parentNode}get _$AU(){return this._$AM._$AU}u(t){const{el:{content:e},parts:s}=this._$AD,i=(t?.creationScope??b).importNode(e,!0);y.currentNode=i;let o=y.nextNode(),n=0,h=0,a=s[0];for(;a!==void 0;){if(n===a.index){let p;a.type===2?p=new U(o,o.nextSibling,this,t):a.type===1?p=new a.ctor(o,a.name,a.strings,this,t):a.type===6&&(p=new kt(o,this,t)),this._$AV.push(p),a=s[++h]}n!==a?.index&&(o=y.nextNode(),n++)}return y.currentNode=b,i}p(t){let e=0;for(const s of this._$AV)s!==void 0&&(s.strings!==void 0?(s._$AI(t,s,e),e+=s.strings.length-2):s._$AI(t[e])),e++}}class U{get _$AU(){return this._$AM?._$AU??this._$Cv}constructor(t,e,s,i){this.type=2,this._$AH=c,this._$AN=void 0,this._$AA=t,this._$AB=e,this._$AM=s,this.options=i,this._$Cv=i?.isConnected??!0}get parentNode(){let t=this._$AA.parentNode;const e=this._$AM;return e!==void 0&&t?.nodeType===11&&(t=e.parentNode),t}get startNode(){return this._$AA}get endNode(){return this._$AB}_$AI(t,e=this){t=x(this,t,e),P(t)?t===c||t==null||t===""?(this._$AH!==c&&this._$AR(),this._$AH=c):t!==this._$AH&&t!==w&&this._(t):t._$litType$!==void 0?this.$(t):t.nodeType!==void 0?this.T(t):At(t)?this.k(t):this._(t)}O(t){return this._$AA.parentNode.insertBefore(t,this._$AB)}T(t){this._$AH!==t&&(this._$AR(),this._$AH=this.O(t))}_(t){this._$AH!==c&&P(this._$AH)?this._$AA.nextSibling.data=t:this.T(b.createTextNode(t)),this._$AH=t}$(t){const{values:e,_$litType$:s}=t,i=typeof s=="number"?this._$AC(t):(s.el===void 0&&(s.el=k.createElement(ht(s.h,s.h[0]),this.options)),s);if(this._$AH?._$AD===i)this._$AH.p(e);else{const o=new St(i,this),n=o.u(this.options);o.p(e),this.T(n),this._$AH=o}}_$AC(t){let e=st.get(t.strings);return e===void 0&&st.set(t.strings,e=new k(t)),e}k(t){q(this._$AH)||(this._$AH=[],this._$AR());const e=this._$AH;let s,i=0;for(const o of t)i===e.length?e.push(s=new U(this.O(C()),this.O(C()),this,this.options)):s=e[i],s._$AI(o),i++;i<e.length&&(this._$AR(s&&s._$AB.nextSibling,i),e.length=i)}_$AR(t=this._$AA.nextSibling,e){for(this._$AP?.(!1,!0,e);t!==this._$AB;){const s=Q(t).nextSibling;Q(t).remove(),t=s}}setConnected(t){this._$AM===void 0&&(this._$Cv=t,this._$AP?.(t))}}class H{get tagName(){return this.element.tagName}get _$AU(){return this._$AM._$AU}constructor(t,e,s,i,o){this.type=1,this._$AH=c,this._$AN=void 0,this.element=t,this.name=e,this._$AM=i,this.options=o,s.length>2||s[0]!==""||s[1]!==""?(this._$AH=Array(s.length-1).fill(new String),this.strings=s):this._$AH=c}_$AI(t,e=this,s,i){const o=this.strings;let n=!1;if(o===void 0)t=x(this,t,e,0),n=!P(t)||t!==this._$AH&&t!==w,n&&(this._$AH=t);else{const h=t;let a,p;for(t=o[0],a=0;a<o.length-1;a++)p=x(this,h[s+a],e,a),p===w&&(p=this._$AH[a]),n||=!P(p)||p!==this._$AH[a],p===c?t=c:t!==c&&(t+=(p??"")+o[a+1]),this._$AH[a]=p}n&&!i&&this.j(t)}j(t){t===c?this.element.removeAttribute(this.name):this.element.setAttribute(this.name,t??"")}}class Et extends H{constructor(){super(...arguments),this.type=3}j(t){this.element[this.name]=t===c?void 0:t}}class Ct extends H{constructor(){super(...arguments),this.type=4}j(t){this.element.toggleAttribute(this.name,!!t&&t!==c)}}class Pt extends H{constructor(t,e,s,i,o){super(t,e,s,i,o),this.type=5}_$AI(t,e=this){if((t=x(this,t,e,0)??c)===w)return;const s=this._$AH,i=t===c&&s!==c||t.capture!==s.capture||t.once!==s.once||t.passive!==s.passive,o=t!==c&&(s===c||i);i&&this.element.removeEventListener(this.name,this,s),o&&this.element.addEventListener(this.name,this,t),this._$AH=t}handleEvent(t){typeof this._$AH=="function"?this._$AH.call(this.options?.host??this.element,t):this._$AH.handleEvent(t)}}class kt{constructor(t,e,s){this.element=t,this.type=6,this._$AN=void 0,this._$AM=e,this.options=s}get _$AU(){return this._$AM._$AU}_$AI(t){x(this,t)}}const Ut=B.litHtmlPolyfillSupport;Ut?.(k,U),(B.litHtmlVersions??=[]).push("3.3.3");const Ot=(r,t,e)=>{const s=e?.renderBefore??t;let i=s._$litPart$;if(i===void 0){const o=e?.renderBefore??null;s._$litPart$=i=new U(t.insertBefore(C(),o),o,void 0,e??{})}return i._$AI(r),i};/**
16
+ * @license
17
+ * Copyright 2017 Google LLC
18
+ * SPDX-License-Identifier: BSD-3-Clause
19
+ */const W=globalThis;class v extends A{constructor(){super(...arguments),this.renderOptions={host:this},this._$Do=void 0}createRenderRoot(){const t=super.createRenderRoot();return this.renderOptions.renderBefore??=t.firstChild,t}update(t){const e=this.render();this.hasUpdated||(this.renderOptions.isConnected=this.isConnected),super.update(t),this._$Do=Ot(e,this.renderRoot,this.renderOptions)}connectedCallback(){super.connectedCallback(),this._$Do?.setConnected(!0)}disconnectedCallback(){super.disconnectedCallback(),this._$Do?.setConnected(!1)}render(){return w}}v._$litElement$=!0,v.finalized=!0,W.litElementHydrateSupport?.({LitElement:v});const Tt=W.litElementPolyfillSupport;Tt?.({LitElement:v});(W.litElementVersions??=[]).push("4.2.2");var ct;let z=0;const it=new Set;function Mt(r){z++;try{r()}finally{z--,z===0&&Nt()}}function Nt(){const r=new Set(it);it.clear();for(const t of r)t()}function Ht(r,t){return r!=r?t==t:r!==t||r&&typeof r=="object"||typeof r=="function"}function Rt(r){return r&&typeof r.deref=="function"}class V{static#e=0;#t;#s;#i=new Set;#r=ct.#e++;name;constructor(t,e){this.#t=t,this.#s=t,this.name=e}_get(){return u.activeConsumer&&u.activeConsumer.track(this),this.#t}_set(t){return Ht(t,this.#t)?(this.#s=this.#t,this.#t=t,this._notify(),!0):!1}subscribe(t,e=!1){let s=t;return e&&(s=new WeakRef(t)),this.#i.add(s),()=>this.#i.delete(s)}_notify(){const t=[...this.#i];for(const e of t){let s;if(Rt(e)){if(s=e.deref(),!s){this.#i.delete(e);continue}}else s=e;s()}}get id(){return this.#r}get previousValue(){return this.#s}toString(){return this.name?`${this.constructor.name}(${this.name})`:`${this.constructor.name}#${this.id}`}}ct=V;class u extends V{static#e=[];constructor(t,e){super(t,e)}static get activeConsumer(){return this.#e[this.#e.length-1]}static push(t){this.#e.push(t)}static pop(){this.#e.pop()}get(){return this._get()}set(t){Mt(()=>{this._set(t)})}get value(){return this.get()}set value(t){this.set(t)}update(t){this.set(t(this.get()))}}class zt extends V{#e;#t=!0;#s=!1;#i=new Map;_debugParent;#r;#o=()=>{this.#t||(this.#t=!0,this._notify())};constructor(t,e,s){super(void 0,e),this.#e=t,this.#r=s?.weak??!0}track(t){if(this.#i.has(t))return;const e=t.subscribe(this.#o,this.#r);this.#i.set(t,e)}dispose(){this.#n()}#n(){for(const t of this.#i.values())t();this.#i.clear()}get(){if(u.activeConsumer&&u.activeConsumer.track(this),!this.#t)return this._get();if(this.#s)throw new Error(`Cycle detected in ${this}
20
+ ↳ while computing ${this._debugParent??"root"}`);this.#s=!0,this.#n(),u.push(this);try{const t=this.#e();this._set(t)}finally{u.pop(),this.#t=!1,this.#s=!1}return this._get()}get value(){return this.get()}set value(t){throw new Error(`Cannot set value of Computed ${this}. Computed values are read-only.`)}subscribe(t,e=!1){const s=super.subscribe(t,e);return u.activeConsumer||this.get(),s}}class jt{#e;#t=new Map;_debugParent;constructor(t){this.#e=t,t.addController(this);const e=t,s=e.update,i=this;e.update=function(o){u.push(i);try{s.call(this,o)}finally{u.pop()}}}track(t){this.#t.has(t)||this.#t.set(t,t.subscribe(this.#s,!0))}#s=()=>{this.#e.requestUpdate()};hostUpdate(){for(const t of this.#t.values())t();this.#t.clear()}hostDisconnected(){for(const t of this.#t.values())t();this.#t.clear()}toString(){const t=this.#e;return`SignalWatcher(${t?.localName??t?.tagName??t?.constructor?.name??"unknown"})`}}class Lt extends v{static{this.properties={results:{attribute:!1},onSelect:{attribute:!1}}}static{this.styles=I`
21
+ :host { display: block; }
22
+ .row { padding: 10px 14px; border-bottom: 1px solid #8882; cursor: pointer; }
23
+ .row:hover { background: #8881; }
24
+ .top { display: flex; justify-content: space-between; gap: 8px; font-size: 13px; }
25
+ .name { font-weight: 600; }
26
+ .meta { opacity: 0.65; font-size: 11px; white-space: nowrap; }
27
+ .desc { font-size: 12px; opacity: 0.8; margin-top: 3px; }
28
+ .tags { margin-top: 4px; display: flex; gap: 4px; flex-wrap: wrap; }
29
+ .tag { font-size: 10px; border: 1px solid #8886; border-radius: 999px; padding: 1px 6px; }
30
+ .type-badge { font-size: 10px; text-transform: uppercase; opacity: 0.6; }
31
+ .empty { padding: 24px; opacity: 0.6; font-size: 13px; }
32
+ `}render(){return!this.results||this.results.length===0?f`<div class="empty">No results.</div>`:f`
33
+ ${this.results.map(t=>f`
34
+ <div class="row" @click=${()=>this.onSelect(t)}>
35
+ <div class="top">
36
+ <span class="name">${t.name}</span>
37
+ <span class="meta">${t.owner??"—"} · ${t.status}</span>
38
+ </div>
39
+ <div class="desc">${t.description}</div>
40
+ <div class="tags">
41
+ <span class="type-badge">${t._table==="skills"?"skill":"memory"}</span>
42
+ ${t.tags.map(e=>f`<span class="tag">${e}</span>`)}
43
+ </div>
44
+ </div>
45
+ `)}
46
+ `}}customElements.define("result-list",Lt);class Dt extends v{static{this.properties={selected:{attribute:!1},_doc:{state:!0}}}static{this.styles=I`
47
+ :host { display: block; padding: 16px; }
48
+ h2 { margin: 0 0 4px; font-size: 16px; }
49
+ .meta { font-size: 12px; opacity: 0.7; margin-bottom: 12px; }
50
+ .source-path {
51
+ font-family: monospace;
52
+ font-size: 11px;
53
+ opacity: 0.6;
54
+ cursor: pointer;
55
+ word-break: break-all;
56
+ }
57
+ pre {
58
+ white-space: pre-wrap;
59
+ font-size: 12px;
60
+ background: #8881;
61
+ padding: 12px;
62
+ border-radius: 6px;
63
+ max-height: 60vh;
64
+ overflow-y: auto;
65
+ }
66
+ .empty { opacity: 0.5; font-size: 13px; }
67
+ `}updated(t){t.has("selected")&&this.selected&&this.#e()}async#e(){const{table:t,id:e}=this.selected,s=await fetch(`/api/entries/${t}/${encodeURIComponent(e)}`);this._doc=s.ok?await s.json():null}#t(){this._doc?.source_path&&navigator.clipboard?.writeText(this._doc.source_path)}render(){if(!this.selected)return f`<div class="empty">Select an entry to view its details.</div>`;if(!this._doc)return c;const t=this._doc;return f`
68
+ <h2>${t.name??t.key??t.id}</h2>
69
+ <div class="meta">
70
+ ${t.tags?.join(", ")||"no tags"} · ${t.status}${t.owner?` · owner: ${t.owner}`:""}
71
+ </div>
72
+ <div class="meta">${t.description}</div>
73
+ <pre>${t.body}</pre>
74
+ <div class="source-path" title="click to copy" @click=${()=>this.#t()}>${t.source_path}</div>
75
+ `}}customElements.define("detail-panel",Dt);const It=[{value:"all",label:"All"},{value:"skill",label:"Skills"},{value:"memory",label:"Memories"}],Bt={tags:[],statuses:[],owners:[],doc_types:[],key_types:[]};class qt extends v{static{this.styles=I`
76
+ :host { display: block; height: 100vh; }
77
+ .filters {
78
+ padding: 12px 16px;
79
+ border-bottom: 1px solid #8883;
80
+ display: flex;
81
+ flex-direction: column;
82
+ gap: 8px;
83
+ }
84
+ .filters input[type='search'] {
85
+ font-size: 14px;
86
+ padding: 8px 10px;
87
+ width: 100%;
88
+ max-width: 480px;
89
+ box-sizing: border-box;
90
+ }
91
+ .row { display: flex; gap: 8px; flex-wrap: wrap; align-items: center; }
92
+ .chip {
93
+ border: 1px solid #8886;
94
+ border-radius: 999px;
95
+ padding: 3px 10px;
96
+ font-size: 12px;
97
+ cursor: pointer;
98
+ background: none;
99
+ color: inherit;
100
+ }
101
+ .chip.active { background: #2563eb; border-color: #2563eb; color: white; }
102
+ .type-toggle button {
103
+ border: 1px solid #8886;
104
+ background: none;
105
+ color: inherit;
106
+ padding: 5px 12px;
107
+ font-size: 13px;
108
+ cursor: pointer;
109
+ }
110
+ .type-toggle button.active { background: #2563eb; border-color: #2563eb; color: white; }
111
+ .type-toggle button:first-child { border-radius: 6px 0 0 6px; }
112
+ .type-toggle button:last-child { border-radius: 0 6px 6px 0; }
113
+ .body-region { display: flex; height: calc(100vh - 130px); }
114
+ result-list { flex: 1 1 40%; overflow-y: auto; border-right: 1px solid #8883; }
115
+ detail-panel { flex: 1 1 60%; overflow-y: auto; }
116
+ label.small { font-size: 12px; opacity: 0.7; }
117
+ `}#e=new u("");#t=new u("all");#s=new u([]);#i=new u([]);#r=new u(Bt);#o=new u(null);constructor(){super(),new jt(this)}#n=new zt(()=>{const t=new URLSearchParams;this.#t.value!=="all"&&t.set("type",this.#t.value),this.#e.value.trim()&&t.set("q",this.#e.value.trim());for(const e of this.#s.value)t.append("tag",e);return t.toString()});connectedCallback(){super.connectedCallback(),this.#a(),this.#l()}async#a(){const t=await fetch(`/api/entries?${this.#n.value}`);this.#i.set(await t.json())}async#l(){const t=this.#t.value==="all"?"":`?type=${this.#t.value}`,e=await fetch(`/api/facets${t}`);this.#r.set(await e.json())}#h(t){this.#t.set(t),this.#s.set([]),this.#l(),this.#a()}#c(t){const e=this.#s.value;this.#s.set(e.includes(t)?e.filter(s=>s!==t):[...e,t]),this.#a()}#p(t){this.#e.set(t.target.value),this.#a()}#d(t){this.#o.set({table:t._table,id:t.id})}render(){const t=this.#r.value;return f`
118
+ <div class="filters">
119
+ <input
120
+ type="search"
121
+ placeholder="Search descriptions, bodies, tags..."
122
+ .value=${this.#e.value}
123
+ @input=${e=>this.#p(e)}
124
+ />
125
+ <div class="row type-toggle">
126
+ ${It.map(e=>f`
127
+ <button
128
+ class=${this.#t.value===e.value?"active":""}
129
+ @click=${()=>this.#h(e.value)}
130
+ >
131
+ ${e.label}
132
+ </button>
133
+ `)}
134
+ </div>
135
+ <div class="row">
136
+ ${t.tags.length===0?f`<label class="small">no tags yet</label>`:t.tags.map(e=>f`
137
+ <button
138
+ class="chip ${this.#s.value.includes(e)?"active":""}"
139
+ @click=${()=>this.#c(e)}
140
+ >
141
+ ${e}
142
+ </button>
143
+ `)}
144
+ </div>
145
+ </div>
146
+ <div class="body-region">
147
+ <result-list .results=${this.#i.value} .onSelect=${e=>this.#d(e)}></result-list>
148
+ <detail-panel .selected=${this.#o.value}></detail-panel>
149
+ </div>
150
+ `}}customElements.define("mem-bucket-app",qt);
@@ -0,0 +1,16 @@
1
+ <!doctype html>
2
+ <html lang="en">
3
+ <head>
4
+ <meta charset="utf-8" />
5
+ <title>mem-bucket viewer</title>
6
+ <style>
7
+ :root { color-scheme: light dark; }
8
+ body { font-family: system-ui, sans-serif; margin: 0; }
9
+ </style>
10
+ <script type="module" crossorigin src="/assets/index-CxhInmjj.js"></script>
11
+ </head>
12
+ <body>
13
+ <mem-bucket-app></mem-bucket-app>
14
+
15
+ </body>
16
+ </html>
@@ -0,0 +1,23 @@
1
+ import fs from 'node:fs';
2
+ import path from 'node:path';
3
+ function memoryDirFlag(argv) {
4
+ const idx = argv.indexOf('--memory-dir');
5
+ return idx !== -1 ? argv[idx + 1] : undefined;
6
+ }
7
+ export function loadConfig(cwd = process.cwd(), argv = process.argv) {
8
+ const configPath = path.join(cwd, 'memory-bucket.config.json');
9
+ const hasConfigFile = fs.existsSync(configPath);
10
+ let overrides = {};
11
+ if (hasConfigFile) {
12
+ overrides = JSON.parse(fs.readFileSync(configPath, 'utf-8'));
13
+ }
14
+ const explicitDir = memoryDirFlag(argv) ?? process.env.MEMORY_BUCKET_DIR;
15
+ const baseDir = explicitDir ? path.resolve(cwd, explicitDir) : cwd;
16
+ const skillSources = (overrides.skill_sources ?? ['./skills']).map((p) => path.resolve(baseDir, p));
17
+ const memorySources = (overrides.memory_sources ?? ['./docs']).map((p) => path.resolve(baseDir, p));
18
+ return {
19
+ skillSources,
20
+ memorySources,
21
+ cacheDbPath: path.join(baseDir, '.memory-bucket-cache.sqlite'),
22
+ };
23
+ }
@@ -0,0 +1,99 @@
1
+ import fs from 'node:fs';
2
+ import path from 'node:path';
3
+ import { randomUUID } from 'node:crypto';
4
+ import { writeMarkdownFile } from '../store/markdown-file.js';
5
+ import { slugify } from '../store/slug.js';
6
+ import { resolveWithinBase } from '../store/safe-path.js';
7
+ import { upsertFile, removeFile, memorySyncSpec } from '../store/sync.js';
8
+ import { normalizeKey } from '../types.js';
9
+ function rowToDoc(row) {
10
+ return {
11
+ id: row.id,
12
+ key: row.key,
13
+ key_type: row.key_type,
14
+ description: row.description,
15
+ doc_type: row.doc_type,
16
+ tags: JSON.parse(row.tags),
17
+ status: row.status,
18
+ related_to: row.related_to,
19
+ source_path: row.source_path,
20
+ body: row.body,
21
+ };
22
+ }
23
+ export class MemoryRepository {
24
+ db;
25
+ defaultSourceDir;
26
+ syncSpec;
27
+ constructor(db, defaultSourceDir) {
28
+ this.db = db;
29
+ this.defaultSourceDir = defaultSourceDir;
30
+ this.syncSpec = memorySyncSpec([defaultSourceDir]);
31
+ }
32
+ /** Exact-match lookup by normalized key, per V0 (no fuzzy matching). */
33
+ getByKey(key, docType) {
34
+ const normalized = normalizeKey(key);
35
+ const rows = docType
36
+ ? this.db.prepare(`SELECT * FROM memory_docs WHERE key = ? AND doc_type = ?`).all(normalized, docType)
37
+ : this.db.prepare(`SELECT * FROM memory_docs WHERE key = ?`).all(normalized);
38
+ return rows.map(rowToDoc);
39
+ }
40
+ get(id) {
41
+ const row = this.db.prepare(`SELECT * FROM memory_docs WHERE id = ?`).get(id);
42
+ return row ? rowToDoc(row) : null;
43
+ }
44
+ listKeys(keyPrefix) {
45
+ const rows = this.db
46
+ .prepare(`SELECT key, COUNT(*) as doc_count FROM memory_docs GROUP BY key ORDER BY key`)
47
+ .all();
48
+ const prefix = keyPrefix ? normalizeKey(keyPrefix) : undefined;
49
+ return rows
50
+ .filter((r) => !prefix || r.key.startsWith(prefix))
51
+ .map((r) => ({ key: r.key, docCount: r.doc_count }));
52
+ }
53
+ create(input) {
54
+ const normalizedKey = normalizeKey(input.key);
55
+ const id = `${slugify(normalizedKey)}-${slugify(input.description)}-${randomUUID().slice(0, 8)}`;
56
+ const filePath = resolveWithinBase(this.defaultSourceDir, input.folder, `${id}.md`);
57
+ fs.mkdirSync(path.dirname(filePath), { recursive: true });
58
+ const fm = {
59
+ id,
60
+ key: normalizedKey,
61
+ key_type: input.key_type,
62
+ description: input.description,
63
+ doc_type: input.doc_type,
64
+ tags: input.tags ?? [],
65
+ status: 'active',
66
+ related_to: input.related_to ?? null,
67
+ source_path: filePath,
68
+ };
69
+ writeMarkdownFile(filePath, stripSourcePath(fm), input.body);
70
+ upsertFile(this.db, this.syncSpec, filePath);
71
+ return { ...fm, body: input.body };
72
+ }
73
+ update(id, frontmatter, body) {
74
+ const existing = this.get(id);
75
+ if (!existing)
76
+ throw new Error(`memory doc with id "${id}" not found`);
77
+ const merged = {
78
+ ...existing,
79
+ ...frontmatter,
80
+ id: existing.id,
81
+ key: frontmatter?.key ? normalizeKey(frontmatter.key) : existing.key,
82
+ };
83
+ const newBody = body ?? existing.body;
84
+ writeMarkdownFile(existing.source_path, stripSourcePath(merged), newBody);
85
+ upsertFile(this.db, this.syncSpec, existing.source_path);
86
+ return { ...merged, body: newBody };
87
+ }
88
+ delete(id) {
89
+ const existing = this.get(id);
90
+ if (!existing)
91
+ throw new Error(`memory doc with id "${id}" not found`);
92
+ fs.unlinkSync(existing.source_path);
93
+ removeFile(this.db, 'memory_docs', existing.source_path);
94
+ }
95
+ }
96
+ function stripSourcePath(fm) {
97
+ const { source_path: _sp, ...rest } = fm;
98
+ return rest;
99
+ }
@@ -0,0 +1,97 @@
1
+ import { z } from 'zod';
2
+ const MEMORY_DOC_TYPES = ['plan', 'spec', 'sql', 'testing-todo', 'discovery', 'session-summary', 'other'];
3
+ const MEMORY_KEY_TYPES = ['ticket', 'freeform'];
4
+ const MEMORY_STATUS = ['active', 'shipped', 'abandoned'];
5
+ const AUTHORING_SKILL_HINT = "Before your first call in a session, run skill_get(\"memory-bucket-authoring\") to learn the exact frontmatter schema and conventions — don't guess the shape.";
6
+ export function registerMemoryTools(mcp, repo) {
7
+ mcp.tool('memory_get', 'Exact-match lookup of memory docs (plan/spec/sql/etc.) by normalized key — a ticket ID or a free-form name like "Spot Chart Design". Returns every doc under that key, or only the matching doc_type if provided.', {
8
+ key: z.string(),
9
+ doc_type: z.enum(MEMORY_DOC_TYPES).optional(),
10
+ }, async ({ key, doc_type }) => {
11
+ const docs = repo.getByKey(key, doc_type);
12
+ return { content: [{ type: 'text', text: JSON.stringify(docs, null, 2) }] };
13
+ });
14
+ mcp.tool('memory_list', 'Browses available memory keys (optionally filtered by a prefix) without needing to know the exact key upfront. Returns each key with its doc count.', { key_prefix: z.string().optional() }, async ({ key_prefix }) => {
15
+ const keys = repo.listKeys(key_prefix);
16
+ return { content: [{ type: 'text', text: JSON.stringify(keys, null, 2) }] };
17
+ });
18
+ mcp.tool('memory_create', `Writes a new memory doc (plan, spec, SQL, testing notes, discovery, etc.) into the memory source directory under the given key. ${AUTHORING_SKILL_HINT}`, {
19
+ key: z.string().describe('lookup handle — ticket ID or free-form name; normalized on write'),
20
+ key_type: z.enum(MEMORY_KEY_TYPES),
21
+ doc_type: z.enum(MEMORY_DOC_TYPES),
22
+ description: z.string().describe('distinguishes this doc from others sharing the same key'),
23
+ body: z.string(),
24
+ tags: z.array(z.string()).optional(),
25
+ related_to: z.string().optional().describe('id of a related doc, e.g. a spec linking to its plan'),
26
+ folder: z.string().optional().describe('optional subdirectory under the memory source dir'),
27
+ }, async ({ key, key_type, doc_type, description, body, tags, related_to, folder }) => {
28
+ try {
29
+ const doc = repo.create({ key, key_type, doc_type, description, body, tags, related_to, folder });
30
+ return { content: [{ type: 'text', text: JSON.stringify(doc, null, 2) }] };
31
+ }
32
+ catch (err) {
33
+ return { content: [{ type: 'text', text: err.message }], isError: true };
34
+ }
35
+ });
36
+ mcp.tool('memory_update', `Edits an existing memory doc in place — frontmatter fields and/or body. Only provided fields change. ${AUTHORING_SKILL_HINT}`, {
37
+ id: z.string(),
38
+ key: z.string().optional(),
39
+ key_type: z.enum(MEMORY_KEY_TYPES).optional(),
40
+ doc_type: z.enum(MEMORY_DOC_TYPES).optional(),
41
+ description: z.string().optional(),
42
+ body: z.string().optional(),
43
+ tags: z.array(z.string()).optional(),
44
+ status: z.enum(MEMORY_STATUS).optional(),
45
+ related_to: z.string().optional(),
46
+ }, async ({ id, body, ...frontmatterFields }) => {
47
+ try {
48
+ const doc = repo.update(id, frontmatterFields, body);
49
+ return { content: [{ type: 'text', text: JSON.stringify(doc, null, 2) }] };
50
+ }
51
+ catch (err) {
52
+ return { content: [{ type: 'text', text: err.message }], isError: true };
53
+ }
54
+ });
55
+ mcp.tool('memory_delete', 'Hard-deletes a memory doc by id — removes the markdown file, no tombstone.', { id: z.string() }, async ({ id }) => {
56
+ try {
57
+ repo.delete(id);
58
+ return { content: [{ type: 'text', text: `Deleted memory doc "${id}"` }] };
59
+ }
60
+ catch (err) {
61
+ return { content: [{ type: 'text', text: err.message }], isError: true };
62
+ }
63
+ });
64
+ mcp.tool('memory_save_session', `Saves a summary of the current chat session as a memory doc (doc_type "session-summary"). Pass a summary, not a raw transcript. If key or description are omitted, ask the user for them rather than guessing. ${AUTHORING_SKILL_HINT}`, {
65
+ summary: z.string().describe('a scannable summary of the session, not a raw transcript'),
66
+ key: z.string().optional(),
67
+ description: z.string().optional(),
68
+ tags: z.array(z.string()).optional(),
69
+ }, async ({ summary, key, description, tags }) => {
70
+ if (!key || !description) {
71
+ const missing = [!key && 'key', !description && 'description'].filter(Boolean).join(' and ');
72
+ return {
73
+ content: [
74
+ {
75
+ type: 'text',
76
+ text: `Missing ${missing}. Ask the user what key (ticket id or free-form name) and description this session summary should be saved under, then call memory_save_session again.`,
77
+ },
78
+ ],
79
+ isError: true,
80
+ };
81
+ }
82
+ try {
83
+ const doc = repo.create({
84
+ key,
85
+ key_type: /^[A-Z]+-\d+$/i.test(key) ? 'ticket' : 'freeform',
86
+ doc_type: 'session-summary',
87
+ description,
88
+ body: summary,
89
+ tags,
90
+ });
91
+ return { content: [{ type: 'text', text: JSON.stringify(doc, null, 2) }] };
92
+ }
93
+ catch (err) {
94
+ return { content: [{ type: 'text', text: err.message }], isError: true };
95
+ }
96
+ });
97
+ }
@@ -0,0 +1,86 @@
1
+ import path from 'node:path';
2
+ import { fileURLToPath } from 'node:url';
3
+ import express from 'express';
4
+ import { McpServer } from '@modelcontextprotocol/sdk/server/mcp.js';
5
+ import { StreamableHTTPServerTransport } from '@modelcontextprotocol/sdk/server/streamableHttp.js';
6
+ import { loadConfig } from './config.js';
7
+ import { openCache } from './store/db.js';
8
+ import { initialScan, watchSources, skillSyncSpec, memorySyncSpec } from './store/sync.js';
9
+ import { SkillRepository } from './skills/repository.js';
10
+ import { MemoryRepository } from './memory/repository.js';
11
+ import { registerSkillTools } from './skills/tools.js';
12
+ import { registerMemoryTools } from './memory/tools.js';
13
+ import { registerRelocateTool } from './shared/relocate-tool.js';
14
+ import { buildWebRouter } from './web/routes.js';
15
+ import { registerUiTool } from './web/ui-tool.js';
16
+ const __dirname = path.dirname(fileURLToPath(import.meta.url));
17
+ // __dirname is <pkg>/src when run via tsx (dev/test) and <pkg>/dist/src once
18
+ // built — either way dist/client (the Vite output) sits one level above the
19
+ // nearer of the two src/ dirs, so walk up until we're out of any src/ nesting.
20
+ const packageRoot = __dirname.endsWith(`${path.sep}dist${path.sep}src`)
21
+ ? path.join(__dirname, '..', '..')
22
+ : path.join(__dirname, '..');
23
+ // Always present regardless of --memory-dir/cwd, so skill_get("memory-bucket-authoring")
24
+ // works no matter where this server is run from (e.g. via `npx` in any project).
25
+ const builtinSkillsDir = path.join(__dirname, 'skills', 'builtin');
26
+ const PORT = process.env.PORT ? Number(process.env.PORT) : 8767;
27
+ const config = loadConfig();
28
+ const db = openCache(config.cacheDbPath);
29
+ const skillSpec = skillSyncSpec([builtinSkillsDir, ...config.skillSources]);
30
+ const memorySpec = memorySyncSpec(config.memorySources);
31
+ initialScan(db, skillSpec);
32
+ initialScan(db, memorySpec);
33
+ watchSources(db, skillSpec);
34
+ watchSources(db, memorySpec);
35
+ const skillRepo = new SkillRepository(db, config.skillSources[0]);
36
+ const memoryRepo = new MemoryRepository(db, config.memorySources[0]);
37
+ // If the user refers to "mem bucket", "mem bucket mcp", "memory bucket", or
38
+ // "skill bucket" (its working-title predecessor) in conversation, they mean
39
+ // this server — surfaced both in serverInfo.description and instructions so
40
+ // clients that expose either to the model can make that association.
41
+ const SERVER_DESCRIPTION = 'Also known as "memory bucket", "mem bucket", or "skill bucket" — if the user refers to this server by any of those names, they mean this one.';
42
+ const SERVER_INSTRUCTIONS = `${SERVER_DESCRIPTION} Exposes skill_* (reusable coding patterns, stored as agentskills.io-standard SKILL.md folders) and memory_* (point-in-time working context — plans, specs, SQL, session summaries — looked up by key) tools, plus a shared relocate tool. Before calling any *_create/*_update/relocate tool, call skill_get("memory-bucket-authoring") first to learn the exact frontmatter schema — don't guess the shape.`;
43
+ function buildMcpServer() {
44
+ const server = new McpServer({ name: 'memory-bucket', version: '0.1.0', description: SERVER_DESCRIPTION }, { capabilities: {}, instructions: SERVER_INSTRUCTIONS });
45
+ registerSkillTools(server, skillRepo);
46
+ registerMemoryTools(server, memoryRepo);
47
+ registerRelocateTool(server, skillRepo, memoryRepo);
48
+ registerUiTool(server, PORT);
49
+ return server;
50
+ }
51
+ const app = express();
52
+ app.use(express.json());
53
+ app.use(buildWebRouter(db));
54
+ app.use(express.static(path.join(packageRoot, 'dist', 'client')));
55
+ app.post('/mcp', async (req, res) => {
56
+ const server = buildMcpServer();
57
+ try {
58
+ const transport = new StreamableHTTPServerTransport({ sessionIdGenerator: undefined });
59
+ res.on('close', () => {
60
+ transport.close();
61
+ server.close();
62
+ });
63
+ await server.connect(transport);
64
+ await transport.handleRequest(req, res, req.body);
65
+ }
66
+ catch (err) {
67
+ console.error('[memory-bucket] error handling MCP request:', err);
68
+ if (!res.headersSent) {
69
+ res.status(500).json({ jsonrpc: '2.0', error: { code: -32603, message: 'Internal server error' }, id: null });
70
+ }
71
+ }
72
+ });
73
+ const methodNotAllowed = (_req, res) => {
74
+ res.writeHead(405).end(JSON.stringify({ jsonrpc: '2.0', error: { code: -32000, message: 'Method not allowed.' }, id: null }));
75
+ };
76
+ app.get('/mcp', methodNotAllowed);
77
+ app.delete('/mcp', methodNotAllowed);
78
+ app.listen(PORT, () => {
79
+ console.error(`[memory-bucket] MCP server listening on http://localhost:${PORT}/mcp`);
80
+ console.error(`[memory-bucket] skill sources: ${config.skillSources.join(', ')}`);
81
+ console.error(`[memory-bucket] memory sources: ${config.memorySources.join(', ')}`);
82
+ });
83
+ process.on('SIGINT', () => {
84
+ db.close();
85
+ process.exit(0);
86
+ });
@@ -0,0 +1,32 @@
1
+ import { z } from 'zod';
2
+ import { relocate } from './relocate.js';
3
+ const AUTHORING_SKILL_HINT = "Before your first call in a session, run skill_get(\"memory-bucket-authoring\") to learn the exact frontmatter schema and conventions — don't guess the shape.";
4
+ export function registerRelocateTool(mcp, skillRepo, memoryRepo) {
5
+ mcp.tool('relocate', `Moves an existing local markdown file into the skill or memory source directory, converting it into a properly frontmattered doc. Default is a move (original deleted); pass keep_original to copy instead. On a weak/ambiguous filename match for memory docs, does nothing — no guess, no partial write; ask the user for an explicit key/description and retry with overrides. ${AUTHORING_SKILL_HINT}`, {
6
+ path: z.string().describe('absolute or relative path to the local file to relocate'),
7
+ target: z.enum(['skill', 'memory']),
8
+ keep_original: z.boolean().optional().describe('if true, copies instead of moving (default: move)'),
9
+ overrides: z
10
+ .object({
11
+ name: z.string().optional().describe('skill target only: lowercase, hyphenated, <=64 chars — becomes the skill folder name'),
12
+ description: z
13
+ .string()
14
+ .max(1024)
15
+ .optional()
16
+ .describe('required for skill target (cannot be inferred from a filename — must state what the skill does and when to use it); optional for memory target where it distinguishes this doc from siblings under the same key'),
17
+ key: z.string().optional().describe('memory target only'),
18
+ key_type: z.enum(['ticket', 'freeform']).optional().describe('memory target only'),
19
+ doc_type: z.enum(['plan', 'spec', 'sql', 'testing-todo', 'discovery', 'session-summary', 'other']).optional().describe('memory target only'),
20
+ tags: z.array(z.string()).optional(),
21
+ status: z.enum(['stable', 'beta', 'unreviewed', 'active', 'shipped', 'abandoned']).optional(),
22
+ folder: z.string().optional().describe('optional subdirectory under the target source dir'),
23
+ })
24
+ .optional(),
25
+ }, async (opts) => {
26
+ const result = relocate(opts, skillRepo, memoryRepo);
27
+ return {
28
+ content: [{ type: 'text', text: JSON.stringify(result, null, 2) }],
29
+ isError: !result.moved,
30
+ };
31
+ });
32
+ }