diff --git a/README.md b/README.md
index b1fafac..714e565 100644
--- a/README.md
+++ b/README.md
@@ -353,6 +353,7 @@ export class MyDurableObject {
See the [examples directory](./examples) for complete implementations:
- **[Contact Extractor](./examples/contact_extractor)** - AI-powered contact extraction using custom types
+- **[MindCache Server Local](./examples/mindcache_server_local)** - local API + browser client for tag-based key discovery
- Form management with AI assistant
- Image processing workflows
- Multi-step workflows with memory persistence
diff --git a/examples/mindcache_server_local/README.md b/examples/mindcache_server_local/README.md
new file mode 100644
index 0000000..9255ea8
--- /dev/null
+++ b/examples/mindcache_server_local/README.md
@@ -0,0 +1,47 @@
+# MindCache Server Local Example
+
+This example creates sample MindCache files, starts the local `mindcache-server` API, and starts a browser client that:
+
+- lists all tags
+- lets you select tags
+- shows keys that match selected tags (`all` or `any` mode)
+
+## Run
+
+```bash
+cd examples/mindcache_server_local
+npm install
+npm run dev
+```
+
+Then open:
+
+- Client UI: [http://127.0.0.1:4173](http://127.0.0.1:4173)
+- API: [http://127.0.0.1:4040](http://127.0.0.1:4040)
+
+## Separate commands
+
+```bash
+# create sample files in ./data
+npm run seed
+
+# start only API server
+npm run server
+
+# start only client UI server
+npm run client
+```
+
+## What gets created
+
+Sample files are created under:
+
+- `examples/mindcache_server_local/data/team-a/memory.md`
+- `examples/mindcache_server_local/data/team-b/memory.md`
+
+The API loads these files and indexes keys as:
+
+- `team-a/memory.md::customer_profile`
+- `team-b/memory.md::customer_profile`
+
+This demonstrates collision-safe keys based on relative file path.
diff --git a/examples/mindcache_server_local/client/app.js b/examples/mindcache_server_local/client/app.js
new file mode 100644
index 0000000..c4bfcf8
--- /dev/null
+++ b/examples/mindcache_server_local/client/app.js
@@ -0,0 +1,161 @@
+const tagsEl = document.getElementById('tags');
+const entriesEl = document.getElementById('entries');
+const statusEl = document.getElementById('status');
+const summaryEl = document.getElementById('summary');
+const apiBaseInput = document.getElementById('apiBase');
+const matchModeSelect = document.getElementById('matchMode');
+const refreshBtn = document.getElementById('refreshBtn');
+const clearBtn = document.getElementById('clearBtn');
+
+const state = {
+ allTags: [],
+ selectedTags: new Set(),
+ entries: []
+};
+
+function escapeHtml(value) {
+ return value
+ .replaceAll('&', '&')
+ .replaceAll('<', '<')
+ .replaceAll('>', '>')
+ .replaceAll('"', '"')
+ .replaceAll("'", ''');
+}
+
+function apiBase() {
+ return apiBaseInput.value.trim().replace(/\/$/, '');
+}
+
+function renderTags() {
+ if (state.allTags.length === 0) {
+ tagsEl.innerHTML = 'No tags found.';
+ return;
+ }
+
+ tagsEl.innerHTML = state.allTags
+ .map((tag) => {
+ const active = state.selectedTags.has(tag);
+ const className = active ? 'tag active' : 'tag';
+ return ``;
+ })
+ .join('');
+}
+
+function renderEntries() {
+ const selected = Array.from(state.selectedTags).sort();
+ const mode = matchModeSelect.value;
+
+ if (state.entries.length === 0) {
+ const filterText = selected.length > 0
+ ? `No keys matched [${selected.join(', ')}] (${mode}).`
+ : 'No keys found.';
+ summaryEl.textContent = filterText;
+ entriesEl.innerHTML = '';
+ return;
+ }
+
+ const label = selected.length > 0
+ ? `Showing ${state.entries.length} key(s) for tags [${selected.join(', ')}], match=${mode}.`
+ : `Showing all ${state.entries.length} key(s).`;
+ summaryEl.textContent = label;
+
+ entriesEl.innerHTML = state.entries
+ .map((entry) => {
+ const tags = (entry.attributes?.contentTags || []).map((tag) => `${escapeHtml(tag)}`).join(' ');
+ return `
+
+ ${escapeHtml(entry.key)}
+ raw key: ${escapeHtml(entry.rawKey)} | file: ${escapeHtml(entry.fileId)}
+ ${tags || 'no tags'}
+
+ `;
+ })
+ .join('');
+}
+
+async function fetchTags() {
+ const response = await fetch(`${apiBase()}/v1/tags`);
+ if (!response.ok) {
+ throw new Error(`Failed fetching tags: ${response.status}`);
+ }
+
+ const body = await response.json();
+ state.allTags = Array.isArray(body.tags) ? body.tags : [];
+
+ for (const tag of Array.from(state.selectedTags)) {
+ if (!state.allTags.includes(tag)) {
+ state.selectedTags.delete(tag);
+ }
+ }
+}
+
+async function fetchEntries() {
+ const selectedTags = Array.from(state.selectedTags).sort();
+ const params = new URLSearchParams();
+ if (selectedTags.length > 0) {
+ params.set('tags', selectedTags.join(','));
+ }
+ params.set('match', matchModeSelect.value);
+
+ const response = await fetch(`${apiBase()}/v1/entries?${params.toString()}`);
+ if (!response.ok) {
+ throw new Error(`Failed fetching entries: ${response.status}`);
+ }
+
+ const body = await response.json();
+ state.entries = Array.isArray(body.entries) ? body.entries : [];
+}
+
+async function refresh() {
+ statusEl.textContent = 'Loading...';
+
+ try {
+ await fetchTags();
+ await fetchEntries();
+ renderTags();
+ renderEntries();
+ statusEl.textContent = `Loaded ${state.allTags.length} tags.`;
+ } catch (error) {
+ statusEl.textContent = error instanceof Error ? error.message : String(error);
+ }
+}
+
+tagsEl.addEventListener('click', async (event) => {
+ const target = event.target;
+ if (!(target instanceof HTMLElement)) {
+ return;
+ }
+
+ const tag = target.dataset.tag;
+ if (!tag) {
+ return;
+ }
+
+ if (state.selectedTags.has(tag)) {
+ state.selectedTags.delete(tag);
+ } else {
+ state.selectedTags.add(tag);
+ }
+
+ renderTags();
+ await refresh();
+});
+
+refreshBtn.addEventListener('click', () => {
+ void refresh();
+});
+
+clearBtn.addEventListener('click', () => {
+ state.selectedTags.clear();
+ void refresh();
+});
+
+matchModeSelect.addEventListener('change', () => {
+ void refresh();
+});
+
+apiBaseInput.addEventListener('change', () => {
+ void refresh();
+});
+
+void refresh();
diff --git a/examples/mindcache_server_local/client/index.html b/examples/mindcache_server_local/client/index.html
new file mode 100644
index 0000000..59b3471
--- /dev/null
+++ b/examples/mindcache_server_local/client/index.html
@@ -0,0 +1,174 @@
+
+
+
+
+
+ MindCache Server Local Client
+
+
+
+
+ MindCache Server Tag Explorer
+ Select one or more tags to see matching keys from the local API.
+
+
+
+
+
+
+
+
+
+
+
+ Loading tags...
+
+
+
+
+ Matching Keys
+ No data yet.
+
+
+
+
+
+
+
diff --git a/examples/mindcache_server_local/data/team-a/memory.md b/examples/mindcache_server_local/data/team-a/memory.md
new file mode 100644
index 0000000..76f3eb9
--- /dev/null
+++ b/examples/mindcache_server_local/data/team-a/memory.md
@@ -0,0 +1,36 @@
+# Team A Memory
+
+Sample MindCache data for Team A
+
+Export Date: 2026-02-08
+
+---
+
+## Keys & Values
+
+### customer_profile
+- **System Tags**: `SystemPrompt, LLMRead`
+- **Z-Index**: `10`
+- **Tags**: `customer`, `acme`, `sales`
+- **Value**:
+```
+Acme Corp: enterprise customer, strong expansion in Q2.
+```
+
+### open_risks
+- **System Tags**: `SystemPrompt, LLMRead`
+- **Z-Index**: `20`
+- **Tags**: `customer`, `risk`, `acme`
+- **Value**:
+```
+Legal review pending for renewal contract.
+```
+
+### internal_note
+- **System Tags**: `LLMRead`
+- **Z-Index**: `30`
+- **Tags**: `team`, `ops`
+- **Value**:
+```
+Schedule stakeholder sync every Friday.
+```
diff --git a/examples/mindcache_server_local/data/team-b/memory.md b/examples/mindcache_server_local/data/team-b/memory.md
new file mode 100644
index 0000000..683c10e
--- /dev/null
+++ b/examples/mindcache_server_local/data/team-b/memory.md
@@ -0,0 +1,27 @@
+# Team B Memory
+
+Sample MindCache data for Team B
+
+Export Date: 2026-02-08
+
+---
+
+## Keys & Values
+
+### customer_profile
+- **System Tags**: `SystemPrompt, LLMRead`
+- **Z-Index**: `10`
+- **Tags**: `customer`, `beta`, `pilot`
+- **Value**:
+```
+Beta Labs: SMB, interested in pilot feature flags.
+```
+
+### feature_requests
+- **System Tags**: `SystemPrompt, LLMRead`
+- **Z-Index**: `20`
+- **Tags**: `product`, `beta`, `requests`
+- **Value**:
+```
+Needs audit logs and custom retention policy.
+```
diff --git a/examples/mindcache_server_local/package-lock.json b/examples/mindcache_server_local/package-lock.json
new file mode 100644
index 0000000..27013a6
--- /dev/null
+++ b/examples/mindcache_server_local/package-lock.json
@@ -0,0 +1,557 @@
+{
+ "name": "mindcache-server-local-example",
+ "version": "0.1.0",
+ "lockfileVersion": 3,
+ "requires": true,
+ "packages": {
+ "": {
+ "name": "mindcache-server-local-example",
+ "version": "0.1.0",
+ "devDependencies": {
+ "tsx": "^4.20.3"
+ }
+ },
+ "node_modules/@esbuild/aix-ppc64": {
+ "version": "0.27.3",
+ "resolved": "https://registry.npmjs.org/@esbuild/aix-ppc64/-/aix-ppc64-0.27.3.tgz",
+ "integrity": "sha512-9fJMTNFTWZMh5qwrBItuziu834eOCUcEqymSH7pY+zoMVEZg3gcPuBNxH1EvfVYe9h0x/Ptw8KBzv7qxb7l8dg==",
+ "cpu": [
+ "ppc64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "aix"
+ ],
+ "engines": {
+ "node": ">=18"
+ }
+ },
+ "node_modules/@esbuild/android-arm": {
+ "version": "0.27.3",
+ "resolved": "https://registry.npmjs.org/@esbuild/android-arm/-/android-arm-0.27.3.tgz",
+ "integrity": "sha512-i5D1hPY7GIQmXlXhs2w8AWHhenb00+GxjxRncS2ZM7YNVGNfaMxgzSGuO8o8SJzRc/oZwU2bcScvVERk03QhzA==",
+ "cpu": [
+ "arm"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "android"
+ ],
+ "engines": {
+ "node": ">=18"
+ }
+ },
+ "node_modules/@esbuild/android-arm64": {
+ "version": "0.27.3",
+ "resolved": "https://registry.npmjs.org/@esbuild/android-arm64/-/android-arm64-0.27.3.tgz",
+ "integrity": "sha512-YdghPYUmj/FX2SYKJ0OZxf+iaKgMsKHVPF1MAq/P8WirnSpCStzKJFjOjzsW0QQ7oIAiccHdcqjbHmJxRb/dmg==",
+ "cpu": [
+ "arm64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "android"
+ ],
+ "engines": {
+ "node": ">=18"
+ }
+ },
+ "node_modules/@esbuild/android-x64": {
+ "version": "0.27.3",
+ "resolved": "https://registry.npmjs.org/@esbuild/android-x64/-/android-x64-0.27.3.tgz",
+ "integrity": "sha512-IN/0BNTkHtk8lkOM8JWAYFg4ORxBkZQf9zXiEOfERX/CzxW3Vg1ewAhU7QSWQpVIzTW+b8Xy+lGzdYXV6UZObQ==",
+ "cpu": [
+ "x64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "android"
+ ],
+ "engines": {
+ "node": ">=18"
+ }
+ },
+ "node_modules/@esbuild/darwin-arm64": {
+ "version": "0.27.3",
+ "resolved": "https://registry.npmjs.org/@esbuild/darwin-arm64/-/darwin-arm64-0.27.3.tgz",
+ "integrity": "sha512-Re491k7ByTVRy0t3EKWajdLIr0gz2kKKfzafkth4Q8A5n1xTHrkqZgLLjFEHVD+AXdUGgQMq+Godfq45mGpCKg==",
+ "cpu": [
+ "arm64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "darwin"
+ ],
+ "engines": {
+ "node": ">=18"
+ }
+ },
+ "node_modules/@esbuild/darwin-x64": {
+ "version": "0.27.3",
+ "resolved": "https://registry.npmjs.org/@esbuild/darwin-x64/-/darwin-x64-0.27.3.tgz",
+ "integrity": "sha512-vHk/hA7/1AckjGzRqi6wbo+jaShzRowYip6rt6q7VYEDX4LEy1pZfDpdxCBnGtl+A5zq8iXDcyuxwtv3hNtHFg==",
+ "cpu": [
+ "x64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "darwin"
+ ],
+ "engines": {
+ "node": ">=18"
+ }
+ },
+ "node_modules/@esbuild/freebsd-arm64": {
+ "version": "0.27.3",
+ "resolved": "https://registry.npmjs.org/@esbuild/freebsd-arm64/-/freebsd-arm64-0.27.3.tgz",
+ "integrity": "sha512-ipTYM2fjt3kQAYOvo6vcxJx3nBYAzPjgTCk7QEgZG8AUO3ydUhvelmhrbOheMnGOlaSFUoHXB6un+A7q4ygY9w==",
+ "cpu": [
+ "arm64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "freebsd"
+ ],
+ "engines": {
+ "node": ">=18"
+ }
+ },
+ "node_modules/@esbuild/freebsd-x64": {
+ "version": "0.27.3",
+ "resolved": "https://registry.npmjs.org/@esbuild/freebsd-x64/-/freebsd-x64-0.27.3.tgz",
+ "integrity": "sha512-dDk0X87T7mI6U3K9VjWtHOXqwAMJBNN2r7bejDsc+j03SEjtD9HrOl8gVFByeM0aJksoUuUVU9TBaZa2rgj0oA==",
+ "cpu": [
+ "x64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "freebsd"
+ ],
+ "engines": {
+ "node": ">=18"
+ }
+ },
+ "node_modules/@esbuild/linux-arm": {
+ "version": "0.27.3",
+ "resolved": "https://registry.npmjs.org/@esbuild/linux-arm/-/linux-arm-0.27.3.tgz",
+ "integrity": "sha512-s6nPv2QkSupJwLYyfS+gwdirm0ukyTFNl3KTgZEAiJDd+iHZcbTPPcWCcRYH+WlNbwChgH2QkE9NSlNrMT8Gfw==",
+ "cpu": [
+ "arm"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "linux"
+ ],
+ "engines": {
+ "node": ">=18"
+ }
+ },
+ "node_modules/@esbuild/linux-arm64": {
+ "version": "0.27.3",
+ "resolved": "https://registry.npmjs.org/@esbuild/linux-arm64/-/linux-arm64-0.27.3.tgz",
+ "integrity": "sha512-sZOuFz/xWnZ4KH3YfFrKCf1WyPZHakVzTiqji3WDc0BCl2kBwiJLCXpzLzUBLgmp4veFZdvN5ChW4Eq/8Fc2Fg==",
+ "cpu": [
+ "arm64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "linux"
+ ],
+ "engines": {
+ "node": ">=18"
+ }
+ },
+ "node_modules/@esbuild/linux-ia32": {
+ "version": "0.27.3",
+ "resolved": "https://registry.npmjs.org/@esbuild/linux-ia32/-/linux-ia32-0.27.3.tgz",
+ "integrity": "sha512-yGlQYjdxtLdh0a3jHjuwOrxQjOZYD/C9PfdbgJJF3TIZWnm/tMd/RcNiLngiu4iwcBAOezdnSLAwQDPqTmtTYg==",
+ "cpu": [
+ "ia32"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "linux"
+ ],
+ "engines": {
+ "node": ">=18"
+ }
+ },
+ "node_modules/@esbuild/linux-loong64": {
+ "version": "0.27.3",
+ "resolved": "https://registry.npmjs.org/@esbuild/linux-loong64/-/linux-loong64-0.27.3.tgz",
+ "integrity": "sha512-WO60Sn8ly3gtzhyjATDgieJNet/KqsDlX5nRC5Y3oTFcS1l0KWba+SEa9Ja1GfDqSF1z6hif/SkpQJbL63cgOA==",
+ "cpu": [
+ "loong64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "linux"
+ ],
+ "engines": {
+ "node": ">=18"
+ }
+ },
+ "node_modules/@esbuild/linux-mips64el": {
+ "version": "0.27.3",
+ "resolved": "https://registry.npmjs.org/@esbuild/linux-mips64el/-/linux-mips64el-0.27.3.tgz",
+ "integrity": "sha512-APsymYA6sGcZ4pD6k+UxbDjOFSvPWyZhjaiPyl/f79xKxwTnrn5QUnXR5prvetuaSMsb4jgeHewIDCIWljrSxw==",
+ "cpu": [
+ "mips64el"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "linux"
+ ],
+ "engines": {
+ "node": ">=18"
+ }
+ },
+ "node_modules/@esbuild/linux-ppc64": {
+ "version": "0.27.3",
+ "resolved": "https://registry.npmjs.org/@esbuild/linux-ppc64/-/linux-ppc64-0.27.3.tgz",
+ "integrity": "sha512-eizBnTeBefojtDb9nSh4vvVQ3V9Qf9Df01PfawPcRzJH4gFSgrObw+LveUyDoKU3kxi5+9RJTCWlj4FjYXVPEA==",
+ "cpu": [
+ "ppc64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "linux"
+ ],
+ "engines": {
+ "node": ">=18"
+ }
+ },
+ "node_modules/@esbuild/linux-riscv64": {
+ "version": "0.27.3",
+ "resolved": "https://registry.npmjs.org/@esbuild/linux-riscv64/-/linux-riscv64-0.27.3.tgz",
+ "integrity": "sha512-3Emwh0r5wmfm3ssTWRQSyVhbOHvqegUDRd0WhmXKX2mkHJe1SFCMJhagUleMq+Uci34wLSipf8Lagt4LlpRFWQ==",
+ "cpu": [
+ "riscv64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "linux"
+ ],
+ "engines": {
+ "node": ">=18"
+ }
+ },
+ "node_modules/@esbuild/linux-s390x": {
+ "version": "0.27.3",
+ "resolved": "https://registry.npmjs.org/@esbuild/linux-s390x/-/linux-s390x-0.27.3.tgz",
+ "integrity": "sha512-pBHUx9LzXWBc7MFIEEL0yD/ZVtNgLytvx60gES28GcWMqil8ElCYR4kvbV2BDqsHOvVDRrOxGySBM9Fcv744hw==",
+ "cpu": [
+ "s390x"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "linux"
+ ],
+ "engines": {
+ "node": ">=18"
+ }
+ },
+ "node_modules/@esbuild/linux-x64": {
+ "version": "0.27.3",
+ "resolved": "https://registry.npmjs.org/@esbuild/linux-x64/-/linux-x64-0.27.3.tgz",
+ "integrity": "sha512-Czi8yzXUWIQYAtL/2y6vogER8pvcsOsk5cpwL4Gk5nJqH5UZiVByIY8Eorm5R13gq+DQKYg0+JyQoytLQas4dA==",
+ "cpu": [
+ "x64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "linux"
+ ],
+ "engines": {
+ "node": ">=18"
+ }
+ },
+ "node_modules/@esbuild/netbsd-arm64": {
+ "version": "0.27.3",
+ "resolved": "https://registry.npmjs.org/@esbuild/netbsd-arm64/-/netbsd-arm64-0.27.3.tgz",
+ "integrity": "sha512-sDpk0RgmTCR/5HguIZa9n9u+HVKf40fbEUt+iTzSnCaGvY9kFP0YKBWZtJaraonFnqef5SlJ8/TiPAxzyS+UoA==",
+ "cpu": [
+ "arm64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "netbsd"
+ ],
+ "engines": {
+ "node": ">=18"
+ }
+ },
+ "node_modules/@esbuild/netbsd-x64": {
+ "version": "0.27.3",
+ "resolved": "https://registry.npmjs.org/@esbuild/netbsd-x64/-/netbsd-x64-0.27.3.tgz",
+ "integrity": "sha512-P14lFKJl/DdaE00LItAukUdZO5iqNH7+PjoBm+fLQjtxfcfFE20Xf5CrLsmZdq5LFFZzb5JMZ9grUwvtVYzjiA==",
+ "cpu": [
+ "x64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "netbsd"
+ ],
+ "engines": {
+ "node": ">=18"
+ }
+ },
+ "node_modules/@esbuild/openbsd-arm64": {
+ "version": "0.27.3",
+ "resolved": "https://registry.npmjs.org/@esbuild/openbsd-arm64/-/openbsd-arm64-0.27.3.tgz",
+ "integrity": "sha512-AIcMP77AvirGbRl/UZFTq5hjXK+2wC7qFRGoHSDrZ5v5b8DK/GYpXW3CPRL53NkvDqb9D+alBiC/dV0Fb7eJcw==",
+ "cpu": [
+ "arm64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "openbsd"
+ ],
+ "engines": {
+ "node": ">=18"
+ }
+ },
+ "node_modules/@esbuild/openbsd-x64": {
+ "version": "0.27.3",
+ "resolved": "https://registry.npmjs.org/@esbuild/openbsd-x64/-/openbsd-x64-0.27.3.tgz",
+ "integrity": "sha512-DnW2sRrBzA+YnE70LKqnM3P+z8vehfJWHXECbwBmH/CU51z6FiqTQTHFenPlHmo3a8UgpLyH3PT+87OViOh1AQ==",
+ "cpu": [
+ "x64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "openbsd"
+ ],
+ "engines": {
+ "node": ">=18"
+ }
+ },
+ "node_modules/@esbuild/openharmony-arm64": {
+ "version": "0.27.3",
+ "resolved": "https://registry.npmjs.org/@esbuild/openharmony-arm64/-/openharmony-arm64-0.27.3.tgz",
+ "integrity": "sha512-NinAEgr/etERPTsZJ7aEZQvvg/A6IsZG/LgZy+81wON2huV7SrK3e63dU0XhyZP4RKGyTm7aOgmQk0bGp0fy2g==",
+ "cpu": [
+ "arm64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "openharmony"
+ ],
+ "engines": {
+ "node": ">=18"
+ }
+ },
+ "node_modules/@esbuild/sunos-x64": {
+ "version": "0.27.3",
+ "resolved": "https://registry.npmjs.org/@esbuild/sunos-x64/-/sunos-x64-0.27.3.tgz",
+ "integrity": "sha512-PanZ+nEz+eWoBJ8/f8HKxTTD172SKwdXebZ0ndd953gt1HRBbhMsaNqjTyYLGLPdoWHy4zLU7bDVJztF5f3BHA==",
+ "cpu": [
+ "x64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "sunos"
+ ],
+ "engines": {
+ "node": ">=18"
+ }
+ },
+ "node_modules/@esbuild/win32-arm64": {
+ "version": "0.27.3",
+ "resolved": "https://registry.npmjs.org/@esbuild/win32-arm64/-/win32-arm64-0.27.3.tgz",
+ "integrity": "sha512-B2t59lWWYrbRDw/tjiWOuzSsFh1Y/E95ofKz7rIVYSQkUYBjfSgf6oeYPNWHToFRr2zx52JKApIcAS/D5TUBnA==",
+ "cpu": [
+ "arm64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "win32"
+ ],
+ "engines": {
+ "node": ">=18"
+ }
+ },
+ "node_modules/@esbuild/win32-ia32": {
+ "version": "0.27.3",
+ "resolved": "https://registry.npmjs.org/@esbuild/win32-ia32/-/win32-ia32-0.27.3.tgz",
+ "integrity": "sha512-QLKSFeXNS8+tHW7tZpMtjlNb7HKau0QDpwm49u0vUp9y1WOF+PEzkU84y9GqYaAVW8aH8f3GcBck26jh54cX4Q==",
+ "cpu": [
+ "ia32"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "win32"
+ ],
+ "engines": {
+ "node": ">=18"
+ }
+ },
+ "node_modules/@esbuild/win32-x64": {
+ "version": "0.27.3",
+ "resolved": "https://registry.npmjs.org/@esbuild/win32-x64/-/win32-x64-0.27.3.tgz",
+ "integrity": "sha512-4uJGhsxuptu3OcpVAzli+/gWusVGwZZHTlS63hh++ehExkVT8SgiEf7/uC/PclrPPkLhZqGgCTjd0VWLo6xMqA==",
+ "cpu": [
+ "x64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "win32"
+ ],
+ "engines": {
+ "node": ">=18"
+ }
+ },
+ "node_modules/esbuild": {
+ "version": "0.27.3",
+ "resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.27.3.tgz",
+ "integrity": "sha512-8VwMnyGCONIs6cWue2IdpHxHnAjzxnw2Zr7MkVxB2vjmQ2ivqGFb4LEG3SMnv0Gb2F/G/2yA8zUaiL1gywDCCg==",
+ "dev": true,
+ "hasInstallScript": true,
+ "license": "MIT",
+ "bin": {
+ "esbuild": "bin/esbuild"
+ },
+ "engines": {
+ "node": ">=18"
+ },
+ "optionalDependencies": {
+ "@esbuild/aix-ppc64": "0.27.3",
+ "@esbuild/android-arm": "0.27.3",
+ "@esbuild/android-arm64": "0.27.3",
+ "@esbuild/android-x64": "0.27.3",
+ "@esbuild/darwin-arm64": "0.27.3",
+ "@esbuild/darwin-x64": "0.27.3",
+ "@esbuild/freebsd-arm64": "0.27.3",
+ "@esbuild/freebsd-x64": "0.27.3",
+ "@esbuild/linux-arm": "0.27.3",
+ "@esbuild/linux-arm64": "0.27.3",
+ "@esbuild/linux-ia32": "0.27.3",
+ "@esbuild/linux-loong64": "0.27.3",
+ "@esbuild/linux-mips64el": "0.27.3",
+ "@esbuild/linux-ppc64": "0.27.3",
+ "@esbuild/linux-riscv64": "0.27.3",
+ "@esbuild/linux-s390x": "0.27.3",
+ "@esbuild/linux-x64": "0.27.3",
+ "@esbuild/netbsd-arm64": "0.27.3",
+ "@esbuild/netbsd-x64": "0.27.3",
+ "@esbuild/openbsd-arm64": "0.27.3",
+ "@esbuild/openbsd-x64": "0.27.3",
+ "@esbuild/openharmony-arm64": "0.27.3",
+ "@esbuild/sunos-x64": "0.27.3",
+ "@esbuild/win32-arm64": "0.27.3",
+ "@esbuild/win32-ia32": "0.27.3",
+ "@esbuild/win32-x64": "0.27.3"
+ }
+ },
+ "node_modules/fsevents": {
+ "version": "2.3.3",
+ "resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.3.tgz",
+ "integrity": "sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw==",
+ "dev": true,
+ "hasInstallScript": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "darwin"
+ ],
+ "engines": {
+ "node": "^8.16.0 || ^10.6.0 || >=11.0.0"
+ }
+ },
+ "node_modules/get-tsconfig": {
+ "version": "4.13.6",
+ "resolved": "https://registry.npmjs.org/get-tsconfig/-/get-tsconfig-4.13.6.tgz",
+ "integrity": "sha512-shZT/QMiSHc/YBLxxOkMtgSid5HFoauqCE3/exfsEcwg1WkeqjG+V40yBbBrsD+jW2HDXcs28xOfcbm2jI8Ddw==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "resolve-pkg-maps": "^1.0.0"
+ },
+ "funding": {
+ "url": "https://github.com/privatenumber/get-tsconfig?sponsor=1"
+ }
+ },
+ "node_modules/resolve-pkg-maps": {
+ "version": "1.0.0",
+ "resolved": "https://registry.npmjs.org/resolve-pkg-maps/-/resolve-pkg-maps-1.0.0.tgz",
+ "integrity": "sha512-seS2Tj26TBVOC2NIc2rOe2y2ZO7efxITtLZcGSOnHHNOQ7CkiUBfw0Iw2ck6xkIhPwLhKNLS8BO+hEpngQlqzw==",
+ "dev": true,
+ "license": "MIT",
+ "funding": {
+ "url": "https://github.com/privatenumber/resolve-pkg-maps?sponsor=1"
+ }
+ },
+ "node_modules/tsx": {
+ "version": "4.21.0",
+ "resolved": "https://registry.npmjs.org/tsx/-/tsx-4.21.0.tgz",
+ "integrity": "sha512-5C1sg4USs1lfG0GFb2RLXsdpXqBSEhAaA/0kPL01wxzpMqLILNxIxIOKiILz+cdg/pLnOUxFYOR5yhHU666wbw==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "esbuild": "~0.27.0",
+ "get-tsconfig": "^4.7.5"
+ },
+ "bin": {
+ "tsx": "dist/cli.mjs"
+ },
+ "engines": {
+ "node": ">=18.0.0"
+ },
+ "optionalDependencies": {
+ "fsevents": "~2.3.3"
+ }
+ }
+ }
+}
diff --git a/examples/mindcache_server_local/package.json b/examples/mindcache_server_local/package.json
new file mode 100644
index 0000000..e4dfacf
--- /dev/null
+++ b/examples/mindcache_server_local/package.json
@@ -0,0 +1,15 @@
+{
+ "name": "mindcache-server-local-example",
+ "version": "0.1.0",
+ "private": true,
+ "type": "module",
+ "scripts": {
+ "seed": "tsx src/seed.ts",
+ "server": "tsx src/server.ts",
+ "client": "tsx src/client.ts",
+ "dev": "tsx src/dev.ts"
+ },
+ "devDependencies": {
+ "tsx": "^4.20.3"
+ }
+}
diff --git a/examples/mindcache_server_local/src/client.ts b/examples/mindcache_server_local/src/client.ts
new file mode 100644
index 0000000..6c16bad
--- /dev/null
+++ b/examples/mindcache_server_local/src/client.ts
@@ -0,0 +1,36 @@
+import { CLIENT_HOST, CLIENT_PORT } from './config.ts';
+import { startClientServer } from './clientServer.ts';
+
+async function main(): Promise {
+ const server = await startClientServer(CLIENT_HOST, CLIENT_PORT);
+
+ // eslint-disable-next-line no-console
+ console.log(`[example] Client running at http://${CLIENT_HOST}:${CLIENT_PORT}`);
+
+ const shutdown = async () => {
+ await new Promise((resolve, reject) => {
+ server.close((error) => {
+ if (error) {
+ reject(error);
+ return;
+ }
+ resolve();
+ });
+ });
+ process.exit(0);
+ };
+
+ process.on('SIGINT', () => {
+ void shutdown();
+ });
+
+ process.on('SIGTERM', () => {
+ void shutdown();
+ });
+}
+
+void main().catch((error) => {
+ // eslint-disable-next-line no-console
+ console.error(`[example] Client failed: ${error instanceof Error ? error.message : String(error)}`);
+ process.exit(1);
+});
diff --git a/examples/mindcache_server_local/src/clientServer.ts b/examples/mindcache_server_local/src/clientServer.ts
new file mode 100644
index 0000000..b729cd2
--- /dev/null
+++ b/examples/mindcache_server_local/src/clientServer.ts
@@ -0,0 +1,60 @@
+import { createServer, type Server } from 'node:http';
+import { promises as fs } from 'node:fs';
+import path from 'node:path';
+
+import { CLIENT_DIR } from './config.ts';
+
+const MIME_BY_EXTENSION: Record = {
+ '.html': 'text/html; charset=utf-8',
+ '.js': 'text/javascript; charset=utf-8',
+ '.css': 'text/css; charset=utf-8',
+ '.json': 'application/json; charset=utf-8'
+};
+
+function contentTypeFor(filePath: string): string {
+ return MIME_BY_EXTENSION[path.extname(filePath).toLowerCase()] || 'application/octet-stream';
+}
+
+function resolveClientFile(requestPath: string): string | null {
+ const normalizedPath = requestPath === '/' ? '/index.html' : requestPath;
+ const absolutePath = path.resolve(CLIENT_DIR, `.${normalizedPath}`);
+
+ if (!absolutePath.startsWith(CLIENT_DIR)) {
+ return null;
+ }
+
+ return absolutePath;
+}
+
+export async function startClientServer(host: string, port: number): Promise {
+ const server = createServer(async (req, res) => {
+ const requestPath = req.url?.split('?')[0] || '/';
+ const filePath = resolveClientFile(requestPath);
+
+ if (!filePath) {
+ res.statusCode = 403;
+ res.end('Forbidden');
+ return;
+ }
+
+ try {
+ const content = await fs.readFile(filePath);
+ res.setHeader('Content-Type', contentTypeFor(filePath));
+ res.statusCode = 200;
+ res.end(content);
+ } catch {
+ res.statusCode = 404;
+ res.end('Not Found');
+ }
+ });
+
+ await new Promise((resolve, reject) => {
+ server.once('error', reject);
+ server.listen(port, host, () => {
+ server.off('error', reject);
+ resolve();
+ });
+ });
+
+ return server;
+}
diff --git a/examples/mindcache_server_local/src/config.ts b/examples/mindcache_server_local/src/config.ts
new file mode 100644
index 0000000..4244307
--- /dev/null
+++ b/examples/mindcache_server_local/src/config.ts
@@ -0,0 +1,15 @@
+import path from 'node:path';
+import { fileURLToPath } from 'node:url';
+
+export const API_HOST = process.env.MINDCACHE_API_HOST || '127.0.0.1';
+export const API_PORT = Number(process.env.MINDCACHE_API_PORT || 4040);
+export const CLIENT_HOST = process.env.MINDCACHE_CLIENT_HOST || '127.0.0.1';
+export const CLIENT_PORT = Number(process.env.MINDCACHE_CLIENT_PORT || 4173);
+
+const currentFile = fileURLToPath(import.meta.url);
+const srcDir = path.dirname(currentFile);
+export const EXAMPLE_DIR = path.resolve(srcDir, '..');
+export const DATA_DIR = path.join(EXAMPLE_DIR, 'data');
+export const CLIENT_DIR = path.join(EXAMPLE_DIR, 'client');
+
+export const API_BASE_URL = `http://${API_HOST}:${API_PORT}`;
diff --git a/examples/mindcache_server_local/src/dev.ts b/examples/mindcache_server_local/src/dev.ts
new file mode 100644
index 0000000..9f8a003
--- /dev/null
+++ b/examples/mindcache_server_local/src/dev.ts
@@ -0,0 +1,47 @@
+import { API_BASE_URL, CLIENT_HOST, CLIENT_PORT, DATA_DIR } from './config.ts';
+import { startClientServer } from './clientServer.ts';
+import { seedExampleFiles } from './seed.ts';
+import { startMindCacheRuntime } from './serverRuntime.ts';
+
+async function main(): Promise {
+ await seedExampleFiles();
+
+ const runtime = await startMindCacheRuntime();
+ const clientServer = await startClientServer(CLIENT_HOST, CLIENT_PORT);
+
+ // eslint-disable-next-line no-console
+ console.log(`[example] Seeded files in ${DATA_DIR}`);
+ // eslint-disable-next-line no-console
+ console.log(`[example] API: ${API_BASE_URL}`);
+ // eslint-disable-next-line no-console
+ console.log(`[example] Client: http://${CLIENT_HOST}:${CLIENT_PORT}`);
+
+ const shutdown = async () => {
+ await new Promise((resolve, reject) => {
+ clientServer.close((error) => {
+ if (error) {
+ reject(error);
+ return;
+ }
+ resolve();
+ });
+ });
+
+ await runtime.stop();
+ process.exit(0);
+ };
+
+ process.on('SIGINT', () => {
+ void shutdown();
+ });
+
+ process.on('SIGTERM', () => {
+ void shutdown();
+ });
+}
+
+void main().catch((error) => {
+ // eslint-disable-next-line no-console
+ console.error(`[example] Dev launcher failed: ${error instanceof Error ? error.message : String(error)}`);
+ process.exit(1);
+});
diff --git a/examples/mindcache_server_local/src/seed.ts b/examples/mindcache_server_local/src/seed.ts
new file mode 100644
index 0000000..8c79c96
--- /dev/null
+++ b/examples/mindcache_server_local/src/seed.ts
@@ -0,0 +1,111 @@
+import { promises as fs } from 'node:fs';
+import path from 'node:path';
+
+import { MindCache, type KeyAttributes } from '../../../packages/mindcache/dist/server.mjs';
+
+import { DATA_DIR } from './config.ts';
+
+interface Entry {
+ key: string;
+ value: unknown;
+ attributes: Partial;
+}
+
+function createMarkdown(entries: Entry[], options: { name: string; description: string }): string {
+ const cache = new MindCache({ accessLevel: 'admin' });
+
+ for (const entry of entries) {
+ cache.set_value(entry.key, entry.value, entry.attributes);
+ }
+
+ return cache.toMarkdown(options);
+}
+
+async function writeMarkdownFile(relativePath: string, content: string): Promise {
+ const absolutePath = path.join(DATA_DIR, relativePath);
+ await fs.mkdir(path.dirname(absolutePath), { recursive: true });
+ await fs.writeFile(absolutePath, content, 'utf8');
+}
+
+export async function seedExampleFiles(): Promise {
+ await fs.mkdir(DATA_DIR, { recursive: true });
+
+ const teamA = createMarkdown(
+ [
+ {
+ key: 'customer_profile',
+ value: 'Acme Corp: enterprise customer, strong expansion in Q2.',
+ attributes: {
+ contentTags: ['customer', 'acme', 'sales'],
+ systemTags: ['SystemPrompt', 'LLMRead'],
+ zIndex: 10
+ }
+ },
+ {
+ key: 'open_risks',
+ value: 'Legal review pending for renewal contract.',
+ attributes: {
+ contentTags: ['customer', 'risk', 'acme'],
+ systemTags: ['SystemPrompt', 'LLMRead'],
+ zIndex: 20
+ }
+ },
+ {
+ key: 'internal_note',
+ value: 'Schedule stakeholder sync every Friday.',
+ attributes: {
+ contentTags: ['team', 'ops'],
+ systemTags: ['LLMRead'],
+ zIndex: 30
+ }
+ }
+ ],
+ {
+ name: 'Team A Memory',
+ description: 'Sample MindCache data for Team A'
+ }
+ );
+
+ const teamB = createMarkdown(
+ [
+ {
+ key: 'customer_profile',
+ value: 'Beta Labs: SMB, interested in pilot feature flags.',
+ attributes: {
+ contentTags: ['customer', 'beta', 'pilot'],
+ systemTags: ['SystemPrompt', 'LLMRead'],
+ zIndex: 10
+ }
+ },
+ {
+ key: 'feature_requests',
+ value: 'Needs audit logs and custom retention policy.',
+ attributes: {
+ contentTags: ['product', 'beta', 'requests'],
+ systemTags: ['SystemPrompt', 'LLMRead'],
+ zIndex: 20
+ }
+ }
+ ],
+ {
+ name: 'Team B Memory',
+ description: 'Sample MindCache data for Team B'
+ }
+ );
+
+ await writeMarkdownFile('team-a/memory.md', teamA);
+ await writeMarkdownFile('team-b/memory.md', teamB);
+}
+
+if (import.meta.url === `file://${process.argv[1]}`) {
+ seedExampleFiles()
+ .then(() => {
+ // eslint-disable-next-line no-console
+ console.log(`[example] Seeded MindCache files in ${DATA_DIR}`);
+ })
+ .catch((error) => {
+ // eslint-disable-next-line no-console
+ console.error(`[example] Failed seeding files: ${error instanceof Error ? error.message : String(error)}`);
+ process.exit(1);
+ });
+}
diff --git a/examples/mindcache_server_local/src/server.ts b/examples/mindcache_server_local/src/server.ts
new file mode 100644
index 0000000..77f4558
--- /dev/null
+++ b/examples/mindcache_server_local/src/server.ts
@@ -0,0 +1,32 @@
+import { API_BASE_URL, DATA_DIR } from './config.ts';
+import { seedExampleFiles } from './seed.ts';
+import { startMindCacheRuntime } from './serverRuntime.ts';
+
+async function main(): Promise {
+ await seedExampleFiles();
+ const runtime = await startMindCacheRuntime();
+
+ // eslint-disable-next-line no-console
+ console.log(`[example] MindCache API running at ${API_BASE_URL}`);
+ // eslint-disable-next-line no-console
+ console.log(`[example] Watching data folder: ${DATA_DIR}`);
+
+ const shutdown = async () => {
+ await runtime.stop();
+ process.exit(0);
+ };
+
+ process.on('SIGINT', () => {
+ void shutdown();
+ });
+
+ process.on('SIGTERM', () => {
+ void shutdown();
+ });
+}
+
+void main().catch((error) => {
+ // eslint-disable-next-line no-console
+ console.error(`[example] Server failed: ${error instanceof Error ? error.message : String(error)}`);
+ process.exit(1);
+});
diff --git a/examples/mindcache_server_local/src/serverRuntime.ts b/examples/mindcache_server_local/src/serverRuntime.ts
new file mode 100644
index 0000000..97bf7ad
--- /dev/null
+++ b/examples/mindcache_server_local/src/serverRuntime.ts
@@ -0,0 +1,287 @@
+import { createServer, type IncomingMessage, type Server, type ServerResponse } from 'node:http';
+import { promises as fs } from 'node:fs';
+import path from 'node:path';
+
+import { MindCache } from '../../../packages/mindcache/dist/server.mjs';
+
+import { API_HOST, API_PORT, DATA_DIR } from './config.ts';
+
+type MatchMode = 'all' | 'any';
+
+interface IndexedEntry {
+ key: string;
+ rawKey: string;
+ fileId: string;
+ value: unknown;
+ attributes: {
+ type: string;
+ contentType?: string;
+ contentTags: string[];
+ systemTags: string[];
+ zIndex: number;
+ customType?: string;
+ };
+}
+
+interface RuntimeHandle {
+ stop: () => Promise;
+}
+
+const SUPPORTED_EXTENSIONS = new Set(['.md', '.markdown', '.mindcache', '.json']);
+
+function canonicalFileId(rootDir: string, absolutePath: string): string {
+ const relative = path.relative(rootDir, absolutePath);
+ if (!relative || relative.startsWith('..') || path.isAbsolute(relative)) {
+ throw new Error(`Path outside root: ${absolutePath}`);
+ }
+ return relative.split(path.sep).join('/');
+}
+
+function parseTags(raw: string | null): string[] {
+ if (!raw) {
+ return [];
+ }
+
+ return raw
+ .split(',')
+ .map(tag => tag.trim())
+ .filter(Boolean);
+}
+
+function matchTags(entryTags: string[], filterTags: string[], mode: MatchMode): boolean {
+ if (filterTags.length === 0) {
+ return true;
+ }
+
+ if (mode === 'any') {
+ return filterTags.some(tag => entryTags.includes(tag));
+ }
+
+ return filterTags.every(tag => entryTags.includes(tag));
+}
+
+async function listMindCacheFiles(rootDir: string): Promise> {
+ const files: Array<{ fileId: string; absolutePath: string }> = [];
+ const stack = [rootDir];
+
+ while (stack.length > 0) {
+ const current = stack.pop();
+ if (!current) {
+ continue;
+ }
+
+ let dirEntries;
+ try {
+ dirEntries = await fs.readdir(current, { withFileTypes: true, encoding: 'utf8' });
+ } catch {
+ continue;
+ }
+
+ for (const entry of dirEntries) {
+ if (entry.isDirectory()) {
+ if (!entry.name.startsWith('.')) {
+ stack.push(path.join(current, entry.name));
+ }
+ continue;
+ }
+
+ if (!entry.isFile()) {
+ continue;
+ }
+
+ const absolutePath = path.join(current, entry.name);
+ const extension = path.extname(entry.name).toLowerCase();
+ if (!SUPPORTED_EXTENSIONS.has(extension)) {
+ continue;
+ }
+
+ files.push({
+ fileId: canonicalFileId(rootDir, absolutePath),
+ absolutePath
+ });
+ }
+ }
+
+ return files;
+}
+
+function parseFileContent(filePath: string, content: string): Record {
+ const extension = path.extname(filePath).toLowerCase();
+ const cache = new MindCache({ accessLevel: 'admin' });
+
+ if (extension === '.json') {
+ const parsed = JSON.parse(content);
+ cache.deserialize(parsed);
+ } else {
+ cache.fromMarkdown(content, false);
+ }
+
+ return cache.serialize() as Record;
+}
+
+async function loadIndex(rootDir: string): Promise<{ tags: string[]; entries: IndexedEntry[] }> {
+ const files = await listMindCacheFiles(rootDir);
+ const entries: IndexedEntry[] = [];
+ const tags = new Set();
+
+ for (const file of files) {
+ let content: string;
+ try {
+ content = await fs.readFile(file.absolutePath, 'utf8');
+ } catch {
+ continue;
+ }
+
+ let parsed: Record;
+ try {
+ parsed = parseFileContent(file.absolutePath, content);
+ } catch {
+ continue;
+ }
+
+ for (const [rawKey, entry] of Object.entries(parsed)) {
+ if (rawKey.startsWith('$')) {
+ continue;
+ }
+
+ const contentTags = Array.from(new Set((entry.attributes?.contentTags || []) as string[]));
+ for (const tag of contentTags) {
+ tags.add(tag);
+ }
+
+ entries.push({
+ key: `${file.fileId}::${rawKey}`,
+ rawKey,
+ fileId: file.fileId,
+ value: entry.value,
+ attributes: {
+ type: entry.attributes?.type || 'text',
+ contentType: entry.attributes?.contentType,
+ contentTags,
+ systemTags: Array.from(new Set((entry.attributes?.systemTags || []) as string[])),
+ zIndex: entry.attributes?.zIndex ?? 0,
+ customType: entry.attributes?.customType
+ }
+ });
+ }
+ }
+
+ entries.sort((a, b) => a.key.localeCompare(b.key));
+
+ return {
+ tags: Array.from(tags).sort((a, b) => a.localeCompare(b)),
+ entries
+ };
+}
+
+function sendJson(res: ServerResponse, statusCode: number, payload: unknown): void {
+ const body = JSON.stringify(payload, null, 2);
+ res.writeHead(statusCode, {
+ 'Content-Type': 'application/json; charset=utf-8',
+ 'Content-Length': Buffer.byteLength(body)
+ });
+ res.end(body);
+}
+
+function withCors(res: ServerResponse): void {
+ res.setHeader('Access-Control-Allow-Origin', '*');
+ res.setHeader('Access-Control-Allow-Headers', 'Content-Type, Authorization');
+ res.setHeader('Access-Control-Allow-Methods', 'GET,OPTIONS');
+}
+
+async function handleRequest(req: IncomingMessage, res: ServerResponse): Promise {
+ withCors(res);
+
+ if (req.method === 'OPTIONS') {
+ res.writeHead(204);
+ res.end();
+ return;
+ }
+
+ if (!req.url) {
+ sendJson(res, 400, { error: 'Missing URL' });
+ return;
+ }
+
+ const url = new URL(req.url, 'http://localhost');
+
+ if (url.pathname === '/' && req.method === 'GET') {
+ sendJson(res, 200, {
+ name: 'mindcache-server-local-example',
+ status: 'ok',
+ endpoints: [
+ 'GET /health',
+ 'GET /v1/tags',
+ 'GET /v1/entries?tags=tag1,tag2&match=all|any'
+ ]
+ });
+ return;
+ }
+
+ if (url.pathname === '/health' && req.method === 'GET') {
+ const index = await loadIndex(DATA_DIR);
+ sendJson(res, 200, {
+ status: 'ok',
+ rootDir: DATA_DIR,
+ tags: index.tags.length,
+ keys: index.entries.length
+ });
+ return;
+ }
+
+ if (url.pathname === '/v1/tags' && req.method === 'GET') {
+ const index = await loadIndex(DATA_DIR);
+ sendJson(res, 200, { tags: index.tags });
+ return;
+ }
+
+ if (url.pathname === '/v1/entries' && req.method === 'GET') {
+ const filterTags = parseTags(url.searchParams.get('tags'));
+ const mode = url.searchParams.get('match') === 'any' ? 'any' : 'all';
+
+ const index = await loadIndex(DATA_DIR);
+ const entries = index.entries.filter(entry => matchTags(entry.attributes.contentTags, filterTags, mode));
+
+ sendJson(res, 200, {
+ tags: filterTags,
+ match: mode,
+ count: entries.length,
+ entries
+ });
+ return;
+ }
+
+ sendJson(res, 404, { error: 'Not found' });
+}
+
+export async function startMindCacheRuntime(): Promise {
+ const server = createServer((req, res) => {
+ void handleRequest(req, res).catch(error => {
+ sendJson(res, 500, {
+ error: error instanceof Error ? error.message : String(error)
+ });
+ });
+ });
+
+ await new Promise((resolve, reject) => {
+ server.once('error', reject);
+ server.listen(API_PORT, API_HOST, () => {
+ server.off('error', reject);
+ resolve();
+ });
+ });
+
+ return {
+ stop: async () => {
+ await new Promise((resolve, reject) => {
+ server.close(error => {
+ if (error) {
+ reject(error);
+ return;
+ }
+ resolve();
+ });
+ });
+ }
+ };
+}
diff --git a/examples/mindcache_server_local/tsconfig.json b/examples/mindcache_server_local/tsconfig.json
new file mode 100644
index 0000000..a95b11e
--- /dev/null
+++ b/examples/mindcache_server_local/tsconfig.json
@@ -0,0 +1,19 @@
+{
+ "compilerOptions": {
+ "target": "ES2022",
+ "module": "ESNext",
+ "moduleResolution": "Bundler",
+ "strict": true,
+ "esModuleInterop": true,
+ "skipLibCheck": true,
+ "resolveJsonModule": true,
+ "allowImportingTsExtensions": true,
+ "types": ["node"],
+ "baseUrl": "../..",
+ "paths": {
+ "mindcache": ["./packages/mindcache/src/index.ts"],
+ "mindcache/server": ["./packages/mindcache/src/server.ts"]
+ }
+ },
+ "include": ["src/**/*"]
+}
diff --git a/packages/README.md b/packages/README.md
index 4907e23..52e156e 100644
--- a/packages/README.md
+++ b/packages/README.md
@@ -98,6 +98,7 @@ Create `.env.local`:
| Package | Port | Description |
|---------|------|-------------|
| `@mindcache/server` | 8787 | Cloudflare Workers + Durable Objects API |
+| `@mindcache/mindcache-server` | 4040 | Local filesystem-backed MindCache API server |
| `@mindcache/web` | 3000 | Next.js dashboard |
| `@mindcache/shared` | - | Shared types and utilities |
| `mindcache` | - | Client SDK |
@@ -129,4 +130,3 @@ cd packages/web
pnpm dev # Next.js dev server
pnpm build # Production build
```
-
diff --git a/packages/mindcache-server/README.md b/packages/mindcache-server/README.md
new file mode 100644
index 0000000..f78047c
--- /dev/null
+++ b/packages/mindcache-server/README.md
@@ -0,0 +1,113 @@
+# @mindcache/mindcache-server
+
+Local API server that indexes many MindCache files from a filesystem directory and exposes query endpoints optimized for tag-based context window retrieval.
+
+## What It Does
+
+- Watches a root directory of MindCache files (`.md`, `.markdown`, `.mindcache`, `.json` by default).
+- Builds one in-memory aggregate index.
+- Uses canonical keys: `relative/path/to/file.ext::rawKey`.
+- Applies per-file key-level diffs on updates (only changed keys are rewritten).
+- Serves HTTP endpoints for tags, entries, and context windows.
+
+## Install
+
+```bash
+npm install @mindcache/mindcache-server
+```
+
+## Run
+
+```bash
+npx mindcache-server --root ./data --port 4040
+```
+
+## CLI Options
+
+- `--root `: root folder containing MindCache files (required)
+- `--host `: HTTP host (default `127.0.0.1`)
+- `--port `: HTTP port (default `4040`)
+- `--extensions `: comma-separated extensions (default `.md,.markdown,.mindcache,.json`)
+- `--poll-interval `: fallback polling interval (default `5000`, `0` disables)
+- `--debounce `: watch debounce delay (default `200`)
+- `--auth-token `: require `Authorization: Bearer ` for all endpoints except `/health`
+- `--no-watch`: disable filesystem watchers
+
+## API
+
+### Health
+
+```http
+GET /health
+```
+
+### List tags
+
+```http
+GET /v1/tags
+```
+
+### Get tags for one key
+
+```http
+GET /v1/key-tags?key=team/a/memory.md::customer_profile
+```
+
+### Find keys by tags
+
+```http
+GET /v1/keys?tags=project,customer&match=all
+```
+
+- `match=all` means key must contain all tags.
+- `match=any` means key can contain any tag.
+
+### Get entries
+
+```http
+GET /v1/entries?tags=project-a&match=all
+```
+
+### Get context window
+
+```http
+GET /v1/context-window?tags=project-a,customer&match=all
+```
+
+or
+
+```http
+POST /v1/context-window
+Content-Type: application/json
+
+{
+ "tags": ["project-a", "customer"],
+ "match": "all"
+}
+```
+
+### Force reconcile
+
+```http
+POST /v1/reconcile
+```
+
+## Programmatic Usage
+
+```ts
+import { MindCacheApiServer, MindCacheServerCore } from '@mindcache/mindcache-server';
+
+const core = new MindCacheServerCore({
+ rootDir: '/data/mindcache-files',
+ watch: true
+});
+
+const api = new MindCacheApiServer({
+ core,
+ host: '127.0.0.1',
+ port: 4040
+});
+
+await core.start();
+await api.start();
+```
diff --git a/packages/mindcache-server/package.json b/packages/mindcache-server/package.json
new file mode 100644
index 0000000..eaabb54
--- /dev/null
+++ b/packages/mindcache-server/package.json
@@ -0,0 +1,63 @@
+{
+ "name": "@mindcache/mindcache-server",
+ "version": "0.1.0",
+ "description": "Local filesystem-backed MindCache API server",
+ "main": "./dist/index.js",
+ "module": "./dist/index.mjs",
+ "types": "./dist/index.d.ts",
+ "exports": {
+ ".": {
+ "types": "./dist/index.d.ts",
+ "import": "./dist/index.mjs",
+ "require": "./dist/index.js"
+ }
+ },
+ "bin": {
+ "mindcache-server": "./dist/cli.js"
+ },
+ "files": [
+ "dist/**/*",
+ "README.md"
+ ],
+ "scripts": {
+ "build": "tsup",
+ "dev": "tsup --watch",
+ "typecheck": "tsc --noEmit",
+ "lint": "eslint src/**/*.ts tests/**/*.ts",
+ "test": "vitest run",
+ "test:watch": "vitest",
+ "clean": "rm -rf dist"
+ },
+ "keywords": [
+ "mindcache",
+ "server",
+ "local",
+ "filesystem",
+ "api"
+ ],
+ "author": "MindCache Team",
+ "license": "MIT",
+ "repository": {
+ "type": "git",
+ "url": "git+https://github.com/dh7/mindcache.git",
+ "directory": "packages/mindcache-server"
+ },
+ "dependencies": {
+ "mindcache": "workspace:*"
+ },
+ "devDependencies": {
+ "@types/node": "^20.0.0",
+ "@typescript-eslint/eslint-plugin": "^6.21.0",
+ "@typescript-eslint/parser": "^6.21.0",
+ "eslint": "^8.57.0",
+ "tsup": "^8.0.0",
+ "typescript": "^5.0.0",
+ "vitest": "^2.1.9"
+ },
+ "engines": {
+ "node": ">=18.0.0"
+ },
+ "publishConfig": {
+ "access": "public"
+ }
+}
diff --git a/packages/mindcache-server/src/MindCacheApiServer.ts b/packages/mindcache-server/src/MindCacheApiServer.ts
new file mode 100644
index 0000000..a55519b
--- /dev/null
+++ b/packages/mindcache-server/src/MindCacheApiServer.ts
@@ -0,0 +1,325 @@
+import { createServer, type IncomingMessage, type Server, type ServerResponse } from 'node:http';
+import { URL } from 'node:url';
+
+import type {
+ ContextWindowQuery,
+ Logger,
+ MindCacheApiServerOptions,
+ MindCacheServerCoreApi,
+ TagMatchMode
+} from './types';
+
+const defaultLogger: Logger = {
+ info: (message: string) => {
+ // eslint-disable-next-line no-console
+ console.log(message);
+ },
+ warn: (message: string) => {
+ // eslint-disable-next-line no-console
+ console.warn(message);
+ },
+ error: (message: string) => {
+ // eslint-disable-next-line no-console
+ console.error(message);
+ }
+};
+
+interface ParsedRequest {
+ pathname: string;
+ query: URLSearchParams;
+}
+
+export class MindCacheApiServer {
+ private readonly core: MindCacheServerCoreApi;
+ private readonly host: string;
+ private readonly port: number;
+ private readonly authToken?: string;
+ private readonly logger: Logger;
+ private server: Server | null = null;
+
+ constructor(options: MindCacheApiServerOptions) {
+ this.core = options.core;
+ this.host = options.host || '127.0.0.1';
+ this.port = options.port || 4040;
+ this.authToken = options.authToken;
+ this.logger = options.logger || defaultLogger;
+ }
+
+ async start(): Promise {
+ if (this.server) {
+ return;
+ }
+
+ this.server = createServer((req, res) => {
+ void this.handleRequest(req, res);
+ });
+
+ await new Promise((resolve, reject) => {
+ if (!this.server) {
+ reject(new Error('Server not initialized'));
+ return;
+ }
+
+ this.server.once('error', reject);
+ this.server.listen(this.port, this.host, () => {
+ this.server?.off('error', reject);
+ resolve();
+ });
+ });
+
+ this.logger.info(`[mindcache-server] API listening on http://${this.host}:${this.port}`);
+ }
+
+ async stop(): Promise {
+ if (!this.server) {
+ return;
+ }
+
+ const server = this.server;
+ this.server = null;
+
+ await new Promise((resolve, reject) => {
+ server.close(error => {
+ if (error) {
+ reject(error);
+ return;
+ }
+ resolve();
+ });
+ });
+ }
+
+ private async handleRequest(req: IncomingMessage, res: ServerResponse): Promise {
+ this.applyCorsHeaders(res);
+
+ if (req.method === 'OPTIONS') {
+ res.writeHead(204);
+ res.end();
+ return;
+ }
+
+ const parsed = this.parseRequest(req);
+ if (!parsed) {
+ this.sendJson(res, 400, { error: 'Invalid URL' });
+ return;
+ }
+
+ if (parsed.pathname !== '/health' && parsed.pathname !== '/' && !this.isAuthorized(req)) {
+ this.sendJson(res, 401, { error: 'Unauthorized' });
+ return;
+ }
+
+ try {
+ if (req.method === 'GET' && parsed.pathname === '/') {
+ this.sendJson(res, 200, {
+ name: 'mindcache-server',
+ status: 'ok',
+ endpoints: [
+ 'GET /health',
+ 'GET /v1/stats',
+ 'GET /v1/tags',
+ 'GET /v1/key-tags?key=',
+ 'GET /v1/keys?tags=tag1,tag2&match=all|any',
+ 'GET /v1/entries?tags=tag1,tag2&match=all|any',
+ 'GET /v1/context-window?tags=tag1,tag2&match=all|any',
+ 'POST /v1/context-window',
+ 'POST /v1/reconcile'
+ ]
+ });
+ return;
+ }
+
+ if (req.method === 'GET' && parsed.pathname === '/health') {
+ this.sendJson(res, 200, {
+ status: 'ok',
+ ...this.core.getStats()
+ });
+ return;
+ }
+
+ if (req.method === 'GET' && parsed.pathname === '/v1/stats') {
+ this.sendJson(res, 200, this.core.getStats());
+ return;
+ }
+
+ if (req.method === 'GET' && parsed.pathname === '/v1/tags') {
+ this.sendJson(res, 200, { tags: this.core.getAllTags() });
+ return;
+ }
+
+ if (req.method === 'GET' && parsed.pathname === '/v1/key-tags') {
+ const key = parsed.query.get('key') || '';
+ if (!key) {
+ this.sendJson(res, 400, { error: 'Missing required query parameter: key' });
+ return;
+ }
+ this.sendJson(res, 200, {
+ key,
+ tags: this.core.getTagsForKey(key)
+ });
+ return;
+ }
+
+ if (req.method === 'GET' && parsed.pathname === '/v1/keys') {
+ const query = this.queryFromSearchParams(parsed.query);
+ const keys = this.core.findKeysByTags(query.tags || [], query.match);
+ this.sendJson(res, 200, {
+ tags: query.tags || [],
+ match: query.match,
+ count: keys.length,
+ keys
+ });
+ return;
+ }
+
+ if (req.method === 'GET' && parsed.pathname === '/v1/entries') {
+ const query = this.queryFromSearchParams(parsed.query);
+ const entries = this.core.listEntries(query);
+ this.sendJson(res, 200, {
+ tags: query.tags || [],
+ match: query.match,
+ count: entries.length,
+ entries
+ });
+ return;
+ }
+
+ if (req.method === 'GET' && parsed.pathname === '/v1/context-window') {
+ const query = this.queryFromSearchParams(parsed.query);
+ const result = this.core.getContextWindow(query);
+ this.sendJson(res, 200, result);
+ return;
+ }
+
+ if (req.method === 'POST' && parsed.pathname === '/v1/context-window') {
+ const body = await this.readBody(req);
+ const query = this.queryFromBody(body);
+ const result = this.core.getContextWindow(query);
+ this.sendJson(res, 200, result);
+ return;
+ }
+
+ if (req.method === 'POST' && parsed.pathname === '/v1/reconcile') {
+ await this.core.reconcile('api');
+ this.sendJson(res, 200, {
+ ok: true,
+ ...this.core.getStats()
+ });
+ return;
+ }
+
+ this.sendJson(res, 404, { error: 'Not found' });
+ } catch (error) {
+ const message = error instanceof Error ? error.message : String(error);
+ const isBadRequest = message === 'Invalid JSON body';
+ this.logger.error(`[mindcache-server] Request error: ${error instanceof Error ? error.message : String(error)}`);
+ this.sendJson(res, isBadRequest ? 400 : 500, {
+ error: isBadRequest ? message : (error instanceof Error ? error.message : 'Internal server error')
+ });
+ }
+ }
+
+ private parseRequest(req: IncomingMessage): ParsedRequest | null {
+ if (!req.url) {
+ return null;
+ }
+
+ try {
+ const url = new URL(req.url, 'http://localhost');
+ return {
+ pathname: url.pathname,
+ query: url.searchParams
+ };
+ } catch {
+ return null;
+ }
+ }
+
+ private queryFromSearchParams(params: URLSearchParams): ContextWindowQuery {
+ const tags = this.parseTags(params.get('tags'));
+ const match = this.parseMatch(params.get('match'));
+ return { tags, match };
+ }
+
+ private queryFromBody(body: unknown): ContextWindowQuery {
+ const object = body && typeof body === 'object' ? body as Record : {};
+ const tagsValue = object.tags;
+ const tags = Array.isArray(tagsValue)
+ ? tagsValue.filter(tag => typeof tag === 'string') as string[]
+ : [];
+
+ const match = this.parseMatch(typeof object.match === 'string' ? object.match : null);
+
+ return { tags, match };
+ }
+
+ private parseTags(raw: string | null): string[] {
+ if (!raw) {
+ return [];
+ }
+
+ return raw
+ .split(',')
+ .map(tag => tag.trim())
+ .filter(Boolean);
+ }
+
+ private parseMatch(raw: string | null): TagMatchMode {
+ if (raw === 'any') {
+ return 'any';
+ }
+ return 'all';
+ }
+
+ private async readBody(req: IncomingMessage): Promise {
+ const chunks: Buffer[] = [];
+
+ for await (const chunk of req) {
+ chunks.push(typeof chunk === 'string' ? Buffer.from(chunk) : chunk);
+ }
+
+ if (chunks.length === 0) {
+ return {};
+ }
+
+ const rawBody = Buffer.concat(chunks).toString('utf8');
+ if (!rawBody.trim()) {
+ return {};
+ }
+
+ try {
+ return JSON.parse(rawBody);
+ } catch {
+ throw new Error('Invalid JSON body');
+ }
+ }
+
+ private isAuthorized(req: IncomingMessage): boolean {
+ if (!this.authToken) {
+ return true;
+ }
+
+ const authHeader = req.headers.authorization || '';
+ if (!authHeader.startsWith('Bearer ')) {
+ return false;
+ }
+
+ const token = authHeader.slice('Bearer '.length).trim();
+ return token === this.authToken;
+ }
+
+ private sendJson(res: ServerResponse, statusCode: number, payload: unknown): void {
+ const body = JSON.stringify(payload, null, 2);
+ res.writeHead(statusCode, {
+ 'Content-Type': 'application/json; charset=utf-8',
+ 'Content-Length': Buffer.byteLength(body)
+ });
+ res.end(body);
+ }
+
+ private applyCorsHeaders(res: ServerResponse): void {
+ res.setHeader('Access-Control-Allow-Origin', '*');
+ res.setHeader('Access-Control-Allow-Headers', 'Authorization, Content-Type');
+ res.setHeader('Access-Control-Allow-Methods', 'GET,POST,OPTIONS');
+ }
+}
diff --git a/packages/mindcache-server/src/MindCacheServerCore.ts b/packages/mindcache-server/src/MindCacheServerCore.ts
new file mode 100644
index 0000000..3b17ecd
--- /dev/null
+++ b/packages/mindcache-server/src/MindCacheServerCore.ts
@@ -0,0 +1,613 @@
+import { promises as fs, watch as fsWatch, type FSWatcher } from 'node:fs';
+import path from 'node:path';
+
+import { MindCache } from 'mindcache/server';
+
+import {
+ normalizeExtensions,
+ parseMindCacheFile,
+ readFileText,
+ toCanonicalKey,
+ toFileId
+} from './fileUtils';
+import type {
+ ContextWindowQuery,
+ ContextWindowResult,
+ EntrySnapshot,
+ FileMetadata,
+ FileSnapshot,
+ IndexedEntry,
+ Logger,
+ MindCacheServerCoreApi,
+ MindCacheServerCoreOptions,
+ MindCacheServerStats,
+ TagMatchMode
+} from './types';
+
+const DEFAULT_EXTENSIONS = ['.md', '.markdown', '.mindcache', '.json'];
+const DEFAULT_POLL_INTERVAL_MS = 5000;
+const DEFAULT_DEBOUNCE_MS = 200;
+
+const defaultLogger: Logger = {
+ info: (message: string) => {
+ // eslint-disable-next-line no-console
+ console.log(message);
+ },
+ warn: (message: string) => {
+ // eslint-disable-next-line no-console
+ console.warn(message);
+ },
+ error: (message: string) => {
+ // eslint-disable-next-line no-console
+ console.error(message);
+ },
+ debug: (message: string) => {
+ // eslint-disable-next-line no-console
+ console.debug(message);
+ }
+};
+
+export class MindCacheServerCore implements MindCacheServerCoreApi {
+ private readonly rootDir: string;
+ private readonly includeExtensions: Set;
+ private readonly watchEnabled: boolean;
+ private readonly pollIntervalMs: number;
+ private readonly debounceMs: number;
+ private readonly logger: Logger;
+
+ private readonly aggregate = new MindCache({ accessLevel: 'admin' });
+
+ private fileSnapshots = new Map();
+ private fileMetadata = new Map();
+ private tagIndex = new Map>();
+ private rawKeyOwners = new Map>();
+ private canonicalKeySources = new Map();
+
+ private watcher: FSWatcher | null = null;
+ private directoryWatchers = new Map();
+ private usingRecursiveWatcher = false;
+ private reconcileTimer: ReturnType | null = null;
+ private pollTimer: ReturnType | null = null;
+
+ private started = false;
+ private reconcileInFlight = false;
+ private reconcileQueued = false;
+ private lastReconcileAt: Date | null = null;
+
+ constructor(options: MindCacheServerCoreOptions) {
+ if (!options.rootDir) {
+ throw new Error('rootDir is required');
+ }
+
+ this.rootDir = path.resolve(options.rootDir);
+ this.includeExtensions = normalizeExtensions(options.includeExtensions || DEFAULT_EXTENSIONS);
+ this.watchEnabled = options.watch ?? true;
+ this.pollIntervalMs = options.pollIntervalMs ?? DEFAULT_POLL_INTERVAL_MS;
+ this.debounceMs = options.debounceMs ?? DEFAULT_DEBOUNCE_MS;
+ this.logger = options.logger || defaultLogger;
+ }
+
+ async start(): Promise {
+ if (this.started) {
+ return;
+ }
+
+ await fs.mkdir(this.rootDir, { recursive: true });
+ await this.reconcile('startup');
+
+ if (this.watchEnabled) {
+ await this.startWatchers();
+ }
+
+ if (this.pollIntervalMs > 0) {
+ this.pollTimer = setInterval(() => {
+ this.scheduleReconcile('poll');
+ }, this.pollIntervalMs);
+ this.pollTimer.unref();
+ }
+
+ this.started = true;
+ }
+
+ async stop(): Promise {
+ if (this.reconcileTimer) {
+ clearTimeout(this.reconcileTimer);
+ this.reconcileTimer = null;
+ }
+
+ if (this.pollTimer) {
+ clearInterval(this.pollTimer);
+ this.pollTimer = null;
+ }
+
+ if (this.watcher) {
+ this.watcher.close();
+ this.watcher = null;
+ }
+
+ for (const watcher of this.directoryWatchers.values()) {
+ watcher.close();
+ }
+ this.directoryWatchers.clear();
+
+ this.started = false;
+ }
+
+ get mindcache(): MindCache {
+ return this.aggregate;
+ }
+
+ get rootPath(): string {
+ return this.rootDir;
+ }
+
+ getAllTags(): string[] {
+ return Array.from(this.tagIndex.keys()).sort((a, b) => a.localeCompare(b));
+ }
+
+ getTagsForKey(canonicalKey: string): string[] {
+ return this.aggregate.getTags(canonicalKey).sort((a, b) => a.localeCompare(b));
+ }
+
+ getContextWindow(query: ContextWindowQuery = {}): ContextWindowResult {
+ const tags = this.normalizeTags(query.tags || []);
+ const match = query.match || 'all';
+ const keys = this.findKeysByTags(tags, match);
+ const scopedCache = new MindCache({ accessLevel: 'admin' });
+
+ for (const key of keys) {
+ const attributes = this.aggregate.get_attributes(key);
+ if (!attributes) {
+ continue;
+ }
+ const value = this.aggregate.get_value(key);
+ scopedCache.set_value(key, value, attributes);
+ }
+
+ const visibleKeys = keys.filter(key => {
+ const attributes = this.aggregate.get_attributes(key);
+ return Boolean(
+ attributes?.systemTags.includes('SystemPrompt') ||
+ attributes?.systemTags.includes('LLMRead')
+ );
+ });
+
+ return {
+ tags,
+ match,
+ keys,
+ visibleKeys,
+ contextWindow: scopedCache.get_system_prompt()
+ };
+ }
+
+ findKeysByTags(tags: string[], match: TagMatchMode = 'all'): string[] {
+ const normalizedTags = this.normalizeTags(tags);
+
+ if (normalizedTags.length === 0) {
+ return this.aggregate.keys().sort((a, b) => a.localeCompare(b));
+ }
+
+ if (match === 'any') {
+ const union = new Set();
+ for (const tag of normalizedTags) {
+ const keys = this.tagIndex.get(tag);
+ if (!keys) {
+ continue;
+ }
+ for (const key of keys) {
+ union.add(key);
+ }
+ }
+ return Array.from(union).sort((a, b) => a.localeCompare(b));
+ }
+
+ const sets: Set[] = [];
+ for (const tag of normalizedTags) {
+ const keys = this.tagIndex.get(tag);
+ if (!keys) {
+ return [];
+ }
+ sets.push(keys);
+ }
+
+ sets.sort((a, b) => a.size - b.size);
+ const [first, ...rest] = sets;
+ const intersection = new Set(first);
+
+ for (const set of rest) {
+ for (const key of intersection) {
+ if (!set.has(key)) {
+ intersection.delete(key);
+ }
+ }
+ if (intersection.size === 0) {
+ return [];
+ }
+ }
+
+ return Array.from(intersection).sort((a, b) => a.localeCompare(b));
+ }
+
+ listEntries(query: ContextWindowQuery = {}): IndexedEntry[] {
+ const tags = this.normalizeTags(query.tags || []);
+ const match = query.match || 'all';
+ const keys = this.findKeysByTags(tags, match);
+ const entries: IndexedEntry[] = [];
+
+ for (const key of keys) {
+ const entry = this.getEntry(key);
+ if (entry) {
+ entries.push(entry);
+ }
+ }
+
+ return entries;
+ }
+
+ getEntry(canonicalKey: string): IndexedEntry | undefined {
+ const source = this.canonicalKeySources.get(canonicalKey);
+ if (!source) {
+ return undefined;
+ }
+
+ const attributes = this.aggregate.get_attributes(canonicalKey);
+ if (!attributes) {
+ return undefined;
+ }
+
+ return {
+ key: canonicalKey,
+ rawKey: source.rawKey,
+ fileId: source.fileId,
+ value: this.aggregate.get_value(canonicalKey),
+ attributes
+ };
+ }
+
+ getStats(): MindCacheServerStats {
+ return {
+ rootDir: this.rootDir,
+ files: this.fileSnapshots.size,
+ keys: this.aggregate.size(),
+ tags: this.tagIndex.size,
+ watchEnabled: this.watchEnabled,
+ started: this.started,
+ lastReconcileAt: this.lastReconcileAt ? this.lastReconcileAt.toISOString() : null
+ };
+ }
+
+ async reconcile(reason = 'manual'): Promise {
+ if (this.reconcileInFlight) {
+ this.reconcileQueued = true;
+ return;
+ }
+
+ this.reconcileInFlight = true;
+
+ try {
+ const discoveredFiles = await this.scanMindCacheFiles();
+ const nextMetadata = new Map();
+
+ for (const [fileId, metadata] of discoveredFiles) {
+ const previousMetadata = this.fileMetadata.get(fileId);
+ const changed = !previousMetadata ||
+ previousMetadata.mtimeMs !== metadata.mtimeMs ||
+ previousMetadata.size !== metadata.size ||
+ previousMetadata.absolutePath !== metadata.absolutePath;
+
+ if (!changed) {
+ if (previousMetadata) {
+ nextMetadata.set(fileId, previousMetadata);
+ }
+ continue;
+ }
+
+ const loaded = await this.reloadFile(metadata);
+ if (loaded) {
+ nextMetadata.set(fileId, metadata);
+ } else if (previousMetadata) {
+ nextMetadata.set(fileId, previousMetadata);
+ }
+ }
+
+ for (const fileId of this.fileMetadata.keys()) {
+ if (!discoveredFiles.has(fileId)) {
+ this.removeFile(fileId);
+ }
+ }
+
+ this.fileMetadata = nextMetadata;
+ this.lastReconcileAt = new Date();
+ this.logger.debug?.(`[mindcache-server] Reconcile complete (${reason}). files=${this.fileSnapshots.size} keys=${this.aggregate.size()}`);
+
+ if (this.watchEnabled && !this.usingRecursiveWatcher) {
+ await this.syncDirectoryWatchers();
+ }
+ } catch (error) {
+ this.logger.error(`[mindcache-server] Reconcile failed (${reason}): ${error instanceof Error ? error.message : String(error)}`);
+ throw error;
+ } finally {
+ this.reconcileInFlight = false;
+ if (this.reconcileQueued) {
+ this.reconcileQueued = false;
+ void this.reconcile('queued');
+ }
+ }
+ }
+
+ private normalizeTags(tags: string[]): string[] {
+ const unique = new Set();
+ for (const tag of tags) {
+ const cleaned = tag.trim();
+ if (cleaned) {
+ unique.add(cleaned);
+ }
+ }
+ return Array.from(unique).sort((a, b) => a.localeCompare(b));
+ }
+
+ private async reloadFile(metadata: FileMetadata): Promise {
+ try {
+ const content = await readFileText(metadata.absolutePath);
+ const nextEntries = parseMindCacheFile(content, metadata.absolutePath);
+ const previousSnapshot = this.fileSnapshots.get(metadata.fileId);
+ const previousEntries = previousSnapshot?.entries || new Map();
+
+ this.applyFileDiff(metadata.fileId, previousEntries, nextEntries);
+
+ this.fileSnapshots.set(metadata.fileId, {
+ fileId: metadata.fileId,
+ absolutePath: metadata.absolutePath,
+ mtimeMs: metadata.mtimeMs,
+ size: metadata.size,
+ entries: nextEntries
+ });
+
+ return true;
+ } catch (error) {
+ this.logger.warn(`[mindcache-server] Failed to load ${metadata.absolutePath}: ${error instanceof Error ? error.message : String(error)}`);
+ return false;
+ }
+ }
+
+ private removeFile(fileId: string): void {
+ const snapshot = this.fileSnapshots.get(fileId);
+ if (!snapshot) {
+ this.fileMetadata.delete(fileId);
+ return;
+ }
+
+ for (const entry of snapshot.entries.values()) {
+ this.removeEntry(fileId, entry);
+ }
+
+ this.fileSnapshots.delete(fileId);
+ this.fileMetadata.delete(fileId);
+ }
+
+ private applyFileDiff(fileId: string, previous: Map, next: Map): void {
+ for (const [rawKey, oldEntry] of previous) {
+ if (!next.has(rawKey)) {
+ this.removeEntry(fileId, oldEntry);
+ }
+ }
+
+ for (const [rawKey, newEntry] of next) {
+ const oldEntry = previous.get(rawKey);
+ if (!oldEntry || oldEntry.hash !== newEntry.hash) {
+ this.upsertEntry(fileId, newEntry, oldEntry);
+ }
+ }
+ }
+
+ private upsertEntry(fileId: string, entry: EntrySnapshot, previous?: EntrySnapshot): void {
+ const canonicalKey = toCanonicalKey(fileId, entry.rawKey);
+
+ this.aggregate.set_value(canonicalKey, entry.value, entry.attributes);
+ this.canonicalKeySources.set(canonicalKey, { fileId, rawKey: entry.rawKey });
+
+ this.updateTagIndex(
+ canonicalKey,
+ previous?.attributes.contentTags || [],
+ entry.attributes.contentTags || []
+ );
+
+ if (!this.rawKeyOwners.has(entry.rawKey)) {
+ this.rawKeyOwners.set(entry.rawKey, new Set());
+ }
+ this.rawKeyOwners.get(entry.rawKey)?.add(canonicalKey);
+ }
+
+ private removeEntry(fileId: string, entry: EntrySnapshot): void {
+ const canonicalKey = toCanonicalKey(fileId, entry.rawKey);
+
+ this.aggregate.delete(canonicalKey);
+ this.canonicalKeySources.delete(canonicalKey);
+ this.updateTagIndex(canonicalKey, entry.attributes.contentTags || [], []);
+
+ const owners = this.rawKeyOwners.get(entry.rawKey);
+ if (owners) {
+ owners.delete(canonicalKey);
+ if (owners.size === 0) {
+ this.rawKeyOwners.delete(entry.rawKey);
+ }
+ }
+ }
+
+ private updateTagIndex(canonicalKey: string, oldTags: string[], newTags: string[]): void {
+ const oldSet = new Set(oldTags);
+ const newSet = new Set(newTags);
+
+ for (const tag of oldSet) {
+ if (newSet.has(tag)) {
+ continue;
+ }
+ const keys = this.tagIndex.get(tag);
+ if (!keys) {
+ continue;
+ }
+ keys.delete(canonicalKey);
+ if (keys.size === 0) {
+ this.tagIndex.delete(tag);
+ }
+ }
+
+ for (const tag of newSet) {
+ if (!this.tagIndex.has(tag)) {
+ this.tagIndex.set(tag, new Set());
+ }
+ this.tagIndex.get(tag)?.add(canonicalKey);
+ }
+ }
+
+ private async scanMindCacheFiles(): Promise