Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
47 changes: 47 additions & 0 deletions examples/mindcache_server_local/README.md
Original file line number Diff line number Diff line change
@@ -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.
161 changes: 161 additions & 0 deletions examples/mindcache_server_local/client/app.js
Original file line number Diff line number Diff line change
@@ -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('<', '&lt;')
.replaceAll('>', '&gt;')
.replaceAll('"', '&quot;')
.replaceAll("'", '&#039;');
}

function apiBase() {
return apiBaseInput.value.trim().replace(/\/$/, '');
}

function renderTags() {
if (state.allTags.length === 0) {
tagsEl.innerHTML = '<span class="hint">No tags found.</span>';
return;
}

tagsEl.innerHTML = state.allTags
.map((tag) => {
const active = state.selectedTags.has(tag);
const className = active ? 'tag active' : 'tag';
return `<button class="${className}" data-tag="${escapeHtml(tag)}">${escapeHtml(tag)}</button>`;
})
.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) => `<span class="tag">${escapeHtml(tag)}</span>`).join(' ');
return `
<article class="item">
<div class="key">${escapeHtml(entry.key)}</div>
<div class="hint">raw key: ${escapeHtml(entry.rawKey)} | file: ${escapeHtml(entry.fileId)}</div>
<div class="tags" style="margin-top:8px;">${tags || '<span class="hint">no tags</span>'}</div>
</article>
`;
})
.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();
174 changes: 174 additions & 0 deletions examples/mindcache_server_local/client/index.html
Original file line number Diff line number Diff line change
@@ -0,0 +1,174 @@
<!doctype html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>MindCache Server Local Client</title>
<style>
:root {
--bg: #f7f3ea;
--card: #fffdf7;
--ink: #1d1a17;
--ink-soft: #60594f;
--line: #d8cec0;
--accent: #145f55;
--accent-soft: #d5efe9;
--chip: #efe6d9;
}

* {
box-sizing: border-box;
}

body {
margin: 0;
font-family: "Space Grotesk", "Avenir Next", "Segoe UI", sans-serif;
color: var(--ink);
background: radial-gradient(circle at 0% 0%, #efe0ca 0%, transparent 36%),
radial-gradient(circle at 100% 0%, #d9eee7 0%, transparent 36%),
var(--bg);
min-height: 100vh;
}

main {
max-width: 1000px;
margin: 0 auto;
padding: 24px 16px 48px;
}

h1 {
margin: 0;
font-size: 1.9rem;
}

.sub {
margin-top: 8px;
color: var(--ink-soft);
}

.card {
background: var(--card);
border: 1px solid var(--line);
border-radius: 16px;
padding: 16px;
margin-top: 16px;
}

.row {
display: flex;
gap: 12px;
flex-wrap: wrap;
align-items: center;
}

input,
select,
button {
font: inherit;
}

input[type="text"],
select {
border: 1px solid var(--line);
border-radius: 10px;
padding: 8px 10px;
background: #fff;
color: var(--ink);
}

button {
border: 0;
border-radius: 10px;
padding: 9px 12px;
cursor: pointer;
background: var(--accent);
color: #f6fffd;
}

button.secondary {
background: #1f1c18;
}

.tags {
display: flex;
flex-wrap: wrap;
gap: 8px;
margin-top: 12px;
}

.tag {
border: 1px solid var(--line);
background: var(--chip);
color: var(--ink);
border-radius: 999px;
padding: 6px 10px;
}

.tag.active {
border-color: #0f7667;
background: var(--accent-soft);
color: #08423a;
}

.list {
margin-top: 12px;
display: grid;
gap: 10px;
}

.item {
border: 1px solid var(--line);
border-radius: 12px;
padding: 10px;
background: #fff;
}

.key {
font-family: "IBM Plex Mono", "SF Mono", monospace;
font-size: 0.9rem;
}

.hint {
color: var(--ink-soft);
font-size: 0.95rem;
}

@media (max-width: 760px) {
h1 {
font-size: 1.5rem;
}
}
</style>
</head>
<body>
<main>
<h1>MindCache Server Tag Explorer</h1>
<p class="sub">Select one or more tags to see matching keys from the local API.</p>

<section class="card">
<div class="row">
<label for="apiBase">API URL</label>
<input id="apiBase" type="text" value="http://127.0.0.1:4040" size="35" />
<label for="matchMode">Match</label>
<select id="matchMode">
<option value="all">all tags</option>
<option value="any">any tag</option>
</select>
<button id="refreshBtn">Refresh</button>
<button id="clearBtn" class="secondary">Clear tags</button>
</div>

<div id="status" class="hint" style="margin-top: 12px;">Loading tags...</div>
<div id="tags" class="tags"></div>
</section>

<section class="card">
<h2 style="margin-top: 0; font-size: 1.2rem;">Matching Keys</h2>
<div id="summary" class="hint">No data yet.</div>
<div id="entries" class="list"></div>
</section>
</main>

<script type="module" src="/app.js"></script>
</body>
</html>
Loading
Loading