feat(memory): v7 — Gedächtnis-Layer mit MCP-Integration
SQLite-basiertes Memory-System fuer persistentes Gedaechtnis ueber Sessions. Cline, OpenCode und Claude Code teilen denselben Speicher via MCP-Server. - routers/memory.py: CRUD + Export-Endpoint (GET/POST/PUT/DELETE /api/memory) - mcp_memory.py: stdio MCP-Server — Tools: get/add/search/update/delete_memory - MemoryPanel.svelte: Gedaechtnis-Tab mit Filter, Inline-Edit, Add-Formular - ConnectPanel.svelte: Gedaechtnis-MCP Setup-Guide (Cline/OpenCode/Bosgame) - Temporaere Eintraege (ephemeral) nach 7 Tagen automatisch geloescht Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
@@ -15,6 +15,7 @@ import CookbookPanel from './panels/CookbookPanel.svelte'
|
||||
import ConnectPanel from './panels/ConnectPanel.svelte'
|
||||
import NewsPanel from './panels/NewsPanel.svelte'
|
||||
import GuidesPanel from './panels/GuidesPanel.svelte'
|
||||
import MemoryPanel from './panels/MemoryPanel.svelte'
|
||||
|
||||
let prevModelStates: Record<string, string> = {}
|
||||
|
||||
@@ -156,6 +157,7 @@ const panels: [string, any][] = [
|
||||
['connect', ConnectPanel],
|
||||
['news', NewsPanel],
|
||||
['guides', GuidesPanel],
|
||||
['memory', MemoryPanel],
|
||||
]
|
||||
for (const [view, Panel] of panels) {
|
||||
const el = document.querySelector(`.view[data-view="${view}"]`)
|
||||
|
||||
@@ -11,6 +11,8 @@
|
||||
let selectedModelIds = $state<string[]>([])
|
||||
let testResult = $state('')
|
||||
let testOk = $state(false)
|
||||
let selectedMemTool = $state('cline')
|
||||
const memBlock = $derived(memMcpBlock(selectedMemTool))
|
||||
|
||||
function baseUrl() { return `http://${location.hostname}:8080/v1` }
|
||||
function isLanIp(h: string) { return /^\d{1,3}(\.\d{1,3}){3}$/.test(h) }
|
||||
@@ -71,6 +73,16 @@
|
||||
function copyVal(inp: HTMLInputElement) {
|
||||
navigator.clipboard?.writeText(inp.value)
|
||||
}
|
||||
|
||||
function memMcpBlock(tool: string) {
|
||||
const pyPath = tool === 'bosgame'
|
||||
? '/opt/mission-control/.venv/bin/python'
|
||||
: 'python'
|
||||
const scriptPath = tool === 'bosgame'
|
||||
? '/opt/mission-control/mcp_memory.py'
|
||||
: 'C:\\\\Users\\\\TobisPC\\\\mission-control\\\\mcp_memory.py'
|
||||
return `{\n "mcpServers": {\n "mission-control-memory": {\n "command": "${pyPath}",\n "args": ["${scriptPath}"],\n "env": {\n "MC_URL": "http://192.168.178.151:9000"\n }\n }\n }\n}`
|
||||
}
|
||||
</script>
|
||||
|
||||
<div class="pagehead">
|
||||
@@ -308,3 +320,56 @@
|
||||
</div>
|
||||
</div>
|
||||
{/if}
|
||||
|
||||
<!-- Memory MCP — unabhaengig von Modellen, immer sichtbar -->
|
||||
<div class="card">
|
||||
<div class="card-h">
|
||||
<h3>Gedächtnis-MCP</h3>
|
||||
<span class="chip" style="background:rgba(45,212,191,.1);border-color:rgba(45,212,191,.3);color:var(--accent)">Neu in v7</span>
|
||||
</div>
|
||||
<div class="card-sub" style="margin-bottom:14px">
|
||||
Damit Cline, OpenCode und Claude Code sich Projektfakten, Entscheidungen und Präferenzen
|
||||
über Sessions hinweg merken — geteilt über alle Tools, verwaltet im
|
||||
<a href="#"
|
||||
onclick={e => { e.preventDefault(); (document.querySelector(".nav-item[data-view='memory']") as HTMLElement)?.click() }}
|
||||
style="color:var(--accent)">Gedächtnis-Tab</a>.
|
||||
</div>
|
||||
|
||||
<!-- Tool selector -->
|
||||
<div class="flex gap-2" style="margin-bottom:14px;flex-wrap:wrap">
|
||||
{#each [
|
||||
['cline', 'Cline / Claude Code', 'Windows — Tools auf dem Entwicklungs-PC'],
|
||||
['opencode', 'OpenCode', 'Windows — Terminal-Agent'],
|
||||
['bosgame', 'Direkt am Bosgame', 'Wenn das Tool am Server läuft'],
|
||||
] as [id, label, desc]}
|
||||
<button class="card-btn{selectedMemTool === id ? ' cb-best' : ''}"
|
||||
style="text-align:left;padding:10px 14px"
|
||||
onclick={() => selectedMemTool = id}>
|
||||
<div style="font-weight:500;font-size:13px">{label}</div>
|
||||
<div class="card-sub" style="margin:2px 0 0;font-size:11.5px">{desc}</div>
|
||||
</button>
|
||||
{/each}
|
||||
</div>
|
||||
|
||||
<!-- Config block -->
|
||||
<div style="position:relative">
|
||||
<button class="ghost" style="position:absolute;top:8px;right:8px;z-index:1"
|
||||
onclick={e => copyCode(e.currentTarget!.parentElement!)}>Kopieren</button>
|
||||
<div class="log" style="max-height:none"><code>{memBlock}</code></div>
|
||||
</div>
|
||||
|
||||
{#if selectedMemTool === 'cline'}
|
||||
<p style="margin-top:12px">In <b>Cline</b>: MCP-Einstellungen öffnen (Stecker-Icon in der Cline-Sidebar) → „Add MCP Server" → diesen Block einfügen.</p>
|
||||
<p>In <b>Claude Code</b>: Block in <code>~/.claude/settings.json</code> unter <code>"mcpServers"</code> einfügen.</p>
|
||||
<div class="alert warn" style="margin-top:8px;padding:10px 14px"><span class="a-dot"></span>
|
||||
<span>Einmalig auf dem Windows-PC: <code>pip install mcp httpx</code> ausführen — danach <code>mcp_memory.py</code> aus dem Repo-Ordner erreichbar machen (Pfad im Snippet anpassen).</span>
|
||||
</div>
|
||||
{:else if selectedMemTool === 'opencode'}
|
||||
<p style="margin-top:12px">Block in <code>~/.config/opencode/opencode.json</code> unter <code>"mcpServers"</code> einfügen.</p>
|
||||
<div class="alert warn" style="margin-top:8px;padding:10px 14px"><span class="a-dot"></span>
|
||||
<span>Einmalig: <code>pip install mcp httpx</code> — und Pfad zu <code>mcp_memory.py</code> im Snippet anpassen.</span>
|
||||
</div>
|
||||
{:else}
|
||||
<p style="margin-top:12px">Block in die MCP-Config des jeweiligen Tools einfügen. Der Bosgame-Pfad <code>/opt/mission-control/mcp_memory.py</code> ist bereits korrekt — kein weiteres Setup nötig.</p>
|
||||
{/if}
|
||||
</div>
|
||||
|
||||
@@ -0,0 +1,237 @@
|
||||
<script lang="ts">
|
||||
import { api } from '@core/api.js'
|
||||
import { toast, confirmModal } from '@core/ui.js'
|
||||
|
||||
type Mem = {
|
||||
id: string
|
||||
content: string
|
||||
category: string
|
||||
source: string
|
||||
created_at: string
|
||||
updated_at: string
|
||||
}
|
||||
|
||||
let memories = $state<Mem[]>([])
|
||||
let loading = $state(true)
|
||||
let filter = $state('all')
|
||||
let adding = $state(false)
|
||||
let newContent = $state('')
|
||||
let newCategory = $state('stable')
|
||||
let editId = $state<string | null>(null)
|
||||
let editContent = $state('')
|
||||
let editCategory = $state('stable')
|
||||
|
||||
const filtered = $derived(
|
||||
filter === 'all' ? memories : memories.filter(m => m.category === filter)
|
||||
)
|
||||
|
||||
const CAT: Record<string, { label: string; color: string }> = {
|
||||
stable: { label: 'Stabil', color: 'var(--accent)' },
|
||||
versioned: { label: 'Versioniert', color: '#f59e0b' },
|
||||
ephemeral: { label: 'Temporär', color: 'var(--mut)' },
|
||||
}
|
||||
|
||||
function relTime(iso: string) {
|
||||
const s = Math.floor((Date.now() - new Date(iso).getTime()) / 1000)
|
||||
if (s < 60) return 'gerade eben'
|
||||
if (s < 3600) return `vor ${Math.floor(s / 60)} Min`
|
||||
if (s < 86400) return `vor ${Math.floor(s / 3600)} Std`
|
||||
return `vor ${Math.floor(s / 86400)} Tagen`
|
||||
}
|
||||
|
||||
function countCat(cat: string) {
|
||||
return memories.filter(m => m.category === cat).length
|
||||
}
|
||||
|
||||
async function load() {
|
||||
try { memories = await api('/api/memory') }
|
||||
catch (e: any) { toast('Laden fehlgeschlagen: ' + e.message, true) }
|
||||
finally { loading = false }
|
||||
}
|
||||
|
||||
async function addMem() {
|
||||
if (!newContent.trim()) return
|
||||
try {
|
||||
const m = await api('/api/memory', {
|
||||
method: 'POST',
|
||||
body: JSON.stringify({ content: newContent.trim(), category: newCategory, source: 'manual' })
|
||||
})
|
||||
memories = [m, ...memories]
|
||||
newContent = ''; newCategory = 'stable'; adding = false
|
||||
toast('Gespeichert')
|
||||
} catch (e: any) { toast('Fehler: ' + e.message, true) }
|
||||
}
|
||||
|
||||
async function saveMem(m: Mem) {
|
||||
try {
|
||||
const updated = await api(`/api/memory/${m.id}`, {
|
||||
method: 'PUT',
|
||||
body: JSON.stringify({ content: editContent, category: editCategory })
|
||||
})
|
||||
memories = memories.map(x => x.id === m.id ? updated : x)
|
||||
editId = null
|
||||
toast('Aktualisiert')
|
||||
} catch (e: any) { toast('Fehler: ' + e.message, true) }
|
||||
}
|
||||
|
||||
async function delMem(m: Mem) {
|
||||
const ok = await confirmModal({
|
||||
title: 'Eintrag löschen?',
|
||||
body: `„${m.content.length > 80 ? m.content.slice(0, 80) + '…' : m.content}" wird dauerhaft entfernt.`,
|
||||
danger: true
|
||||
})
|
||||
if (!ok) return
|
||||
try {
|
||||
await api(`/api/memory/${m.id}`, { method: 'DELETE' })
|
||||
memories = memories.filter(x => x.id !== m.id)
|
||||
toast('Gelöscht')
|
||||
} catch (e: any) { toast('Fehler: ' + e.message, true) }
|
||||
}
|
||||
|
||||
function startEdit(m: Mem) {
|
||||
editId = m.id; editContent = m.content; editCategory = m.category
|
||||
}
|
||||
|
||||
$effect(() => { load() })
|
||||
</script>
|
||||
|
||||
<div class="pagehead" style="display:flex;justify-content:space-between;align-items:flex-start">
|
||||
<div>
|
||||
<h1>Gedächtnis</h1>
|
||||
<div class="sub">Fakten, Entscheidungen und Kontext — geteilt von allen KI-Tools via MCP.</div>
|
||||
</div>
|
||||
<button class="primary" style="flex-shrink:0;margin-top:4px"
|
||||
onclick={() => { adding = !adding; newContent = '' }}>
|
||||
{adding ? 'Abbrechen' : '+ Eintrag'}
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<!-- Add form -->
|
||||
{#if adding}
|
||||
<div class="card">
|
||||
<div class="card-h"><h3>Neuer Eintrag</h3></div>
|
||||
<label>Inhalt</label>
|
||||
<textarea
|
||||
rows="3"
|
||||
placeholder="z.B.: Mission Control läuft auf Port 9000 — oder: Wir nutzen SQLite statt Qdrant"
|
||||
style="width:100%;box-sizing:border-box;resize:vertical;margin-bottom:10px"
|
||||
bind:value={newContent}
|
||||
onkeydown={e => { if (e.key === 'Enter' && (e.ctrlKey || e.metaKey)) addMem() }}
|
||||
></textarea>
|
||||
<label>Kategorie</label>
|
||||
<select bind:value={newCategory} style="margin-bottom:14px">
|
||||
<option value="stable">Stabil — Projektfakten, die sich selten ändern</option>
|
||||
<option value="versioned">Versioniert — Tech-Versionen (bei Updates überschreiben)</option>
|
||||
<option value="ephemeral">Temporär — Aktuelle Aufgabe (nach 7 Tagen gelöscht)</option>
|
||||
</select>
|
||||
<div class="btn-row">
|
||||
<button class="primary" onclick={addMem} disabled={!newContent.trim()}>Speichern</button>
|
||||
<button class="ghost" onclick={() => { adding = false; newContent = '' }}>Abbrechen</button>
|
||||
<span class="card-sub" style="font-size:11.5px;margin-left:4px">Strg+Enter zum Speichern</span>
|
||||
</div>
|
||||
</div>
|
||||
{/if}
|
||||
|
||||
<!-- Category filter -->
|
||||
<div class="card" style="padding:10px 16px">
|
||||
<div class="flex gap-2" style="flex-wrap:wrap;align-items:center">
|
||||
<span style="font-size:11.5px;color:var(--mut);margin-right:4px">Filter:</span>
|
||||
{#each [
|
||||
['all', 'Alle', '', memories.length],
|
||||
['stable', 'Stabil', 'var(--accent)', countCat('stable')],
|
||||
['versioned', 'Versioniert', '#f59e0b', countCat('versioned')],
|
||||
['ephemeral', 'Temporär', 'var(--mut)', countCat('ephemeral')],
|
||||
] as [val, lbl, clr, cnt]}
|
||||
<span class="chip"
|
||||
role="button" tabindex="0"
|
||||
style="cursor:pointer;{filter === val
|
||||
? `background:rgba(45,212,191,.12);border-color:${clr || 'var(--accent)'};color:${clr || 'var(--accent)'}`
|
||||
: ''}"
|
||||
onclick={() => filter = val}
|
||||
onkeydown={e => e.key === 'Enter' && (filter = val)}>
|
||||
{lbl} <span style="color:var(--mut);margin-left:2px">{cnt}</span>
|
||||
</span>
|
||||
{/each}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Memory list -->
|
||||
{#if loading}
|
||||
<div class="card"><div class="card-sub">Lade…</div></div>
|
||||
{:else if filtered.length === 0}
|
||||
<div class="card">
|
||||
<div class="empty-c" style="padding:8px 0 4px">
|
||||
<div class="e-t">{memories.length === 0 ? 'Noch keine Einträge' : 'Keine Treffer'}</div>
|
||||
<div class="e-s">
|
||||
{memories.length === 0
|
||||
? 'Klicke „+ Eintrag" um manuell etwas zu speichern, oder verbinde ein KI-Tool via MCP.'
|
||||
: 'Andere Kategorie wählen oder Eintrag anlegen.'}
|
||||
</div>
|
||||
{#if memories.length === 0}
|
||||
<button class="primary" style="margin-top:14px" onclick={() => adding = true}>
|
||||
+ Ersten Eintrag anlegen
|
||||
</button>
|
||||
{/if}
|
||||
</div>
|
||||
</div>
|
||||
{:else}
|
||||
<div class="card" style="padding:0;overflow:hidden">
|
||||
{#each filtered as m, i (m.id)}
|
||||
<div style="padding:14px 16px;{i < filtered.length - 1 ? 'border-bottom:1px solid var(--brd, rgba(255,255,255,.07))' : ''}">
|
||||
{#if editId === m.id}
|
||||
<textarea
|
||||
rows="3"
|
||||
style="width:100%;box-sizing:border-box;resize:vertical;margin-bottom:8px"
|
||||
bind:value={editContent}
|
||||
></textarea>
|
||||
<select bind:value={editCategory} style="margin-bottom:10px">
|
||||
<option value="stable">Stabil</option>
|
||||
<option value="versioned">Versioniert</option>
|
||||
<option value="ephemeral">Temporär</option>
|
||||
</select>
|
||||
<div class="btn-row">
|
||||
<button class="primary" onclick={() => saveMem(m)}>Speichern</button>
|
||||
<button class="ghost" onclick={() => editId = null}>Abbrechen</button>
|
||||
</div>
|
||||
{:else}
|
||||
<div style="display:flex;gap:10px;align-items:flex-start">
|
||||
<div style="flex:1;min-width:0">
|
||||
<div style="font-size:13.5px;line-height:1.55;word-break:break-word">{m.content}</div>
|
||||
<div class="flex gap-2" style="margin-top:8px;flex-wrap:wrap;align-items:center">
|
||||
<span class="chip" style="border-color:{CAT[m.category]?.color || 'var(--mut)'};color:{CAT[m.category]?.color || 'var(--mut)'}">
|
||||
{CAT[m.category]?.label || m.category}
|
||||
</span>
|
||||
<span class="chip">{m.source}</span>
|
||||
<span style="font-size:11px;color:var(--mut)">{relTime(m.updated_at)}</span>
|
||||
</div>
|
||||
</div>
|
||||
<div class="flex gap-2" style="flex-shrink:0;margin-top:2px">
|
||||
<button class="ghost" style="padding:4px 10px;font-size:12px"
|
||||
onclick={() => startEdit(m)}>Bearbeiten</button>
|
||||
<button class="ghost" style="padding:4px 10px;font-size:12px;color:var(--err)"
|
||||
onclick={() => delMem(m)}>Löschen</button>
|
||||
</div>
|
||||
</div>
|
||||
{/if}
|
||||
</div>
|
||||
{/each}
|
||||
</div>
|
||||
{/if}
|
||||
|
||||
<!-- Info-Box: wie KI-Tools das Gedächtnis nutzen -->
|
||||
<div class="card" style="background:rgba(45,212,191,.04);border-color:rgba(45,212,191,.2)">
|
||||
<div class="card-h"><h3 style="font-size:13px;font-weight:500">Wie KI-Tools dieses Gedächtnis nutzen</h3></div>
|
||||
<div class="card-sub" style="line-height:1.6">
|
||||
Cline, OpenCode und Claude Code greifen über den <b>Memory MCP-Server</b> auf diese Einträge zu.
|
||||
Beim Start einer Session lädt der Agent automatisch alle Einträge — er kennt dann deinen Stack,
|
||||
deine Präferenzen und frühere Entscheidungen. Den Setup-Guide findest du unter
|
||||
<a href="#"
|
||||
onclick={e => { e.preventDefault(); (document.querySelector(".nav-item[data-view='connect']") as HTMLElement)?.click() }}
|
||||
style="color:var(--accent)">Verbinden → Gedächtnis-MCP</a>.
|
||||
</div>
|
||||
<div class="card-sub" style="margin-top:8px;font-size:11.5px">
|
||||
Kategorien: <b style="color:var(--accent)">Stabil</b> = Projektfakten die sich selten ändern ·
|
||||
<b style="color:#f59e0b">Versioniert</b> = Tech-Versionen (bei Updates überschreiben) ·
|
||||
<b style="color:var(--mut)">Temporär</b> = aktuelle Aufgaben, nach 7 Tagen automatisch gelöscht
|
||||
</div>
|
||||
</div>
|
||||
Reference in New Issue
Block a user