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 +21 -0
- package/README.md +76 -0
- package/bin/mcp-memory-bucket.js +2 -0
- package/dist/client/assets/index-CxhInmjj.js +150 -0
- package/dist/client/index.html +16 -0
- package/dist/src/config.js +23 -0
- package/dist/src/memory/repository.js +99 -0
- package/dist/src/memory/tools.js +97 -0
- package/dist/src/server.js +86 -0
- package/dist/src/shared/relocate-tool.js +32 -0
- package/dist/src/shared/relocate.js +106 -0
- package/dist/src/skills/builtin/memory-bucket-authoring/SKILL.md +187 -0
- package/dist/src/skills/repository.js +144 -0
- package/dist/src/skills/tools.js +82 -0
- package/dist/src/store/db.js +74 -0
- package/dist/src/store/markdown-file.js +19 -0
- package/dist/src/store/safe-path.js +11 -0
- package/dist/src/store/skill-name.js +11 -0
- package/dist/src/store/slug.js +7 -0
- package/dist/src/store/sync.js +144 -0
- package/dist/src/types.js +4 -0
- package/dist/src/web/routes.js +204 -0
- package/dist/src/web/ui-tool.js +6 -0
- package/package.json +54 -0
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,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
|
+
}
|