mirror of
https://github.com/lukaszraczylo/claude-mnemonic.git
synced 2026-10-12 04:55:18 +00:00
Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
af8e6f44cd | ||
|
|
cf4161d459 | ||
|
|
910aabda22 | ||
|
|
6115ea8a44 | ||
|
|
93446d1437 | ||
|
|
1d1807da9e | ||
|
|
7bdb523106 | ||
|
|
abd34c9629 | ||
|
|
5fe25e9328 | ||
|
|
b7b82ce22f | ||
|
|
f78370a531 | ||
|
|
5f89559842 | ||
|
|
794eacf02a | ||
|
|
6fc752abb1 | ||
|
|
9ce29ac7b1 | ||
|
|
a0d0e856f5 | ||
|
|
549fefc675 | ||
|
|
c5967c3eaa |
@@ -20,7 +20,7 @@ jobs:
|
||||
with:
|
||||
go-version: ">=1.24"
|
||||
node-enabled: true
|
||||
node-version: "20"
|
||||
node-version: "22"
|
||||
node-build-script: "cd ui && npm ci && npm run build"
|
||||
node-cache-dependency-path: "ui/package-lock.json"
|
||||
node-output-path: "ui/dist"
|
||||
|
||||
@@ -27,7 +27,7 @@ jobs:
|
||||
- name: Setup Node.js
|
||||
uses: actions/setup-node@v4
|
||||
with:
|
||||
node-version: '20'
|
||||
node-version: '22'
|
||||
cache: 'npm'
|
||||
cache-dependency-path: 'docs/package-lock.json'
|
||||
|
||||
|
||||
@@ -91,18 +91,21 @@ func handleUserPrompt(ctx *hooks.HookContext, input *Input) (string, error) {
|
||||
observationCount int
|
||||
)
|
||||
|
||||
// Start search in background
|
||||
// Start search in background. Pass the deadline context so a wedged worker
|
||||
// aborts the request at the deadline instead of blocking for the full
|
||||
// hookClient timeout (10s). Errors are ignored -- fail open with no memory.
|
||||
wg.Add(1)
|
||||
go func() {
|
||||
defer wg.Done()
|
||||
searchResult, _ = hooks.GET(ctx.Port, searchURL)
|
||||
searchResult, _ = hooks.GETWithContext(deadline, ctx.Port, searchURL)
|
||||
}()
|
||||
|
||||
// Start session init in parallel (with observationCount=0; approximate is fine)
|
||||
// Start session init in parallel (with observationCount=0; approximate is fine).
|
||||
// Deadline context guards this call too.
|
||||
wg.Add(1)
|
||||
go func() {
|
||||
defer wg.Done()
|
||||
initResult, initErr = hooks.POST(ctx.Port, "/api/sessions/init", map[string]interface{}{
|
||||
initResult, initErr = hooks.POSTWithContextResult(deadline, ctx.Port, "/api/sessions/init", map[string]interface{}{
|
||||
"claudeSessionId": ctx.SessionID,
|
||||
"project": ctx.Project,
|
||||
"prompt": input.Prompt,
|
||||
@@ -113,7 +116,8 @@ func handleUserPrompt(ctx *hooks.HookContext, input *Input) (string, error) {
|
||||
// Wait for both to complete
|
||||
wg.Wait()
|
||||
|
||||
// Check deadline after network calls
|
||||
// Check deadline after network calls -- if exceeded, fail open (proceed with
|
||||
// no injected memory) rather than blocking or erroring the user's prompt.
|
||||
if deadline.Err() != nil {
|
||||
return "", nil
|
||||
}
|
||||
@@ -173,9 +177,11 @@ func handleUserPrompt(ctx *hooks.HookContext, input *Input) (string, error) {
|
||||
contextToInject = contextBuilder
|
||||
}
|
||||
|
||||
// Check session init result
|
||||
// Check session init result. A session-init failure must never block the
|
||||
// prompt: degrade gracefully and still inject any memory we found.
|
||||
if initErr != nil {
|
||||
return "", initErr
|
||||
fmt.Fprintf(os.Stderr, "[user-prompt] Session init failed: %v\n", initErr)
|
||||
return contextToInject, nil
|
||||
}
|
||||
if initResult == nil {
|
||||
return contextToInject, nil // Non-JSON response from worker, skip session init
|
||||
@@ -201,13 +207,15 @@ func handleUserPrompt(ctx *hooks.HookContext, input *Input) (string, error) {
|
||||
|
||||
fmt.Fprintf(os.Stderr, "[user-prompt] Session %d, prompt #%d\n", sessionID, promptNumber)
|
||||
|
||||
// Start SDK agent (depends on session init result, so kept sequential)
|
||||
_, err := hooks.POST(ctx.Port, fmt.Sprintf("/sessions/%d/init", sessionID), map[string]interface{}{
|
||||
// Start SDK agent (depends on session init result, so kept sequential).
|
||||
// Deadline-guarded so a wedged worker cannot stall past the hook budget.
|
||||
// Failure here must not block the prompt: degrade gracefully, still inject memory.
|
||||
if _, err := hooks.POSTWithContextResult(deadline, ctx.Port, fmt.Sprintf("/sessions/%d/init", sessionID), map[string]interface{}{
|
||||
"userPrompt": input.Prompt,
|
||||
"promptNumber": promptNumber,
|
||||
})
|
||||
if err != nil {
|
||||
return "", err
|
||||
}); err != nil {
|
||||
fmt.Fprintf(os.Stderr, "[user-prompt] SDK agent init failed: %v\n", err)
|
||||
return contextToInject, nil
|
||||
}
|
||||
|
||||
// Return context if we found relevant observations
|
||||
|
||||
Generated
+732
-1397
File diff suppressed because it is too large
Load Diff
+6
-6
@@ -8,13 +8,13 @@
|
||||
"preview": "vite preview"
|
||||
},
|
||||
"dependencies": {
|
||||
"vue": "^3.5.25"
|
||||
"vue": "^3.5.34"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@vitejs/plugin-vue": "^6.0.3",
|
||||
"autoprefixer": "^10.4.23",
|
||||
"postcss": "^8.5.6",
|
||||
"tailwindcss": "^3.4.19",
|
||||
"vite": "^7.3.0"
|
||||
"@tailwindcss/postcss": "^4.3.0",
|
||||
"@vitejs/plugin-vue": "^6.0.7",
|
||||
"postcss": "^8.5.15",
|
||||
"tailwindcss": "^4.3.0",
|
||||
"vite": "^8.0.14"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,6 +1,5 @@
|
||||
export default {
|
||||
plugins: {
|
||||
tailwindcss: {},
|
||||
autoprefixer: {},
|
||||
'@tailwindcss/postcss': {},
|
||||
},
|
||||
}
|
||||
|
||||
+165
-405
@@ -1,398 +1,153 @@
|
||||
<template>
|
||||
<div class="bg-slate-950 text-white min-h-screen relative overflow-hidden">
|
||||
<!-- Animated Background -->
|
||||
<div class="fixed inset-0 pointer-events-none">
|
||||
<!-- Gradient orbs -->
|
||||
<div class="absolute top-0 left-1/4 w-96 h-96 bg-amber-500/10 rounded-full blur-3xl animate-float"></div>
|
||||
<div class="absolute bottom-1/4 right-1/4 w-80 h-80 bg-amber-400/5 rounded-full blur-3xl animate-float-delayed"></div>
|
||||
<div class="absolute top-1/2 right-0 w-64 h-64 bg-slate-700/20 rounded-full blur-3xl animate-glow"></div>
|
||||
|
||||
<!-- Grid pattern -->
|
||||
<div class="absolute inset-0 bg-[linear-gradient(rgba(148,163,184,0.03)_1px,transparent_1px),linear-gradient(90deg,rgba(148,163,184,0.03)_1px,transparent_1px)] bg-[size:50px_50px]"></div>
|
||||
|
||||
<!-- Floating particles -->
|
||||
<div v-for="i in 12" :key="i"
|
||||
class="particle animate-particle"
|
||||
:style="{
|
||||
left: `${(i * 8.3) % 100}%`,
|
||||
animationDelay: `${i * 1.5}s`,
|
||||
animationDuration: `${15 + (i % 5) * 3}s`
|
||||
}"></div>
|
||||
</div>
|
||||
|
||||
<div class="min-h-screen flex flex-col">
|
||||
<NavBar :mobile-menu-open="mobileMenuOpen" @toggle-menu="mobileMenuOpen = !mobileMenuOpen" />
|
||||
|
||||
<HeroSection
|
||||
badge="The missing piece for Claude Code"
|
||||
title-before="Yesterday's context."
|
||||
title-highlight="Today's session."
|
||||
subtitle="Stop re-explaining your codebase. Claude Mnemonic captures bug fixes, architecture decisions, and coding patterns - then brings them back exactly when you need them."
|
||||
/>
|
||||
<main class="flex-1">
|
||||
<HeroSection />
|
||||
|
||||
<!-- Dashboard Preview -->
|
||||
<section class="py-12 lg:py-16 px-4 sm:px-6 relative">
|
||||
<div class="max-w-6xl mx-auto">
|
||||
<div class="relative rounded-xl overflow-hidden border border-slate-700/50 shadow-2xl shadow-amber-500/5">
|
||||
<div class="absolute inset-0 bg-gradient-to-t from-slate-950 via-transparent to-transparent pointer-events-none z-10"></div>
|
||||
<img
|
||||
src="/claude-mnemonic.jpg"
|
||||
alt="Claude Mnemonic Dashboard"
|
||||
class="w-full h-auto"
|
||||
/>
|
||||
<section class="py-10 px-4 sm:px-6">
|
||||
<div class="max-w-5xl mx-auto">
|
||||
<div class="rounded-xl overflow-hidden border border-neutral-800">
|
||||
<img src="/claude-mnemonic.jpg" alt="Claude Mnemonic Dashboard" class="w-full h-auto block" loading="lazy" />
|
||||
</div>
|
||||
<p class="text-center text-neutral-600 text-xs mt-3 tracking-wide uppercase">Dashboard — localhost:37777</p>
|
||||
</div>
|
||||
<p class="text-center text-slate-500 text-sm mt-4">The dashboard at localhost:37777 - browse, search, and manage your memories. View graph stats, vector metrics, storage savings, and performance analytics.</p>
|
||||
</div>
|
||||
</section>
|
||||
</section>
|
||||
|
||||
<!-- Problem Section -->
|
||||
<section class="py-20 lg:py-28 px-4 sm:px-6 relative">
|
||||
<div class="max-w-6xl mx-auto grid lg:grid-cols-2 gap-8 lg:gap-16 items-center">
|
||||
<div>
|
||||
<h2 class="text-2xl sm:text-3xl md:text-4xl lg:text-5xl font-bold text-white mb-6">Sound familiar?</h2>
|
||||
<p class="text-slate-400 text-base sm:text-lg mb-8">Every Claude Code session starts fresh. That bug you fixed last Tuesday? Gone. The auth flow you explained in detail? Forgotten. Your team's naming conventions? You'll explain them again.</p>
|
||||
<ul class="space-y-4 sm:space-y-5">
|
||||
<li class="flex items-start gap-3 sm:gap-4 text-slate-400">
|
||||
<i class="fas fa-times-circle text-red-400 mt-1 flex-shrink-0"></i>
|
||||
<span class="text-sm sm:text-base">"We fixed this exact race condition last week..."</span>
|
||||
</li>
|
||||
<li class="flex items-start gap-3 sm:gap-4 text-slate-400">
|
||||
<i class="fas fa-times-circle text-red-400 mt-1 flex-shrink-0"></i>
|
||||
<span class="text-sm sm:text-base">"No, we use kebab-case for API routes, not camelCase..."</span>
|
||||
</li>
|
||||
<li class="flex items-start gap-3 sm:gap-4 text-slate-400">
|
||||
<i class="fas fa-times-circle text-red-400 mt-1 flex-shrink-0"></i>
|
||||
<span class="text-sm sm:text-base">"The database is Postgres, not MySQL. I told you yesterday..."</span>
|
||||
</li>
|
||||
<li class="flex items-start gap-3 sm:gap-4 text-white">
|
||||
<i class="fas fa-check-circle text-amber-500 mt-1 flex-shrink-0"></i>
|
||||
<span class="text-sm sm:text-base"><strong>Mnemonic remembers so you don't have to repeat yourself.</strong></span>
|
||||
</li>
|
||||
</ul>
|
||||
</div>
|
||||
|
||||
<div class="glass rounded-2xl p-6 sm:p-8 relative overflow-hidden glow-amber">
|
||||
<div class="absolute top-0 left-0 right-0 h-px bg-gradient-to-r from-transparent via-amber-500 to-transparent"></div>
|
||||
<p class="text-slate-500 text-xs sm:text-sm mb-4 font-mono">// What Mnemonic captures automatically:</p>
|
||||
<div class="space-y-3">
|
||||
<FlowItem icon="fas fa-bug" title="Bug fixes & solutions" description='"Fixed N+1 query in user loader by adding .includes(:posts)"' />
|
||||
<FlowItem icon="fas fa-sitemap" title="Architecture decisions" description='"Using event sourcing for audit trail - all mutations go through EventStore"' />
|
||||
<FlowItem icon="fas fa-code-branch" title="Project conventions" description='"API routes use /api/v1/ prefix, kebab-case for endpoints"' />
|
||||
<FlowItem icon="fas fa-shield-alt" title="Security patterns" description='"Always validate JWT on server side, never trust client claims"' />
|
||||
<section id="features" class="py-20 px-4 sm:px-6">
|
||||
<div class="max-w-5xl mx-auto">
|
||||
<h2 class="text-3xl sm:text-4xl font-bold text-white mb-4">What it does</h2>
|
||||
<p class="text-neutral-500 mb-12 max-w-xl">Captures context from your Claude Code sessions and brings it back when you need it. No manual effort.</p>
|
||||
<div class="grid sm:grid-cols-2 lg:grid-cols-3 gap-6">
|
||||
<FeatureItem
|
||||
v-for="feature in features"
|
||||
:key="feature.title"
|
||||
:icon="feature.icon"
|
||||
:title="feature.title"
|
||||
:description="feature.description"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
</section>
|
||||
|
||||
<!-- Features Section -->
|
||||
<section id="features" class="py-20 lg:py-28 bg-slate-900/30 relative">
|
||||
<div class="absolute top-0 left-0 right-0 h-px bg-gradient-to-r from-transparent via-slate-700 to-transparent"></div>
|
||||
<div class="max-w-6xl mx-auto px-4 sm:px-6">
|
||||
<SectionHeader title="Built for real workflows" subtitle="Not just storage - intelligent context that makes Claude more useful over time" />
|
||||
<div class="grid sm:grid-cols-2 lg:grid-cols-3 gap-4 sm:gap-6">
|
||||
<FeatureCard
|
||||
v-for="feature in features"
|
||||
:key="feature.title"
|
||||
:icon="feature.icon"
|
||||
:title="feature.title"
|
||||
:description="feature.description"
|
||||
/>
|
||||
<section class="py-10 px-4 sm:px-6">
|
||||
<div class="max-w-5xl mx-auto">
|
||||
<div class="rounded-xl overflow-hidden border border-neutral-800">
|
||||
<img src="/observation-relation-graph.jpg" alt="Knowledge Graph" class="w-full h-auto block" loading="lazy" />
|
||||
</div>
|
||||
<p class="text-center text-neutral-600 text-xs mt-3 tracking-wide uppercase">Observation relationship graph</p>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
</section>
|
||||
|
||||
<!-- Knowledge Graph Preview -->
|
||||
<section class="py-16 lg:py-20 px-4 sm:px-6 relative">
|
||||
<div class="max-w-5xl mx-auto">
|
||||
<SectionHeader title="See how knowledge connects" subtitle="The knowledge graph reveals relationships between your memories automatically" />
|
||||
<div class="relative rounded-xl overflow-hidden border border-slate-700/50 shadow-2xl shadow-purple-500/5">
|
||||
<img
|
||||
src="/observation-relation-graph.jpg"
|
||||
alt="Knowledge Graph Visualization"
|
||||
class="w-full h-auto"
|
||||
/>
|
||||
</div>
|
||||
<p class="text-center text-slate-500 text-sm mt-4">Click any observation to explore its relationships - see what causes what, what fixes what, and how concepts evolve</p>
|
||||
</div>
|
||||
</section>
|
||||
<section id="install" class="py-20 px-4 sm:px-6">
|
||||
<div class="max-w-5xl mx-auto">
|
||||
<h2 class="text-3xl sm:text-4xl font-bold text-white mb-4">Install</h2>
|
||||
<p class="text-neutral-500 mb-8">One command. No configuration. No account.</p>
|
||||
|
||||
<!-- Before/After Section -->
|
||||
<section class="py-20 lg:py-28 px-4 sm:px-6">
|
||||
<div class="max-w-6xl mx-auto">
|
||||
<SectionHeader title="The difference" subtitle="Same question. Different experience." />
|
||||
<div class="grid md:grid-cols-2 gap-6 sm:gap-8">
|
||||
<!-- Before -->
|
||||
<div class="glass rounded-2xl p-5 sm:p-6 relative border-red-500/20">
|
||||
<div class="absolute top-3 sm:top-4 right-3 sm:right-4 bg-red-500/20 text-red-400 px-2 sm:px-3 py-1 rounded-full text-xs font-medium">Without Mnemonic</div>
|
||||
<div class="space-y-3 sm:space-y-4 mt-8">
|
||||
<div class="bg-slate-800/50 rounded-lg p-3 sm:p-4">
|
||||
<p class="text-slate-400 text-xs sm:text-sm"><span class="text-blue-400">You:</span> Fix the authentication bug in the login flow</p>
|
||||
</div>
|
||||
<div class="bg-slate-800/50 rounded-lg p-3 sm:p-4">
|
||||
<p class="text-slate-400 text-xs sm:text-sm"><span class="text-amber-400">Claude:</span> I'd be happy to help! Can you tell me about your authentication setup? What framework are you using? How is the login flow structured?</p>
|
||||
</div>
|
||||
<div class="bg-slate-800/50 rounded-lg p-3 sm:p-4">
|
||||
<p class="text-slate-400 text-xs sm:text-sm"><span class="text-blue-400">You:</span> We use NextAuth with Postgres. The flow goes through /api/auth/... We talked about this last week...</p>
|
||||
</div>
|
||||
<p class="text-red-400 text-xs sm:text-sm text-center pt-2"><i class="fas fa-clock mr-2"></i>5+ minutes of re-explaining context</p>
|
||||
</div>
|
||||
<div class="flex gap-1 mb-6">
|
||||
<button
|
||||
v-for="tab in installTabs"
|
||||
:key="tab.id"
|
||||
@click="activeTab = tab.id"
|
||||
:class="[
|
||||
'px-4 py-2 text-sm rounded-lg transition-colors',
|
||||
activeTab === tab.id
|
||||
? 'bg-neutral-800 text-white'
|
||||
: 'text-neutral-500 hover:text-neutral-300'
|
||||
]"
|
||||
>
|
||||
{{ tab.label }}
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<!-- After -->
|
||||
<div class="glass rounded-2xl p-5 sm:p-6 relative border-green-500/20">
|
||||
<div class="absolute top-3 sm:top-4 right-3 sm:right-4 bg-green-500/20 text-green-400 px-2 sm:px-3 py-1 rounded-full text-xs font-medium">With Mnemonic</div>
|
||||
<div class="space-y-3 sm:space-y-4 mt-8">
|
||||
<div class="bg-slate-800/50 rounded-lg p-3 sm:p-4">
|
||||
<p class="text-slate-400 text-xs sm:text-sm"><span class="text-blue-400">You:</span> Fix the authentication bug in the login flow</p>
|
||||
</div>
|
||||
<div class="bg-slate-800/50 rounded-lg p-3 sm:p-4">
|
||||
<p class="text-slate-400 text-xs sm:text-sm"><span class="text-amber-400">Claude:</span> Looking at your NextAuth setup with Postgres, I see the issue is likely in the callback at <code class="text-amber-300 text-xs">/api/auth/[...nextauth].ts</code>. Based on the session validation pattern we established, the fix is...</p>
|
||||
</div>
|
||||
<p class="text-green-400 text-xs sm:text-sm text-center pt-2"><i class="fas fa-bolt mr-2"></i>Instant context. Immediate value.</p>
|
||||
</div>
|
||||
<div v-show="activeTab === 'macos'">
|
||||
<CodeBlock code="curl -sSL https://raw.githubusercontent.com/lukaszraczylo/claude-mnemonic/main/scripts/install.sh | bash" />
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<!-- How It Works Section -->
|
||||
<section id="how-it-works" class="py-20 lg:py-28 px-4 sm:px-6 bg-slate-900/30">
|
||||
<div class="max-w-6xl mx-auto">
|
||||
<SectionHeader title="Zero setup. Zero maintenance." subtitle="Install once, benefit forever. Mnemonic works silently in the background." />
|
||||
<div class="grid sm:grid-cols-2 lg:grid-cols-4 gap-6 sm:gap-8 relative">
|
||||
<div class="hidden lg:block absolute top-10 left-[60px] right-[60px] h-0.5 bg-gradient-to-r from-amber-500 via-amber-400 to-amber-500 opacity-30"></div>
|
||||
<StepCard
|
||||
v-for="step in steps"
|
||||
:key="step.number"
|
||||
:number="step.number"
|
||||
:title="step.title"
|
||||
:description="step.description"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
<div v-show="activeTab === 'windows'">
|
||||
<CodeBlock code="irm https://raw.githubusercontent.com/lukaszraczylo/claude-mnemonic/main/scripts/install.ps1 | iex" />
|
||||
</div>
|
||||
|
||||
<!-- Installation Section -->
|
||||
<section id="installation" class="py-20 lg:py-28 px-4 sm:px-6">
|
||||
<div class="max-w-6xl mx-auto">
|
||||
<SectionHeader title="Quick install" subtitle="One command. No configuration. No account required." />
|
||||
<div v-show="activeTab === 'source'">
|
||||
<CodeBlock code="git clone https://github.com/lukaszraczylo/claude-mnemonic.git cd claude-mnemonic make build && make install" />
|
||||
<p class="text-neutral-600 text-xs mt-2">Requires: Go 1.24+, Node.js 18+, CGO compiler</p>
|
||||
</div>
|
||||
|
||||
<div class="flex flex-wrap justify-center gap-2 mb-6 sm:mb-8">
|
||||
<button
|
||||
v-for="tab in installTabs"
|
||||
:key="tab.id"
|
||||
@click="activeTab = tab.id"
|
||||
:class="[
|
||||
'px-4 sm:px-5 py-2 sm:py-2.5 rounded-lg text-xs sm:text-sm font-medium transition-all',
|
||||
activeTab === tab.id
|
||||
? 'bg-amber-500/15 border border-amber-500 text-amber-500'
|
||||
: 'glass text-slate-400 hover:border-slate-600'
|
||||
]"
|
||||
>
|
||||
{{ tab.label }}
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<div v-show="activeTab === 'macos'">
|
||||
<CodeBlock :code="installCommands.macos">
|
||||
<span class="text-slate-500"># That's it. Seriously.</span>
|
||||
<br>
|
||||
<span class="text-amber-400">curl -sSL https://raw.githubusercontent.com/lukaszraczylo/claude-mnemonic/main/scripts/install.sh | bash</span>
|
||||
</CodeBlock>
|
||||
</div>
|
||||
|
||||
<div v-show="activeTab === 'windows'">
|
||||
<CodeBlock :code="installCommands.windows">
|
||||
<span class="text-slate-500"># PowerShell (as Administrator)</span>
|
||||
<br>
|
||||
<span class="text-amber-400">irm https://raw.githubusercontent.com/lukaszraczylo/claude-mnemonic/main/scripts/install.ps1 | iex</span>
|
||||
</CodeBlock>
|
||||
</div>
|
||||
|
||||
<div v-show="activeTab === 'source'">
|
||||
<CodeBlock :code="installCommands.source">
|
||||
<span class="text-slate-500"># For contributors and tinkerers</span>
|
||||
<br>
|
||||
<span class="text-amber-400">git clone https://github.com/lukaszraczylo/claude-mnemonic.git</span>
|
||||
<br>
|
||||
<span class="text-amber-400">cd claude-mnemonic</span>
|
||||
<br>
|
||||
<span class="text-amber-400">make build && make install</span>
|
||||
<br><br>
|
||||
<span class="text-slate-500"># Requires: Go 1.24+, Node.js 18+, CGO compiler</span>
|
||||
</CodeBlock>
|
||||
</div>
|
||||
|
||||
<p class="text-center text-slate-500 mt-6 sm:mt-8 text-xs sm:text-sm">
|
||||
After install, open <a href="http://localhost:37777" class="text-amber-400 hover:underline animated-underline">localhost:37777</a> to see the dashboard.
|
||||
Start a new Claude Code CLI session - memory is now active.
|
||||
</p>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<!-- Requirements Section -->
|
||||
<section id="requirements" class="py-20 lg:py-28 px-4 sm:px-6 bg-slate-900/30">
|
||||
<div class="max-w-6xl mx-auto">
|
||||
<SectionHeader title="Requirements" subtitle="Minimal dependencies. Everything else is built-in." />
|
||||
|
||||
<div class="max-w-xl mx-auto">
|
||||
<div class="glass rounded-2xl p-5 sm:p-6">
|
||||
<div class="flex items-center gap-2 mb-4">
|
||||
<i class="fas fa-check-circle text-green-500"></i>
|
||||
<span class="text-white font-semibold text-sm sm:text-base">That's all you need</span>
|
||||
</div>
|
||||
<div class="space-y-3">
|
||||
<div v-for="req in requiredDeps" :key="req.name" class="flex items-start gap-3 p-3 bg-slate-800/50 rounded-lg">
|
||||
<i :class="[req.icon, 'text-amber-500 mt-0.5']"></i>
|
||||
<div>
|
||||
<code class="text-white text-xs sm:text-sm font-semibold">{{ req.name }}</code>
|
||||
<p class="text-slate-400 text-xs mt-1">{{ req.description }}</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<p class="text-slate-500 text-xs mt-4">
|
||||
<i class="fas fa-info-circle mr-1"></i>
|
||||
No Python. No external services. Semantic search with local embeddings is built-in.
|
||||
<div class="mt-8 p-4 rounded-lg border border-neutral-800 bg-neutral-900/50 max-w-2xl">
|
||||
<p class="text-neutral-500 text-sm">
|
||||
<i class="fas fa-info-circle text-neutral-600 mr-1"></i>
|
||||
After install, open <a href="http://localhost:37777" class="text-amber-500 hover:text-amber-400">localhost:37777</a> for the dashboard. Start a new Claude Code session — memory is active.
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
</section>
|
||||
|
||||
<!-- Configuration Section -->
|
||||
<section id="configuration" class="py-20 lg:py-28 px-4 sm:px-6 bg-slate-900/30">
|
||||
<div class="max-w-6xl mx-auto">
|
||||
<SectionHeader title="Fully configurable" subtitle="Works out of the box, but adapts to your preferences" />
|
||||
<section id="requirements" class="py-20 px-4 sm:px-6">
|
||||
<div class="max-w-5xl mx-auto">
|
||||
<h2 class="text-3xl sm:text-4xl font-bold text-white mb-4">Requirements</h2>
|
||||
<p class="text-neutral-500 mb-8">Minimal dependencies. Everything else is built in.</p>
|
||||
|
||||
<div class="grid lg:grid-cols-2 gap-6 sm:gap-8">
|
||||
<!-- Config file example -->
|
||||
<div class="glass rounded-2xl p-5 sm:p-6">
|
||||
<div class="flex items-center gap-2 mb-4">
|
||||
<i class="fas fa-cog text-amber-500"></i>
|
||||
<span class="text-white font-semibold text-sm sm:text-base">~/.claude-mnemonic/settings.json</span>
|
||||
<div class="grid sm:grid-cols-2 gap-4 max-w-xl">
|
||||
<div v-for="req in requiredDeps" :key="req.name" class="p-4 rounded-lg border border-neutral-800 bg-neutral-900/50">
|
||||
<div class="flex items-center gap-2 mb-1">
|
||||
<i :class="[req.icon, 'text-amber-500 text-sm']"></i>
|
||||
<code class="text-white text-sm font-semibold">{{ req.name }}</code>
|
||||
</div>
|
||||
<p class="text-neutral-500 text-xs pl-6">{{ req.description }}</p>
|
||||
</div>
|
||||
<pre class="text-xs sm:text-sm font-mono overflow-x-auto"><code><span class="text-slate-500">{</span>
|
||||
<span class="text-emerald-400">"CLAUDE_MNEMONIC_WORKER_PORT"</span><span class="text-slate-500">:</span> <span class="text-amber-400">37777</span><span class="text-slate-500">,</span>
|
||||
<span class="text-emerald-400">"CLAUDE_MNEMONIC_CONTEXT_OBSERVATIONS"</span><span class="text-slate-500">:</span> <span class="text-amber-400">100</span><span class="text-slate-500">,</span>
|
||||
<span class="text-emerald-400">"CLAUDE_MNEMONIC_CONTEXT_FULL_COUNT"</span><span class="text-slate-500">:</span> <span class="text-amber-400">25</span><span class="text-slate-500">,</span>
|
||||
<span class="text-emerald-400">"CLAUDE_MNEMONIC_MODEL"</span><span class="text-slate-500">:</span> <span class="text-sky-400">"haiku"</span>
|
||||
<span class="text-slate-500">}</span></code></pre>
|
||||
</div>
|
||||
<p class="text-neutral-600 text-sm mt-4">No Python. No external services. No API keys — uses your existing Claude Pro/Max subscription.</p>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<!-- Config options list -->
|
||||
<div class="space-y-3 sm:space-y-4">
|
||||
<div v-for="config in configOptions" :key="config.name" class="glass rounded-xl p-4 hover:border-amber-500/20 transition-colors">
|
||||
<div class="flex items-start gap-3">
|
||||
<div class="w-8 h-8 bg-amber-500/15 rounded-lg flex items-center justify-center text-amber-500 flex-shrink-0 text-sm">
|
||||
<i :class="config.icon"></i>
|
||||
<section id="config" class="py-20 px-4 sm:px-6">
|
||||
<div class="max-w-5xl mx-auto">
|
||||
<h2 class="text-3xl sm:text-4xl font-bold text-white mb-4">Configuration</h2>
|
||||
<p class="text-neutral-500 mb-8">Works out of the box. Adjust if you want to.</p>
|
||||
|
||||
<div class="grid lg:grid-cols-2 gap-8">
|
||||
<div class="rounded-lg border border-neutral-800 bg-neutral-900/50 p-5">
|
||||
<p class="text-neutral-600 text-xs mb-3 font-mono">~/.claude-mnemonic/settings.json</p>
|
||||
<pre class="text-sm overflow-x-auto leading-relaxed"><code>{
|
||||
<span class="text-emerald-400">"CLAUDE_MNEMONIC_WORKER_PORT"</span>: <span class="text-amber-400">37777</span>,
|
||||
<span class="text-emerald-400">"CLAUDE_MNEMONIC_CONTEXT_OBSERVATIONS"</span>: <span class="text-amber-400">100</span>,
|
||||
<span class="text-emerald-400">"CLAUDE_MNEMONIC_CONTEXT_FULL_COUNT"</span>: <span class="text-amber-400">25</span>,
|
||||
<span class="text-emerald-400">"CLAUDE_MNEMONIC_RERANKING_ENABLED"</span>: <span class="text-amber-400">true</span>
|
||||
}</code></pre>
|
||||
</div>
|
||||
|
||||
<div class="space-y-2">
|
||||
<div v-for="config in configOptions" :key="config.name" class="flex items-start gap-3 p-3 rounded-lg border border-neutral-800/50">
|
||||
<div class="w-6 h-6 rounded bg-neutral-800 flex items-center justify-center flex-shrink-0 mt-0.5">
|
||||
<i class="fas fa-sliders text-neutral-500 text-xs"></i>
|
||||
</div>
|
||||
<div class="min-w-0">
|
||||
<code class="text-amber-400 text-xs sm:text-sm">{{ config.name }}</code>
|
||||
<p class="text-slate-400 text-xs sm:text-sm mt-1">{{ config.description }}</p>
|
||||
<div>
|
||||
<code class="text-amber-500 text-xs">{{ config.name }}</code>
|
||||
<p class="text-neutral-500 text-xs mt-0.5">{{ config.description }}</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<p class="text-neutral-600 text-xs mt-6">
|
||||
All settings also work as environment variables. <a href="https://github.com/lukaszraczylo/claude-mnemonic#configuration" target="_blank" class="text-neutral-500 hover:text-neutral-300">Full docs</a>.
|
||||
</p>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<p class="text-center text-slate-500 mt-6 sm:mt-8 text-xs sm:text-sm">
|
||||
All settings can also be set via environment variables. See <a href="https://github.com/lukaszraczylo/claude-mnemonic#configuration" target="_blank" class="text-amber-400 hover:underline">full documentation</a> for all options.
|
||||
</p>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<!-- Technical Details -->
|
||||
<section class="py-20 lg:py-28 px-4 sm:px-6">
|
||||
<div class="max-w-6xl mx-auto">
|
||||
<SectionHeader title="Under the hood" subtitle="Built with simplicity and performance in mind" />
|
||||
<div class="grid sm:grid-cols-2 lg:grid-cols-3 gap-4 sm:gap-6 text-center">
|
||||
<div class="glass rounded-2xl p-6 sm:p-8 hover:border-amber-500/30 transition-colors">
|
||||
<div class="text-3xl sm:text-4xl font-bold text-amber-500 mb-2">Go</div>
|
||||
<p class="text-slate-400 text-xs sm:text-sm">Single binary. Fast startup, low memory. Zero runtime dependencies.</p>
|
||||
</div>
|
||||
<div class="glass rounded-2xl p-6 sm:p-8 hover:border-amber-500/30 transition-colors">
|
||||
<div class="text-3xl sm:text-4xl font-bold text-amber-500 mb-2">SQLite</div>
|
||||
<p class="text-slate-400 text-xs sm:text-sm">FTS5 full-text search. Single file database. Survives restarts.</p>
|
||||
</div>
|
||||
<div class="glass rounded-2xl p-6 sm:p-8 hover:border-amber-500/30 transition-colors">
|
||||
<div class="text-3xl sm:text-4xl font-bold text-amber-500 mb-2">sqlite-vec</div>
|
||||
<p class="text-slate-400 text-xs sm:text-sm">Hybrid vector storage with LEANN-inspired selective embeddings. 60-80% storage reduction.</p>
|
||||
</div>
|
||||
<div class="glass rounded-2xl p-6 sm:p-8 hover:border-amber-500/30 transition-colors">
|
||||
<div class="text-3xl sm:text-4xl font-bold text-amber-500 mb-2">BGE</div>
|
||||
<p class="text-slate-400 text-xs sm:text-sm">Two-stage retrieval: bi-encoder embeddings + cross-encoder reranking for high accuracy.</p>
|
||||
</div>
|
||||
<div class="glass rounded-2xl p-6 sm:p-8 hover:border-amber-500/30 transition-colors">
|
||||
<div class="text-3xl sm:text-4xl font-bold text-amber-500 mb-2">Tree-sitter</div>
|
||||
<p class="text-slate-400 text-xs sm:text-sm">AST-aware code chunking respects function boundaries for Go, Python, and TypeScript.</p>
|
||||
</div>
|
||||
<div class="glass rounded-2xl p-6 sm:p-8 hover:border-amber-500/30 transition-colors">
|
||||
<div class="text-3xl sm:text-4xl font-bold text-amber-500 mb-2">CSR Graph</div>
|
||||
<p class="text-slate-400 text-xs sm:text-sm">Memory-efficient observation relationship graph with edge detection and hub identification.</p>
|
||||
<section id="architecture" class="py-20 px-4 sm:px-6">
|
||||
<div class="max-w-5xl mx-auto">
|
||||
<h2 class="text-3xl sm:text-4xl font-bold text-white mb-4">Under the hood</h2>
|
||||
<p class="text-neutral-500 mb-10">No magic. Here's what's running locally on your machine.</p>
|
||||
<div class="grid sm:grid-cols-2 lg:grid-cols-3 gap-4">
|
||||
<TechCard v-for="tech in techStack" :key="tech.name" :name="tech.name" :description="tech.description" />
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
</section>
|
||||
|
||||
<!-- Trust Section -->
|
||||
<section class="py-20 lg:py-28 px-4 sm:px-6 bg-slate-900/30">
|
||||
<div class="max-w-4xl mx-auto">
|
||||
<SectionHeader title="Open source. Real person." subtitle="Not another anonymous package - built by a developer you can verify" />
|
||||
|
||||
<div class="glass rounded-2xl p-6 sm:p-8 text-center">
|
||||
<div class="flex flex-col sm:flex-row items-center justify-center gap-6 sm:gap-8 mb-6">
|
||||
<a href="https://github.com/lukaszraczylo" target="_blank" class="group">
|
||||
<img
|
||||
src="https://github.com/lukaszraczylo.png"
|
||||
alt="Lukasz Raczylo"
|
||||
class="w-20 h-20 sm:w-24 sm:h-24 rounded-full border-2 border-slate-700 group-hover:border-amber-500 transition-colors"
|
||||
/>
|
||||
</a>
|
||||
<div class="text-center sm:text-left">
|
||||
<h3 class="text-white font-semibold text-lg sm:text-xl mb-1">Lukasz Raczylo</h3>
|
||||
<p class="text-slate-400 text-sm mb-3">Principal Engineer & Open Source Maintainer</p>
|
||||
<div class="flex flex-wrap justify-center sm:justify-start gap-3">
|
||||
<a href="https://github.com/lukaszraczylo" target="_blank" class="text-slate-500 hover:text-white transition-colors text-sm">
|
||||
<i class="fab fa-github mr-1"></i>GitHub
|
||||
</a>
|
||||
<a href="https://linkedin.com/in/lukaszraczylo" target="_blank" class="text-slate-500 hover:text-white transition-colors text-sm">
|
||||
<i class="fab fa-linkedin mr-1"></i>LinkedIn
|
||||
</a>
|
||||
<a href="https://raczylo.com" target="_blank" class="text-slate-500 hover:text-white transition-colors text-sm">
|
||||
<i class="fas fa-globe mr-1"></i>Website
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="grid sm:grid-cols-3 gap-4 pt-6 border-t border-slate-700/50">
|
||||
<div class="text-center">
|
||||
<div class="text-amber-500 font-bold text-xl sm:text-2xl mb-1">100%</div>
|
||||
<p class="text-slate-400 text-xs sm:text-sm">Open Source</p>
|
||||
</div>
|
||||
<div class="text-center">
|
||||
<div class="text-amber-500 font-bold text-xl sm:text-2xl mb-1">MIT</div>
|
||||
<p class="text-slate-400 text-xs sm:text-sm">License</p>
|
||||
</div>
|
||||
<div class="text-center">
|
||||
<div class="text-amber-500 font-bold text-xl sm:text-2xl mb-1">0</div>
|
||||
<p class="text-slate-400 text-xs sm:text-sm">Data collection</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<p class="text-center text-slate-500 mt-6 sm:mt-8 text-xs sm:text-sm">
|
||||
Unlike anonymous packages, you can verify who built this, read every line of code, and reach out directly with questions.
|
||||
<br class="hidden sm:block">
|
||||
Your security matters - that's why transparency is non-negotiable.
|
||||
</p>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<!-- FAQ Section -->
|
||||
<section id="faq" class="py-20 lg:py-28 px-4 sm:px-6">
|
||||
<div class="max-w-4xl mx-auto">
|
||||
<SectionHeader title="Questions?" />
|
||||
<div class="grid sm:grid-cols-2 gap-4 sm:gap-6">
|
||||
<section id="faq" class="py-20 px-4 sm:px-6">
|
||||
<div class="max-w-3xl mx-auto">
|
||||
<h2 class="text-3xl sm:text-4xl font-bold text-white mb-10">FAQ</h2>
|
||||
<FaqItem
|
||||
v-for="faq in faqs"
|
||||
:key="faq.question"
|
||||
@@ -400,8 +155,21 @@
|
||||
:answer="faq.answer"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
</section>
|
||||
|
||||
<section class="py-20 px-4 sm:px-6">
|
||||
<div class="max-w-3xl mx-auto text-center">
|
||||
<p class="text-neutral-600 text-sm mb-2">Open source, built by</p>
|
||||
<a href="https://github.com/lukaszraczylo" target="_blank" class="inline-flex items-center gap-3 text-neutral-400 hover:text-white transition-colors group">
|
||||
<img src="https://github.com/lukaszraczylo.png" alt="Lukasz Raczylo" class="w-10 h-10 rounded-full border border-neutral-700 group-hover:border-neutral-500 transition-colors" />
|
||||
<div class="text-left">
|
||||
<div class="text-sm font-medium">Lukasz Raczylo</div>
|
||||
<div class="text-xs text-neutral-600">MIT License</div>
|
||||
</div>
|
||||
</a>
|
||||
</div>
|
||||
</section>
|
||||
</main>
|
||||
|
||||
<FooterSection />
|
||||
</div>
|
||||
@@ -411,10 +179,8 @@
|
||||
import { ref } from 'vue'
|
||||
import NavBar from './components/NavBar.vue'
|
||||
import HeroSection from './components/HeroSection.vue'
|
||||
import SectionHeader from './components/SectionHeader.vue'
|
||||
import FeatureCard from './components/FeatureCard.vue'
|
||||
import StepCard from './components/StepCard.vue'
|
||||
import FlowItem from './components/FlowItem.vue'
|
||||
import FeatureItem from './components/FeatureItem.vue'
|
||||
import TechCard from './components/TechCard.vue'
|
||||
import CodeBlock from './components/CodeBlock.vue'
|
||||
import FaqItem from './components/FaqItem.vue'
|
||||
import FooterSection from './components/FooterSection.vue'
|
||||
@@ -423,22 +189,15 @@ const mobileMenuOpen = ref(false)
|
||||
const activeTab = ref('macos')
|
||||
|
||||
const features = [
|
||||
{ icon: 'fas fa-brain', title: 'Learns as you work', description: 'Every bug fix, every architecture decision, every "aha moment" - captured automatically without breaking your flow.' },
|
||||
{ icon: 'fas fa-search', title: 'Two-stage retrieval', description: 'Cross-encoder reranking delivers highly relevant results. Finds what you need even with vague queries like "that auth thing".' },
|
||||
{ icon: 'fas fa-project-diagram', title: 'Graph-based search', description: 'LEANN Phase 2: Graph relationships between observations (file overlap, semantic similarity, temporal proximity) for smarter context retrieval.' },
|
||||
{ icon: 'fas fa-microchip', title: 'AST-aware chunking', description: 'Intelligent code splitting respects function boundaries. Go, Python, and TypeScript code is chunked at semantic boundaries, not arbitrary line counts.' },
|
||||
{ icon: 'fas fa-database', title: 'Hybrid vector storage', description: 'LEANN-inspired selective storage: frequently-accessed "hub" observations store embeddings, others recompute on-demand. 60-80% storage savings with <50ms latency.' },
|
||||
{ icon: 'fas fa-folder-tree', title: 'Project-aware context', description: 'Your React knowledge stays in React projects. Your Go patterns stay in Go projects. No context pollution.' },
|
||||
{ icon: 'fas fa-chart-line', title: 'Smart scoring', description: 'Importance decay, pattern detection, and conflict resolution ensure the most valuable memories surface first.' },
|
||||
{ icon: 'fas fa-gauge-high', title: 'Auto-tuning', description: 'Dynamic hub threshold adjustment based on query performance. Automatically balances storage efficiency with search latency for your workload.' },
|
||||
{ icon: 'fas fa-lock', title: '100% private', description: 'Your code context never leaves your machine. No telemetry. No cloud sync. Your memories are yours.' },
|
||||
]
|
||||
|
||||
const steps = [
|
||||
{ number: 1, title: 'Install', description: 'One command. Hooks into Claude Code automatically.' },
|
||||
{ number: 2, title: 'Work normally', description: 'Code with Claude as usual. Mnemonic listens silently in the background.' },
|
||||
{ number: 3, title: 'Knowledge builds', description: 'Every session adds to your knowledge base. Bug fixes. Decisions. Patterns.' },
|
||||
{ number: 4, title: 'Context appears', description: 'Start a new session - relevant memories are already there. No re-explaining.' },
|
||||
{ icon: 'fas fa-brain', title: 'Session memory', description: 'Captures bug fixes, decisions, and patterns automatically from your Claude Code sessions.' },
|
||||
{ icon: 'fas fa-magnifying-glass-chart', title: 'Two-stage retrieval', description: 'Bi-encoder embeddings followed by cross-encoder reranking for accurate results.' },
|
||||
{ icon: 'fas fa-diagram-project', title: 'Knowledge graph', description: 'Automatic relationship detection between observations via file overlap and semantic similarity.' },
|
||||
{ icon: 'fas fa-folder-tree', title: 'Project isolation', description: 'Each project has its own memory. Your React knowledge doesn\'t leak into Go projects.' },
|
||||
{ icon: 'fas fa-code', title: 'AST-aware chunking', description: 'Tree-sitter splits Go, Python, and TypeScript code at semantic boundaries.' },
|
||||
{ icon: 'fas fa-database', title: 'Hybrid vector storage', description: 'Selective embedding caching. 60-80% storage reduction with minimal latency impact.' },
|
||||
{ icon: 'fas fa-lock', title: 'Local embeddings', description: 'ONNX Runtime with all-MiniLM-L6-v2. No external API calls, everything stays on your machine.' },
|
||||
{ icon: 'fas fa-gauge', title: 'Live statusline', description: 'Real-time metrics in Claude Code: [mnemonic] ● served:42 | project:28 memories' },
|
||||
{ icon: 'fas fa-arrows-rotate', title: 'Auto-updates', description: 'Checks for new versions on startup, downloads in the background, applies on restart.' },
|
||||
]
|
||||
|
||||
const installTabs = [
|
||||
@@ -447,36 +206,37 @@ const installTabs = [
|
||||
{ id: 'source', label: 'From Source' },
|
||||
]
|
||||
|
||||
const installCommands = {
|
||||
macos: `curl -sSL https://raw.githubusercontent.com/lukaszraczylo/claude-mnemonic/main/scripts/install.sh | bash`,
|
||||
windows: `irm https://raw.githubusercontent.com/lukaszraczylo/claude-mnemonic/main/scripts/install.ps1 | iex`,
|
||||
source: `git clone https://github.com/lukaszraczylo/claude-mnemonic.git\ncd claude-mnemonic\nmake build && make install`,
|
||||
}
|
||||
|
||||
const configOptions = [
|
||||
{ name: 'CLAUDE_MNEMONIC_WORKER_PORT', description: 'HTTP port for the worker service (default: 37777)', icon: 'fas fa-network-wired' },
|
||||
{ name: 'CLAUDE_MNEMONIC_CONTEXT_OBSERVATIONS', description: 'Maximum observations injected per session (default: 100)', icon: 'fas fa-layer-group' },
|
||||
{ name: 'CLAUDE_MNEMONIC_RERANKING_ENABLED', description: 'Enable cross-encoder reranking for improved search relevance (default: true)', icon: 'fas fa-sort-amount-down' },
|
||||
{ name: 'CLAUDE_MNEMONIC_CONTEXT_RELEVANCE_THRESHOLD', description: 'Minimum similarity score for inclusion, 0.0-1.0 (default: 0.3)', icon: 'fas fa-filter' },
|
||||
{ name: 'CLAUDE_MNEMONIC_VECTOR_STORAGE_STRATEGY', description: 'Storage strategy: "hub" (default), "always", or "on_demand"', icon: 'fas fa-database' },
|
||||
{ name: 'CLAUDE_MNEMONIC_GRAPH_ENABLED', description: 'Enable graph-based search with observation relationships (default: true)', icon: 'fas fa-project-diagram' },
|
||||
{ name: 'CLAUDE_MNEMONIC_GRAPH_MAX_HOPS', description: 'Maximum graph traversal depth for search expansion (default: 2)', icon: 'fas fa-route' },
|
||||
{ name: 'CLAUDE_MNEMONIC_GRAPH_REBUILD_INTERVAL_MIN', description: 'How often to rebuild the observation graph in minutes (default: 60)', icon: 'fas fa-clock' },
|
||||
{ name: 'WORKER_PORT', description: 'Dashboard and API port (default: 37777)' },
|
||||
{ name: 'CONTEXT_OBSERVATIONS', description: 'Max memories injected per session (default: 100)' },
|
||||
{ name: 'CONTEXT_FULL_COUNT', description: 'Full-detail memories, rest condensed (default: 25)' },
|
||||
{ name: 'CONTEXT_RELEVANCE_THRESHOLD', description: 'Min similarity score 0.0-1.0 (default: 0.3)' },
|
||||
{ name: 'RERANKING_ENABLED', description: 'Cross-encoder reranking (default: true)' },
|
||||
{ name: 'VECTOR_STORAGE_STRATEGY', description: 'hub (default), always, or on_demand' },
|
||||
{ name: 'GRAPH_ENABLED', description: 'Graph-based search with relationships (default: true)' },
|
||||
{ name: 'EMBEDDING_MODEL', description: 'Embedding model for semantic search (default: bge-v1.5)' },
|
||||
]
|
||||
|
||||
const requiredDeps = [
|
||||
{ name: 'Claude Code CLI', description: 'Host application - this is a plugin for Claude Code. Uses your existing subscription (Pro/Max) instead of API keys.', icon: 'fas fa-terminal' },
|
||||
{ name: 'jq', description: 'JSON processor used during installation. Usually pre-installed on most systems.', icon: 'fas fa-code' },
|
||||
{ name: 'Claude Code CLI', description: 'Host application. Uses your existing subscription.', icon: 'fas fa-terminal' },
|
||||
{ name: 'jq', description: 'JSON processing during install. Usually pre-installed.', icon: 'fas fa-code' },
|
||||
]
|
||||
|
||||
const techStack = [
|
||||
{ name: 'Go', description: 'Single binary, fast startup, low memory, zero runtime dependencies.' },
|
||||
{ name: 'SQLite + FTS5', description: 'Full-text search in a single file. Survives restarts.' },
|
||||
{ name: 'sqlite-vec', description: 'Vector search embedded in SQLite with LEANN-inspired selective embeddings.' },
|
||||
{ name: 'BGE reranker', description: 'Cross-encoder for two-stage retrieval: bi-encoder + reranking.' },
|
||||
{ name: 'Tree-sitter', description: 'AST-aware code chunking for Go, Python, and TypeScript.' },
|
||||
{ name: 'CSR Graph', description: 'Memory-efficient observation relationship graph with edge detection.' },
|
||||
]
|
||||
|
||||
const faqs = [
|
||||
{ question: 'Will it confuse Claude with wrong context?', answer: 'No. Mnemonic uses project isolation and semantic relevance scoring. Only memories from the current project (or global best practices) are injected, and only when they\'re actually relevant to your prompt.' },
|
||||
{ question: 'What exactly gets saved?', answer: 'Bug fixes with context ("Fixed race condition by adding mutex"), architecture decisions ("Using repository pattern for data access"), conventions ("All API routes prefixed with /api/v1"), and learnings you want to preserve.' },
|
||||
{ question: 'How does hybrid vector storage work?', answer: 'LEANN-inspired selective storage: frequently-accessed "hub" observations (identified by access patterns and graph centrality) store embeddings. Infrequently-accessed observations recompute embeddings on-demand during search. This reduces storage by 60-80% with minimal latency impact (<50ms).' },
|
||||
{ question: 'Can I delete or edit memories?', answer: 'Yes. The web dashboard at localhost:37777 lets you browse, search, edit, and delete any memory. You can also view graph relationships, storage metrics, and performance analytics. You\'re always in control.' },
|
||||
{ question: 'Does it work with my existing Claude Code setup?', answer: 'Yes. Mnemonic installs as a Claude Code plugin with hooks. Your existing workflows, settings, and shortcuts remain unchanged.' },
|
||||
{ question: 'What if I switch between projects frequently?', answer: 'That\'s the point. Each project has isolated memories. Switch from your Python ML project to your TypeScript app - context switches automatically.' },
|
||||
{ question: 'Is there a performance impact?', answer: 'Minimal. The Go worker is lightweight (typically under 30MB RAM). Hybrid storage and auto-tuning optimize for your workload. Context injection at session start takes milliseconds for most projects.' },
|
||||
{ question: 'What is AST-aware chunking?', answer: 'When processing code observations, Mnemonic uses Tree-sitter parsers to respect function and class boundaries instead of arbitrary line limits. Go, Python, and TypeScript code is chunked at semantic boundaries for better search accuracy.' },
|
||||
{ question: 'Will it confuse Claude with wrong context?', answer: 'Project isolation and semantic relevance scoring prevent that. Only memories from the current project (or global best practices) are injected, and only when relevant to your prompt.' },
|
||||
{ question: 'What gets stored?', answer: 'Bug fixes with context, architecture decisions, project conventions, security patterns — things you\'d otherwise re-explain each session. No raw conversation dumps.' },
|
||||
{ question: 'Can I delete or edit memories?', answer: 'Yes. The dashboard at localhost:37777 lets you browse, search, edit, and delete anything. You can also view graph relationships and storage metrics.' },
|
||||
{ question: 'Performance impact?', answer: 'The Go worker uses ~30MB RAM. Context injection at session start takes milliseconds for most projects. Hybrid storage auto-tunes for your workload.' },
|
||||
{ question: 'Does it work with my existing setup?', answer: 'Yes. Installs as a Claude Code plugin with hooks. Your workflows, settings, and shortcuts stay the same.' },
|
||||
{ question: 'What if I switch between projects?', answer: 'Each project has isolated memories. Switch from your Python ML project to your TypeScript app — context switches automatically.' },
|
||||
]
|
||||
</script>
|
||||
|
||||
@@ -1,9 +1,9 @@
|
||||
<template>
|
||||
<div class="glass rounded-xl p-4 sm:p-6 relative max-w-4xl mx-auto glow-amber">
|
||||
<button @click="copyCode" class="absolute top-3 sm:top-4 right-3 sm:right-4 bg-slate-800/50 border border-slate-700/50 text-slate-500 hover:text-white hover:border-slate-600 px-2 sm:px-3 py-1 sm:py-1.5 rounded-md text-xs transition-all">
|
||||
{{ copied ? 'Copied!' : 'Copy' }}
|
||||
<div class="rounded-lg border border-neutral-800 bg-neutral-900/30 p-4 relative overflow-hidden">
|
||||
<button @click="copyCode" class="absolute top-3 right-3 text-neutral-600 hover:text-neutral-300 text-xs transition-colors px-2 py-1 rounded border border-neutral-800 hover:border-neutral-600">
|
||||
{{ copied ? 'Copied' : 'Copy' }}
|
||||
</button>
|
||||
<pre class="text-xs sm:text-sm whitespace-pre-wrap break-all font-mono pr-16 sm:pr-20"><slot></slot></pre>
|
||||
<pre class="text-sm whitespace-pre-wrap break-all font-mono text-neutral-300 pr-20 leading-relaxed"><code>{{ code }}</code></pre>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
@@ -11,10 +11,7 @@
|
||||
import { ref } from 'vue'
|
||||
|
||||
const props = defineProps({
|
||||
code: {
|
||||
type: String,
|
||||
default: ''
|
||||
}
|
||||
code: { type: String, default: '' },
|
||||
})
|
||||
|
||||
const copied = ref(false)
|
||||
|
||||
@@ -1,19 +1,22 @@
|
||||
<template>
|
||||
<div class="glass rounded-xl p-4 sm:p-6 hover:border-amber-500/20 transition-colors">
|
||||
<h3 class="text-white font-semibold text-sm sm:text-base mb-2 sm:mb-3">{{ question }}</h3>
|
||||
<p class="text-slate-400 text-xs sm:text-sm leading-relaxed">{{ answer }}</p>
|
||||
<div class="border-b border-neutral-800 last:border-0">
|
||||
<button @click="open = !open" class="w-full text-left flex items-center justify-between py-4">
|
||||
<span class="text-white text-sm font-medium pr-4">{{ question }}</span>
|
||||
<i :class="open ? 'fas fa-minus' : 'fas fa-plus'" class="text-neutral-600 text-xs flex-shrink-0"></i>
|
||||
</button>
|
||||
<div v-if="open" class="pb-4">
|
||||
<p class="text-neutral-500 text-sm leading-relaxed">{{ answer }}</p>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import { ref } from 'vue'
|
||||
|
||||
defineProps({
|
||||
question: {
|
||||
type: String,
|
||||
required: true
|
||||
},
|
||||
answer: {
|
||||
type: String,
|
||||
required: true
|
||||
}
|
||||
question: { type: String, required: true },
|
||||
answer: { type: String, required: true },
|
||||
})
|
||||
|
||||
const open = ref(false)
|
||||
</script>
|
||||
|
||||
@@ -1,27 +0,0 @@
|
||||
<template>
|
||||
<div class="glass rounded-2xl p-5 sm:p-8 relative overflow-hidden group hover:border-amber-500/30 hover:-translate-y-1 transition-all duration-300">
|
||||
<div class="absolute top-0 left-0 right-0 h-0.5 bg-gradient-to-r from-amber-500 to-transparent opacity-0 group-hover:opacity-100 transition-opacity"></div>
|
||||
<div class="w-10 h-10 sm:w-12 sm:h-12 bg-amber-500/15 rounded-xl flex items-center justify-center mb-4 sm:mb-5 text-amber-500 text-lg sm:text-xl">
|
||||
<i :class="icon"></i>
|
||||
</div>
|
||||
<h3 class="text-white font-semibold text-base sm:text-lg mb-2 sm:mb-3">{{ title }}</h3>
|
||||
<p class="text-slate-400 text-xs sm:text-sm leading-relaxed">{{ description }}</p>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
defineProps({
|
||||
icon: {
|
||||
type: String,
|
||||
required: true
|
||||
},
|
||||
title: {
|
||||
type: String,
|
||||
required: true
|
||||
},
|
||||
description: {
|
||||
type: String,
|
||||
required: true
|
||||
}
|
||||
})
|
||||
</script>
|
||||
@@ -0,0 +1,17 @@
|
||||
<template>
|
||||
<div class="p-5 rounded-lg border border-neutral-800 bg-neutral-900/30">
|
||||
<div class="flex items-center gap-3 mb-2">
|
||||
<i :class="[icon, 'text-amber-500 text-sm']"></i>
|
||||
<h3 class="text-white font-semibold text-sm">{{ title }}</h3>
|
||||
</div>
|
||||
<p class="text-neutral-500 text-xs leading-relaxed pl-7">{{ description }}</p>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
defineProps({
|
||||
icon: { type: String, required: true },
|
||||
title: { type: String, required: true },
|
||||
description: { type: String, required: true },
|
||||
})
|
||||
</script>
|
||||
@@ -1,28 +0,0 @@
|
||||
<template>
|
||||
<div class="flex items-start sm:items-center gap-3 sm:gap-4 p-3 sm:p-4 bg-slate-800/50 rounded-lg border-l-4 border-amber-500">
|
||||
<div class="w-8 h-8 sm:w-10 sm:h-10 bg-amber-500/15 rounded-lg flex items-center justify-center text-amber-500 flex-shrink-0 text-sm sm:text-base">
|
||||
<i :class="icon"></i>
|
||||
</div>
|
||||
<div class="min-w-0">
|
||||
<strong class="block text-white text-sm sm:text-base mb-0.5 sm:mb-1">{{ title }}</strong>
|
||||
<span class="text-slate-500 text-xs sm:text-sm block break-words">{{ description }}</span>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
defineProps({
|
||||
icon: {
|
||||
type: String,
|
||||
required: true
|
||||
},
|
||||
title: {
|
||||
type: String,
|
||||
required: true
|
||||
},
|
||||
description: {
|
||||
type: String,
|
||||
required: true
|
||||
}
|
||||
})
|
||||
</script>
|
||||
@@ -1,13 +1,13 @@
|
||||
<template>
|
||||
<footer class="border-t border-slate-800 py-10 sm:py-16 relative">
|
||||
<div class="max-w-6xl mx-auto px-4 sm:px-6 flex flex-col md:flex-row justify-between items-center gap-4 sm:gap-6">
|
||||
<div class="flex flex-wrap justify-center gap-4 sm:gap-6">
|
||||
<footer class="border-t border-neutral-800/80 py-8 px-4 sm:px-6">
|
||||
<div class="max-w-5xl mx-auto flex flex-col sm:flex-row justify-between items-center gap-4">
|
||||
<p class="text-neutral-600 text-xs">MIT License</p>
|
||||
<div class="flex gap-5">
|
||||
<a v-for="link in links" :key="link.href" :href="link.href" target="_blank"
|
||||
class="text-slate-500 hover:text-white text-xs sm:text-sm transition-colors animated-underline">
|
||||
class="text-neutral-600 hover:text-neutral-400 text-xs transition-colors">
|
||||
{{ link.label }}
|
||||
</a>
|
||||
</div>
|
||||
<p class="text-slate-600 text-xs sm:text-sm text-center">{{ copyright }}</p>
|
||||
</div>
|
||||
</footer>
|
||||
</template>
|
||||
@@ -20,12 +20,8 @@ defineProps({
|
||||
{ label: 'GitHub', href: 'https://github.com/lukaszraczylo/claude-mnemonic' },
|
||||
{ label: 'Releases', href: 'https://github.com/lukaszraczylo/claude-mnemonic/releases' },
|
||||
{ label: 'Issues', href: 'https://github.com/lukaszraczylo/claude-mnemonic/issues' },
|
||||
{ label: 'MIT License', href: 'https://github.com/lukaszraczylo/claude-mnemonic/blob/main/LICENSE' },
|
||||
{ label: 'License', href: 'https://github.com/lukaszraczylo/claude-mnemonic/blob/main/LICENSE' },
|
||||
]
|
||||
},
|
||||
copyright: {
|
||||
type: String,
|
||||
default: 'Made with care for the Claude Code community'
|
||||
}
|
||||
})
|
||||
</script>
|
||||
|
||||
@@ -1,44 +1,35 @@
|
||||
<template>
|
||||
<section class="min-h-screen flex items-center relative pt-24 sm:pt-28 pb-16 sm:pb-20">
|
||||
<!-- Background Effects -->
|
||||
<div class="absolute inset-0 overflow-hidden">
|
||||
<div class="absolute top-[-50%] left-1/2 -translate-x-1/2 w-[150%] h-full bg-[radial-gradient(ellipse_at_center,rgba(245,158,11,0.12)_0%,transparent_60%)] opacity-60"></div>
|
||||
</div>
|
||||
<section class="pt-24 pb-8 sm:pt-32 sm:pb-12 px-4 sm:px-6">
|
||||
<div class="max-w-5xl mx-auto">
|
||||
<p class="text-amber-500 text-sm font-medium mb-4 tracking-wide">Persistent memory for Claude Code</p>
|
||||
|
||||
<div class="relative z-10 max-w-6xl mx-auto px-4 sm:px-6 text-center">
|
||||
<div class="inline-flex items-center gap-2 glass px-3 sm:px-4 py-1.5 sm:py-2 rounded-full text-xs sm:text-sm text-slate-400 mb-6 sm:mb-8 opacity-0 animate-fade-in-up">
|
||||
<span class="w-2 h-2 bg-amber-500 rounded-full animate-pulse-slow"></span>
|
||||
{{ badge }}
|
||||
</div>
|
||||
|
||||
<h1 class="text-3xl sm:text-4xl md:text-5xl lg:text-6xl xl:text-7xl font-bold text-white mb-4 sm:mb-6 leading-tight opacity-0 animate-fade-in-up animation-delay-100">
|
||||
{{ titleBefore }}<br class="hidden sm:block">
|
||||
<span class="sm:hidden"> </span>
|
||||
<span class="text-gradient">{{ titleHighlight }}</span>
|
||||
<h1 class="text-4xl sm:text-5xl lg:text-6xl font-extrabold text-white leading-[1.1] mb-6 max-w-3xl">
|
||||
Stop re-explaining your codebase every session.
|
||||
</h1>
|
||||
|
||||
<p class="text-base sm:text-lg md:text-xl text-slate-400 max-w-2xl mx-auto mb-8 sm:mb-10 opacity-0 animate-fade-in-up animation-delay-200 px-2">
|
||||
{{ subtitle }}
|
||||
<p class="text-neutral-400 text-lg sm:text-xl max-w-2xl mb-10 leading-relaxed">
|
||||
Claude Code forgets everything when your session ends. Mnemonic captures what you've already explained — bug fixes, architecture decisions, patterns — and brings it back when relevant.
|
||||
</p>
|
||||
|
||||
<div class="flex flex-col sm:flex-row gap-3 sm:gap-4 justify-center mb-12 sm:mb-16 opacity-0 animate-fade-in-up animation-delay-300 px-4 sm:px-0">
|
||||
<a :href="primaryCta.href" class="inline-flex items-center justify-center gap-2 bg-gradient-to-br from-amber-500 to-amber-400 text-slate-950 px-6 sm:px-8 py-3 sm:py-3.5 rounded-lg font-semibold hover:scale-105 hover:shadow-lg hover:shadow-amber-500/30 transition-all">
|
||||
<i :class="primaryCta.icon"></i>
|
||||
{{ primaryCta.label }}
|
||||
<div class="flex flex-col sm:flex-row gap-3 mb-12">
|
||||
<a href="#install" class="inline-flex items-center justify-center gap-2 bg-white text-neutral-900 px-6 py-3 rounded-lg font-semibold text-sm hover:bg-neutral-200 transition-colors">
|
||||
Install now
|
||||
</a>
|
||||
<a :href="secondaryCta.href" target="_blank" class="inline-flex items-center justify-center gap-2 glass text-white px-6 sm:px-8 py-3 sm:py-3.5 rounded-lg font-medium hover:border-amber-500/30 hover:bg-amber-500/10 transition-all">
|
||||
<i :class="secondaryCta.icon"></i>
|
||||
{{ secondaryCta.label }}
|
||||
<a href="https://github.com/lukaszraczylo/claude-mnemonic" target="_blank" class="inline-flex items-center justify-center gap-2 border border-neutral-700 text-neutral-400 px-6 py-3 rounded-lg font-medium text-sm hover:border-neutral-500 hover:text-white transition-colors">
|
||||
<i class="fab fa-github"></i> View source
|
||||
</a>
|
||||
</div>
|
||||
|
||||
<!-- Install Command -->
|
||||
<div class="opacity-0 animate-fade-in-up animation-delay-400 px-2 sm:px-0">
|
||||
<div class="glass rounded-xl px-4 sm:px-6 py-4 sm:py-5 flex flex-col sm:flex-row items-center justify-between gap-3 sm:gap-4 max-w-4xl mx-auto glow-amber">
|
||||
<code class="text-amber-400 text-xs sm:text-sm md:text-base flex-1 text-center sm:text-left break-all font-mono">{{ installCommand }}</code>
|
||||
<button @click="copyCommand" class="text-slate-500 hover:text-white transition-colors p-2 flex-shrink-0 rounded-lg hover:bg-slate-800/50">
|
||||
<i :class="copied ? 'fas fa-check text-amber-500' : 'fas fa-copy'"></i>
|
||||
<span class="ml-2 text-sm sm:hidden">{{ copied ? 'Copied!' : 'Copy' }}</span>
|
||||
<div class="max-w-2xl">
|
||||
<div class="flex items-stretch bg-neutral-900 border border-neutral-800 rounded-lg overflow-hidden">
|
||||
<div class="flex items-center px-3 gap-1.5 border-r border-neutral-800">
|
||||
<span class="w-3 h-3 rounded-full bg-neutral-700"></span>
|
||||
<span class="w-3 h-3 rounded-full bg-neutral-700"></span>
|
||||
<span class="w-3 h-3 rounded-full bg-neutral-700"></span>
|
||||
</div>
|
||||
<code class="flex-1 text-neutral-300 text-xs sm:text-sm px-4 py-3 font-mono overflow-x-auto">{{ installCommand }}</code>
|
||||
<button @click="copyCommand" class="px-4 text-neutral-600 hover:text-neutral-300 transition-colors border-l border-neutral-800 text-sm" title="Copy">
|
||||
<i :class="copied ? 'fas fa-check text-emerald-500' : 'fas fa-copy'"></i>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
@@ -49,41 +40,11 @@
|
||||
<script setup>
|
||||
import { ref } from 'vue'
|
||||
|
||||
defineProps({
|
||||
badge: {
|
||||
type: String,
|
||||
default: 'Persistent Memory for Claude Code'
|
||||
},
|
||||
titleBefore: {
|
||||
type: String,
|
||||
default: 'Claude forgets.'
|
||||
},
|
||||
titleHighlight: {
|
||||
type: String,
|
||||
default: 'Make it remember.'
|
||||
},
|
||||
subtitle: {
|
||||
type: String,
|
||||
default: 'Capture learnings, decisions, and patterns from your coding sessions. Bring that knowledge back in every future conversation.'
|
||||
},
|
||||
primaryCta: {
|
||||
type: Object,
|
||||
default: () => ({ label: 'Install Now', href: '#installation', icon: 'fas fa-download' })
|
||||
},
|
||||
secondaryCta: {
|
||||
type: Object,
|
||||
default: () => ({ label: 'View Source', href: 'https://github.com/lukaszraczylo/claude-mnemonic', icon: 'fab fa-github' })
|
||||
},
|
||||
installCommand: {
|
||||
type: String,
|
||||
default: 'curl -sSL https://raw.githubusercontent.com/lukaszraczylo/claude-mnemonic/main/scripts/install.sh | bash'
|
||||
}
|
||||
})
|
||||
|
||||
const installCommand = 'curl -sSL https://raw.githubusercontent.com/lukaszraczylo/claude-mnemonic/main/scripts/install.sh | bash'
|
||||
const copied = ref(false)
|
||||
|
||||
function copyCommand() {
|
||||
navigator.clipboard.writeText('curl -sSL https://raw.githubusercontent.com/lukaszraczylo/claude-mnemonic/main/scripts/install.sh | bash')
|
||||
navigator.clipboard.writeText(installCommand)
|
||||
copied.value = true
|
||||
setTimeout(() => copied.value = false, 2000)
|
||||
}
|
||||
|
||||
@@ -1,70 +1,45 @@
|
||||
<template>
|
||||
<nav class="fixed top-0 left-0 right-0 z-50 bg-gradient-to-b from-slate-950 via-slate-950/95 to-transparent backdrop-blur-md">
|
||||
<div class="max-w-6xl mx-auto px-4 sm:px-6 py-4 sm:py-5 flex justify-between items-center">
|
||||
<a href="#" class="flex items-center gap-2 sm:gap-3 text-white font-bold text-xl sm:text-2xl">
|
||||
<div class="w-8 h-8 sm:w-9 sm:h-9 bg-gradient-to-br from-amber-500 to-amber-400 rounded-lg flex items-center justify-center">
|
||||
<i class="fas fa-brain text-slate-950 text-sm sm:text-base"></i>
|
||||
</div>
|
||||
{{ title }}
|
||||
</a>
|
||||
<nav class="border-b border-neutral-800/80 sticky top-0 z-50 bg-[#09090b]/90 backdrop-blur-sm">
|
||||
<div class="max-w-5xl mx-auto px-4 sm:px-6 h-14 flex justify-between items-center">
|
||||
<a href="#" class="text-white font-bold text-base tracking-tight">Claude Mnemonic</a>
|
||||
|
||||
<ul class="hidden md:flex gap-6 lg:gap-8">
|
||||
<ul class="hidden md:flex items-center gap-6">
|
||||
<li v-for="link in links" :key="link.href">
|
||||
<a :href="link.href" class="text-slate-400 hover:text-white text-sm font-medium transition-colors animated-underline">
|
||||
{{ link.label }}
|
||||
</a>
|
||||
<a :href="link.href" class="text-neutral-500 hover:text-white text-sm transition-colors">{{ link.label }}</a>
|
||||
</li>
|
||||
</ul>
|
||||
|
||||
<div class="flex items-center gap-3 sm:gap-4">
|
||||
<a :href="githubUrl" target="_blank" class="text-slate-400 hover:text-white text-lg sm:text-xl transition-colors">
|
||||
<div class="flex items-center gap-4">
|
||||
<a :href="githubUrl" target="_blank" class="text-neutral-500 hover:text-white transition-colors text-lg">
|
||||
<i class="fab fa-github"></i>
|
||||
</a>
|
||||
<a href="#installation" class="hidden sm:inline-flex bg-gradient-to-br from-amber-500 to-amber-400 text-slate-950 px-4 sm:px-5 py-2 sm:py-2.5 rounded-lg font-semibold text-sm hover:scale-105 hover:shadow-lg hover:shadow-amber-500/30 transition-all">
|
||||
Get Started
|
||||
<a href="#install" class="hidden sm:inline-flex items-center gap-2 text-sm font-medium text-amber-500 hover:text-amber-400 transition-colors">
|
||||
Install <i class="fas fa-arrow-right text-xs"></i>
|
||||
</a>
|
||||
<button @click="$emit('toggle-menu')" class="md:hidden text-white text-xl p-2">
|
||||
<button @click="$emit('toggle-menu')" class="md:hidden text-neutral-500 hover:text-white p-1">
|
||||
<i :class="mobileMenuOpen ? 'fas fa-times' : 'fas fa-bars'"></i>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Mobile Menu -->
|
||||
<Transition
|
||||
enter-active-class="transition duration-200 ease-out"
|
||||
enter-from-class="opacity-0 -translate-y-2"
|
||||
enter-to-class="opacity-100 translate-y-0"
|
||||
leave-active-class="transition duration-150 ease-in"
|
||||
leave-from-class="opacity-100 translate-y-0"
|
||||
leave-to-class="opacity-0 -translate-y-2"
|
||||
>
|
||||
<div v-if="mobileMenuOpen" class="md:hidden bg-slate-900/95 backdrop-blur-xl border-t border-slate-800 px-4 sm:px-6 py-4">
|
||||
<a v-for="link in links" :key="link.href" :href="link.href"
|
||||
@click="$emit('toggle-menu')"
|
||||
class="block py-3 text-slate-400 hover:text-white border-b border-slate-800 last:border-0 transition-colors">
|
||||
{{ link.label }}
|
||||
</a>
|
||||
<a href="#installation" @click="$emit('toggle-menu')"
|
||||
class="mt-4 block text-center bg-gradient-to-br from-amber-500 to-amber-400 text-slate-950 px-5 py-2.5 rounded-lg font-semibold text-sm">
|
||||
Get Started
|
||||
</a>
|
||||
</div>
|
||||
</Transition>
|
||||
<div v-if="mobileMenuOpen" class="md:hidden border-t border-neutral-800/80 px-6 py-4 space-y-1">
|
||||
<a v-for="link in links" :key="link.href" :href="link.href"
|
||||
@click="$emit('toggle-menu')"
|
||||
class="block py-2 text-neutral-500 hover:text-white text-sm transition-colors">
|
||||
{{ link.label }}
|
||||
</a>
|
||||
</div>
|
||||
</nav>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
defineProps({
|
||||
title: {
|
||||
type: String,
|
||||
default: 'Mnemonic'
|
||||
},
|
||||
links: {
|
||||
type: Array,
|
||||
default: () => [
|
||||
{ label: 'Features', href: '#features' },
|
||||
{ label: 'How It Works', href: '#how-it-works' },
|
||||
{ label: 'Install', href: '#installation' },
|
||||
{ label: 'Install', href: '#install' },
|
||||
{ label: 'Config', href: '#config' },
|
||||
{ label: 'FAQ', href: '#faq' },
|
||||
]
|
||||
},
|
||||
|
||||
@@ -1,20 +0,0 @@
|
||||
<template>
|
||||
<div class="text-center mb-10 sm:mb-16">
|
||||
<h2 class="text-2xl sm:text-3xl md:text-4xl lg:text-5xl font-bold text-white mb-3 sm:mb-4">
|
||||
{{ title }}
|
||||
</h2>
|
||||
<p v-if="subtitle" class="text-sm sm:text-base md:text-lg text-slate-400 max-w-xl mx-auto px-2">
|
||||
{{ subtitle }}
|
||||
</p>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
defineProps({
|
||||
title: {
|
||||
type: String,
|
||||
required: true
|
||||
},
|
||||
subtitle: String
|
||||
})
|
||||
</script>
|
||||
@@ -1,26 +0,0 @@
|
||||
<template>
|
||||
<div class="text-center relative">
|
||||
<div class="w-14 h-14 sm:w-20 sm:h-20 bg-slate-900 border-2 border-amber-500 rounded-full flex items-center justify-center mx-auto mb-4 sm:mb-6 text-amber-500 font-bold text-xl sm:text-3xl relative z-10">
|
||||
{{ number }}
|
||||
</div>
|
||||
<h3 class="text-white font-semibold text-base sm:text-lg mb-2 sm:mb-3">{{ title }}</h3>
|
||||
<p class="text-slate-400 text-xs sm:text-sm">{{ description }}</p>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
defineProps({
|
||||
number: {
|
||||
type: [String, Number],
|
||||
required: true
|
||||
},
|
||||
title: {
|
||||
type: String,
|
||||
required: true
|
||||
},
|
||||
description: {
|
||||
type: String,
|
||||
required: true
|
||||
}
|
||||
})
|
||||
</script>
|
||||
@@ -0,0 +1,13 @@
|
||||
<template>
|
||||
<div class="p-4 rounded-lg border border-neutral-800">
|
||||
<div class="text-white font-semibold text-sm mb-1">{{ name }}</div>
|
||||
<p class="text-neutral-500 text-xs leading-relaxed">{{ description }}</p>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
defineProps({
|
||||
name: { type: String, required: true },
|
||||
description: { type: String, required: true },
|
||||
})
|
||||
</script>
|
||||
+9
-97
@@ -1,6 +1,9 @@
|
||||
@tailwind base;
|
||||
@tailwind components;
|
||||
@tailwind utilities;
|
||||
@import "tailwindcss";
|
||||
|
||||
@theme {
|
||||
--font-sans: 'Inter', system-ui, -apple-system, BlinkMacSystemFont, 'Segoe UI', 'Roboto', sans-serif;
|
||||
--font-mono: 'SF Mono', Monaco, Consolas, 'Liberation Mono', 'Courier New', monospace;
|
||||
}
|
||||
|
||||
@layer base {
|
||||
html {
|
||||
@@ -10,105 +13,14 @@
|
||||
}
|
||||
|
||||
body {
|
||||
@apply font-sans text-slate-300;
|
||||
}
|
||||
|
||||
h1, h2, h3, h4, h5, h6 {
|
||||
@apply font-sans font-bold tracking-tight;
|
||||
@apply font-sans text-neutral-300 bg-[#09090b];
|
||||
}
|
||||
|
||||
code, pre {
|
||||
@apply font-mono;
|
||||
}
|
||||
}
|
||||
|
||||
@layer components {
|
||||
/* Animated gradient background */
|
||||
.bg-animated-gradient {
|
||||
background: linear-gradient(
|
||||
135deg,
|
||||
rgba(245, 158, 11, 0.1) 0%,
|
||||
transparent 50%,
|
||||
rgba(245, 158, 11, 0.05) 100%
|
||||
);
|
||||
background-size: 200% 200%;
|
||||
animation: gradientShift 8s ease-in-out infinite;
|
||||
}
|
||||
|
||||
/* Floating particles */
|
||||
.particle {
|
||||
position: absolute;
|
||||
width: 4px;
|
||||
height: 4px;
|
||||
background: rgba(245, 158, 11, 0.4);
|
||||
border-radius: 50%;
|
||||
pointer-events: none;
|
||||
}
|
||||
|
||||
/* Glass effect */
|
||||
.glass {
|
||||
@apply bg-slate-900/60 backdrop-blur-xl border border-slate-700/50;
|
||||
}
|
||||
|
||||
/* Glow effect */
|
||||
.glow-amber {
|
||||
box-shadow: 0 0 40px rgba(245, 158, 11, 0.15);
|
||||
}
|
||||
|
||||
/* Animated underline */
|
||||
.animated-underline {
|
||||
position: relative;
|
||||
}
|
||||
.animated-underline::after {
|
||||
content: '';
|
||||
position: absolute;
|
||||
bottom: -2px;
|
||||
left: 0;
|
||||
width: 0;
|
||||
height: 2px;
|
||||
background: linear-gradient(90deg, #f59e0b, #fbbf24);
|
||||
transition: width 0.3s ease;
|
||||
}
|
||||
.animated-underline:hover::after {
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
/* Animated border */
|
||||
.animated-border {
|
||||
position: relative;
|
||||
overflow: hidden;
|
||||
}
|
||||
.animated-border::before {
|
||||
content: '';
|
||||
position: absolute;
|
||||
top: 0;
|
||||
left: -100%;
|
||||
width: 100%;
|
||||
height: 1px;
|
||||
background: linear-gradient(90deg, transparent, #f59e0b, transparent);
|
||||
animation: borderSlide 3s linear infinite;
|
||||
}
|
||||
|
||||
@keyframes borderSlide {
|
||||
0% { left: -100%; }
|
||||
100% { left: 100%; }
|
||||
}
|
||||
}
|
||||
|
||||
@layer utilities {
|
||||
.animation-delay-100 { animation-delay: 0.1s; }
|
||||
.animation-delay-200 { animation-delay: 0.2s; }
|
||||
.animation-delay-300 { animation-delay: 0.3s; }
|
||||
.animation-delay-400 { animation-delay: 0.4s; }
|
||||
.animation-delay-500 { animation-delay: 0.5s; }
|
||||
|
||||
/* Text gradient */
|
||||
.text-gradient {
|
||||
@apply bg-gradient-to-r from-amber-400 to-amber-500 bg-clip-text text-transparent;
|
||||
}
|
||||
|
||||
/* Opacity starting state for animations */
|
||||
.opacity-0-start {
|
||||
opacity: 0;
|
||||
::selection {
|
||||
@apply bg-amber-500/30 text-white;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,63 +0,0 @@
|
||||
/** @type {import('tailwindcss').Config} */
|
||||
export default {
|
||||
content: [
|
||||
"./index.html",
|
||||
"./src/**/*.{vue,js,ts,jsx,tsx}",
|
||||
],
|
||||
darkMode: 'class',
|
||||
theme: {
|
||||
extend: {
|
||||
colors: {
|
||||
amber: {
|
||||
DEFAULT: '#f59e0b',
|
||||
light: '#fbbf24',
|
||||
glow: 'rgba(245, 158, 11, 0.15)',
|
||||
},
|
||||
slate: {
|
||||
950: '#0a0f1a',
|
||||
}
|
||||
},
|
||||
fontFamily: {
|
||||
sans: ['Inter', 'system-ui', '-apple-system', 'BlinkMacSystemFont', 'Segoe UI', 'Roboto', 'sans-serif'],
|
||||
mono: ['SF Mono', 'Monaco', 'Consolas', 'Liberation Mono', 'Courier New', 'monospace'],
|
||||
},
|
||||
animation: {
|
||||
'fade-in-up': 'fadeInUp 0.8s ease-out forwards',
|
||||
'pulse-slow': 'pulse 3s ease-in-out infinite',
|
||||
'float': 'float 6s ease-in-out infinite',
|
||||
'float-delayed': 'float 6s ease-in-out 3s infinite',
|
||||
'glow': 'glow 4s ease-in-out infinite',
|
||||
'gradient-shift': 'gradientShift 8s ease-in-out infinite',
|
||||
'particle': 'particle 20s linear infinite',
|
||||
},
|
||||
keyframes: {
|
||||
fadeInUp: {
|
||||
'0%': { opacity: '0', transform: 'translateY(30px)' },
|
||||
'100%': { opacity: '1', transform: 'translateY(0)' },
|
||||
},
|
||||
float: {
|
||||
'0%, 100%': { transform: 'translateY(0px)' },
|
||||
'50%': { transform: 'translateY(-20px)' },
|
||||
},
|
||||
glow: {
|
||||
'0%, 100%': { opacity: '0.5' },
|
||||
'50%': { opacity: '0.8' },
|
||||
},
|
||||
gradientShift: {
|
||||
'0%, 100%': { backgroundPosition: '0% 50%' },
|
||||
'50%': { backgroundPosition: '100% 50%' },
|
||||
},
|
||||
particle: {
|
||||
'0%': { transform: 'translateY(100vh) rotate(0deg)', opacity: '0' },
|
||||
'10%': { opacity: '1' },
|
||||
'90%': { opacity: '1' },
|
||||
'100%': { transform: 'translateY(-100vh) rotate(720deg)', opacity: '0' },
|
||||
},
|
||||
},
|
||||
backgroundSize: {
|
||||
'200%': '200% 200%',
|
||||
},
|
||||
},
|
||||
},
|
||||
plugins: [],
|
||||
}
|
||||
@@ -8,16 +8,16 @@ require (
|
||||
github.com/asg017/sqlite-vec-go-bindings v0.1.6
|
||||
github.com/fsnotify/fsnotify v1.10.1
|
||||
github.com/go-chi/chi/v5 v5.3.0
|
||||
github.com/go-gormigrate/gormigrate/v2 v2.1.5
|
||||
github.com/go-gormigrate/gormigrate/v2 v2.1.6
|
||||
github.com/goccy/go-json v0.10.6
|
||||
github.com/lukaszraczylo/oss-telemetry v0.2.1
|
||||
github.com/mattn/go-sqlite3 v1.14.44
|
||||
github.com/lukaszraczylo/oss-telemetry v0.2.3
|
||||
github.com/mattn/go-sqlite3 v1.14.45
|
||||
github.com/rs/zerolog v1.35.1
|
||||
github.com/smacker/go-tree-sitter v0.0.0-20240827094217-dd81d9e9be82
|
||||
github.com/stretchr/testify v1.11.1
|
||||
github.com/sugarme/tokenizer v0.3.0
|
||||
github.com/yalue/onnxruntime_go v1.30.1
|
||||
golang.org/x/sync v0.20.0
|
||||
github.com/yalue/onnxruntime_go v1.31.0
|
||||
golang.org/x/sync v0.21.0
|
||||
gorm.io/driver/sqlite v1.6.0
|
||||
gorm.io/gorm v1.31.1
|
||||
)
|
||||
@@ -27,7 +27,7 @@ require (
|
||||
github.com/emirpasic/gods v1.18.1 // indirect
|
||||
github.com/jinzhu/inflection v1.0.0 // indirect
|
||||
github.com/jinzhu/now v1.1.5 // indirect
|
||||
github.com/mattn/go-colorable v0.1.14 // indirect
|
||||
github.com/mattn/go-colorable v0.1.15 // indirect
|
||||
github.com/mattn/go-isatty v0.0.22 // indirect
|
||||
github.com/mitchellh/colorstring v0.0.0-20190213212951-d06e56a500db // indirect
|
||||
github.com/patrickmn/go-cache v2.1.0+incompatible // indirect
|
||||
@@ -35,7 +35,7 @@ require (
|
||||
github.com/rivo/uniseg v0.4.7 // indirect
|
||||
github.com/schollz/progressbar/v2 v2.15.0 // indirect
|
||||
github.com/sugarme/regexpset v0.0.0-20200920021344-4d4ec8eaf93c // indirect
|
||||
golang.org/x/sys v0.45.0 // indirect
|
||||
golang.org/x/text v0.37.0 // indirect
|
||||
golang.org/x/sys v0.46.0 // indirect
|
||||
golang.org/x/text v0.38.0 // indirect
|
||||
gopkg.in/yaml.v3 v3.0.1 // indirect
|
||||
)
|
||||
|
||||
@@ -11,22 +11,22 @@ github.com/fsnotify/fsnotify v1.10.1 h1:b0/UzAf9yR5rhf3RPm9gf3ehBPpf0oZKIjtpKrx5
|
||||
github.com/fsnotify/fsnotify v1.10.1/go.mod h1:TLheqan6HD6GBK6PrDWyDPBaEV8LspOxvPSjC+bVfgo=
|
||||
github.com/go-chi/chi/v5 v5.3.0 h1:halUjDxhshgXHMrao5bB8eNBXo/rnzwr8m5m36glehM=
|
||||
github.com/go-chi/chi/v5 v5.3.0/go.mod h1:R+tYY2hNuVUUjxoPtqUdgBqevM9s9njzkTLutVsOCto=
|
||||
github.com/go-gormigrate/gormigrate/v2 v2.1.5 h1:1OyorA5LtdQw12cyJDEHuTrEV3GiXiIhS4/QTTa/SM8=
|
||||
github.com/go-gormigrate/gormigrate/v2 v2.1.5/go.mod h1:mj9ekk/7CPF3VjopaFvWKN2v7fN3D9d3eEOAXRhi/+M=
|
||||
github.com/go-gormigrate/gormigrate/v2 v2.1.6 h1:VtX+l1Stj2v5RGubVQk0LS/8EPGXR+ldcOyCmlmKoyg=
|
||||
github.com/go-gormigrate/gormigrate/v2 v2.1.6/go.mod h1:PZpedQc4tWaxn6kvXicwhinh3L0seLpMc5ReKRX5id4=
|
||||
github.com/goccy/go-json v0.10.6 h1:p8HrPJzOakx/mn/bQtjgNjdTcN+/S6FcG2CTtQOrHVU=
|
||||
github.com/goccy/go-json v0.10.6/go.mod h1:oq7eo15ShAhp70Anwd5lgX2pLfOS3QCiwU/PULtXL6M=
|
||||
github.com/jinzhu/inflection v1.0.0 h1:K317FqzuhWc8YvSVlFMCCUb36O/S9MCKRDI7QkRKD/E=
|
||||
github.com/jinzhu/inflection v1.0.0/go.mod h1:h+uFLlag+Qp1Va5pdKtLDYj+kHp5pxUVkryuEj+Srlc=
|
||||
github.com/jinzhu/now v1.1.5 h1:/o9tlHleP7gOFmsnYNz3RGnqzefHA47wQpKrrdTIwXQ=
|
||||
github.com/jinzhu/now v1.1.5/go.mod h1:d3SSVoowX0Lcu0IBviAWJpolVfI5UJVZZ7cO71lE/z8=
|
||||
github.com/lukaszraczylo/oss-telemetry v0.2.1 h1:6ULyfzXplpdmIY/i01OPM1jeod9+L1RAhI0jtbVnJI0=
|
||||
github.com/lukaszraczylo/oss-telemetry v0.2.1/go.mod h1:+Cn78qZo8rc3T9eZt0v3oICYRdd75wORtSidc8lNjDQ=
|
||||
github.com/mattn/go-colorable v0.1.14 h1:9A9LHSqF/7dyVVX6g0U9cwm9pG3kP9gSzcuIPHPsaIE=
|
||||
github.com/mattn/go-colorable v0.1.14/go.mod h1:6LmQG8QLFO4G5z1gPvYEzlUgJ2wF+stgPZH1UqBm1s8=
|
||||
github.com/lukaszraczylo/oss-telemetry v0.2.3 h1:xoDtBqeZGmXj7IteiE1M5WMuzeoqag58qEleI0Cf2Ms=
|
||||
github.com/lukaszraczylo/oss-telemetry v0.2.3/go.mod h1:+Cn78qZo8rc3T9eZt0v3oICYRdd75wORtSidc8lNjDQ=
|
||||
github.com/mattn/go-colorable v0.1.15 h1:+u9SLTRGnXv73cEsnsmoZBom+dMU88B2M0aDcWy0/jY=
|
||||
github.com/mattn/go-colorable v0.1.15/go.mod h1:6LmQG8QLFO4G5z1gPvYEzlUgJ2wF+stgPZH1UqBm1s8=
|
||||
github.com/mattn/go-isatty v0.0.22 h1:j8l17JJ9i6VGPUFUYoTUKPSgKe/83EYU2zBC7YNKMw4=
|
||||
github.com/mattn/go-isatty v0.0.22/go.mod h1:ZXfXG4SQHsB/w3ZeOYbR0PrPwLy+n6xiMrJlRFqopa4=
|
||||
github.com/mattn/go-sqlite3 v1.14.44 h1:3VSe+xafpbzsLbdr2AWlAZk9yRHiBhTBakioXaCKTF8=
|
||||
github.com/mattn/go-sqlite3 v1.14.44/go.mod h1:pjEuOr8IwzLJP2MfGeTb0A35jauH+C2kbHKBr7yXKVQ=
|
||||
github.com/mattn/go-sqlite3 v1.14.45 h1:6KA/spDguL3KV8rnybG7ezSaE4SeMR3KC9VbUoAQaIk=
|
||||
github.com/mattn/go-sqlite3 v1.14.45/go.mod h1:pjEuOr8IwzLJP2MfGeTb0A35jauH+C2kbHKBr7yXKVQ=
|
||||
github.com/mitchellh/colorstring v0.0.0-20190213212951-d06e56a500db h1:62I3jR2EmQ4l5rM/4FEfDWcRD+abF5XlKShorW5LRoQ=
|
||||
github.com/mitchellh/colorstring v0.0.0-20190213212951-d06e56a500db/go.mod h1:l0dey0ia/Uv7NcFFVbCLtqEBQbrT4OCwCSKTEv6enCw=
|
||||
github.com/patrickmn/go-cache v2.1.0+incompatible h1:HRMgzkcYKYpi3C8ajMPV8OFXaaRUnok+kx1WdO15EQc=
|
||||
@@ -47,14 +47,14 @@ github.com/stretchr/testify v1.11.1 h1:7s2iGBzp5EwR7/aIZr8ao5+dra3wiQyKjjFuvgVKu
|
||||
github.com/stretchr/testify v1.11.1/go.mod h1:wZwfW3scLgRK+23gO65QZefKpKQRnfz6sD981Nm4B6U=
|
||||
github.com/sugarme/regexpset v0.0.0-20200920021344-4d4ec8eaf93c h1:pwb4kNSHb4K89ymCaN+5lPH/MwnfSVg4rzGDh4d+iy4=
|
||||
github.com/sugarme/regexpset v0.0.0-20200920021344-4d4ec8eaf93c/go.mod h1:2gwkXLWbDGUQWeL3RtpCmcY4mzCtU13kb9UsAg9xMaw=
|
||||
github.com/yalue/onnxruntime_go v1.30.1 h1:NaEng5lWbsHZ/8X1dtaw1mIj7eV1ozyjbFo//g0ktl4=
|
||||
github.com/yalue/onnxruntime_go v1.30.1/go.mod h1:b4X26A8pekNb1ACJ58wAXgNKeUCGEAQ9dmACut9Sm/4=
|
||||
golang.org/x/sync v0.20.0 h1:e0PTpb7pjO8GAtTs2dQ6jYa5BWYlMuX047Dco/pItO4=
|
||||
golang.org/x/sync v0.20.0/go.mod h1:9xrNwdLfx4jkKbNva9FpL6vEN7evnE43NNNJQ2LF3+0=
|
||||
golang.org/x/sys v0.45.0 h1:dO4czNzziLiiXplLQgBCEpCvXQ3dnkn0SdaZSYdQ+FY=
|
||||
golang.org/x/sys v0.45.0/go.mod h1:4GL1E5IUh+htKOUEOaiffhrAeqysfVGipDYzABqnCmw=
|
||||
golang.org/x/text v0.37.0 h1:Cqjiwd9eSg8e0QAkyCaQTNHFIIzWtidPahFWR83rTrc=
|
||||
golang.org/x/text v0.37.0/go.mod h1:a5sjxXGs9hsn/AJVwuElvCAo9v8QYLzvavO5z2PiM38=
|
||||
github.com/yalue/onnxruntime_go v1.31.0 h1:1ln4YW1SFOFfGJZXe3jNOb2JUSt+l2pEneZfV8HdtFA=
|
||||
github.com/yalue/onnxruntime_go v1.31.0/go.mod h1:b4X26A8pekNb1ACJ58wAXgNKeUCGEAQ9dmACut9Sm/4=
|
||||
golang.org/x/sync v0.21.0 h1:HLII4xRRTtCRkxYp4HNFF0Js/Og6q2i++KXbg0gHCwM=
|
||||
golang.org/x/sync v0.21.0/go.mod h1:9xrNwdLfx4jkKbNva9FpL6vEN7evnE43NNNJQ2LF3+0=
|
||||
golang.org/x/sys v0.46.0 h1:noSf2Fq6F8DBgS+LysIkx7rIExoNHJsxOAtPp4rthXw=
|
||||
golang.org/x/sys v0.46.0/go.mod h1:4GL1E5IUh+htKOUEOaiffhrAeqysfVGipDYzABqnCmw=
|
||||
golang.org/x/text v0.38.0 h1:sXmwo9DwP3OK9EZ7PqAdaooSGozfl/3a6/xJcbzPRhE=
|
||||
golang.org/x/text v0.38.0/go.mod h1:YXZt3QhHUKYT53r2lLKFIVi6Ao1jdzrTR/KQ09qyxF4=
|
||||
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405 h1:yhCVgyC4o1eVCa2tZl7eS0r+SDo693bJlVdllGtEeKM=
|
||||
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
|
||||
gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA=
|
||||
|
||||
+51
-38
@@ -39,44 +39,46 @@ var CriticalConcepts = []string{
|
||||
// Config holds the application configuration.
|
||||
// Field order optimized for memory alignment (fieldalignment).
|
||||
type Config struct {
|
||||
ContextFullField string `json:"context_full_field"`
|
||||
DBPath string `json:"db_path"`
|
||||
Model string `json:"model"`
|
||||
ClaudeCodePath string `json:"claude_code_path"`
|
||||
EmbeddingModel string `json:"embedding_model"`
|
||||
VectorStorageStrategy string `json:"vector_storage_strategy"`
|
||||
ContextObsConcepts []string `json:"context_obs_concepts"`
|
||||
ContextObsTypes []string `json:"context_obs_types"`
|
||||
ContextFullCount int `json:"context_full_count"`
|
||||
GraphBranchFactor int `json:"graph_branch_factor"`
|
||||
GraphEdgeWeight float64 `json:"graph_edge_weight"`
|
||||
ContextRelevanceThreshold float64 `json:"context_relevance_threshold"`
|
||||
RerankingCandidates int `json:"reranking_candidates"`
|
||||
WorkerPort int `json:"worker_port"`
|
||||
DeduplicationThreshold float64 `json:"deduplication_threshold"`
|
||||
RerankingMinImprovement float64 `json:"reranking_min_improvement"`
|
||||
ContextObservations int `json:"context_observations"`
|
||||
ContextMaxPromptResults int `json:"context_max_prompt_results"`
|
||||
ContextSessionCount int `json:"context_session_count"`
|
||||
MaxConns int `json:"max_conns"`
|
||||
RerankingAlpha float64 `json:"reranking_alpha"`
|
||||
GraphMaxHops int `json:"graph_max_hops"`
|
||||
RerankingResults int `json:"reranking_results"`
|
||||
GraphRebuildIntervalMin int `json:"graph_rebuild_interval_min"`
|
||||
HubThreshold int `json:"hub_threshold"`
|
||||
ObservationRetentionDays int `json:"observation_retention_days"`
|
||||
MaintenanceIntervalHours int `json:"maintenance_interval_hours"`
|
||||
ContextMaxTokensStartup int `json:"context_max_tokens_startup"`
|
||||
ContextMaxTokensPrompt int `json:"context_max_tokens_prompt"`
|
||||
ContextShowWorkTokens bool `json:"context_show_work_tokens"`
|
||||
ContextShowReadTokens bool `json:"context_show_read_tokens"`
|
||||
RerankingPureMode bool `json:"reranking_pure_mode"`
|
||||
GraphEnabled bool `json:"graph_enabled"`
|
||||
DeduplicationEnabled bool `json:"deduplication_enabled"`
|
||||
MaintenanceEnabled bool `json:"maintenance_enabled"`
|
||||
RerankingEnabled bool `json:"reranking_enabled"`
|
||||
ContextShowLastSummary bool `json:"context_show_last_summary"`
|
||||
CleanupStaleObservations bool `json:"cleanup_stale_observations"`
|
||||
ContextFullField string `json:"context_full_field"`
|
||||
DBPath string `json:"db_path"`
|
||||
Model string `json:"model"`
|
||||
ClaudeCodePath string `json:"claude_code_path"`
|
||||
EmbeddingModel string `json:"embedding_model"`
|
||||
VectorStorageStrategy string `json:"vector_storage_strategy"`
|
||||
ContextObsConcepts []string `json:"context_obs_concepts"`
|
||||
ContextObsTypes []string `json:"context_obs_types"`
|
||||
ContextFullCount int `json:"context_full_count"`
|
||||
GraphBranchFactor int `json:"graph_branch_factor"`
|
||||
GraphEdgeWeight float64 `json:"graph_edge_weight"`
|
||||
ContextRelevanceThreshold float64 `json:"context_relevance_threshold"`
|
||||
RerankingCandidates int `json:"reranking_candidates"`
|
||||
WorkerPort int `json:"worker_port"`
|
||||
DeduplicationThreshold float64 `json:"deduplication_threshold"`
|
||||
RerankingMinImprovement float64 `json:"reranking_min_improvement"`
|
||||
ContextObservations int `json:"context_observations"`
|
||||
ContextMaxPromptResults int `json:"context_max_prompt_results"`
|
||||
ContextSessionCount int `json:"context_session_count"`
|
||||
MaxConns int `json:"max_conns"`
|
||||
RerankingAlpha float64 `json:"reranking_alpha"`
|
||||
GraphMaxHops int `json:"graph_max_hops"`
|
||||
RerankingResults int `json:"reranking_results"`
|
||||
GraphRebuildIntervalMin int `json:"graph_rebuild_interval_min"`
|
||||
HubThreshold int `json:"hub_threshold"`
|
||||
ObservationRetentionDays int `json:"observation_retention_days"`
|
||||
MaintenanceIntervalHours int `json:"maintenance_interval_hours"`
|
||||
WALCheckpointIntervalSeconds int `json:"wal_checkpoint_interval_seconds"`
|
||||
WALCheckpointThresholdBytes int64 `json:"wal_checkpoint_threshold_bytes"`
|
||||
ContextMaxTokensStartup int `json:"context_max_tokens_startup"`
|
||||
ContextMaxTokensPrompt int `json:"context_max_tokens_prompt"`
|
||||
ContextShowWorkTokens bool `json:"context_show_work_tokens"`
|
||||
ContextShowReadTokens bool `json:"context_show_read_tokens"`
|
||||
RerankingPureMode bool `json:"reranking_pure_mode"`
|
||||
GraphEnabled bool `json:"graph_enabled"`
|
||||
DeduplicationEnabled bool `json:"deduplication_enabled"`
|
||||
MaintenanceEnabled bool `json:"maintenance_enabled"`
|
||||
RerankingEnabled bool `json:"reranking_enabled"`
|
||||
ContextShowLastSummary bool `json:"context_show_last_summary"`
|
||||
CleanupStaleObservations bool `json:"cleanup_stale_observations"`
|
||||
}
|
||||
|
||||
var (
|
||||
@@ -181,6 +183,10 @@ func Default() *Config {
|
||||
MaintenanceIntervalHours: 6, // Run every 6 hours
|
||||
ObservationRetentionDays: 0, // 0 = no age-based deletion (keep all)
|
||||
CleanupStaleObservations: false, // Don't auto-cleanup stale observations
|
||||
// WAL checkpoint loop tunables (issue #49). Defaults mirror the worker constants:
|
||||
// check the WAL every 60s and TRUNCATE-checkpoint once it reaches 4 MiB.
|
||||
WALCheckpointIntervalSeconds: 60,
|
||||
WALCheckpointThresholdBytes: 4 << 20, // 4 MiB
|
||||
}
|
||||
}
|
||||
|
||||
@@ -284,6 +290,13 @@ func Load() (*Config, error) {
|
||||
if v, ok := settings["CLAUDE_MNEMONIC_CONTEXT_MAX_TOKENS_PROMPT"].(float64); ok && v > 0 {
|
||||
cfg.ContextMaxTokensPrompt = int(v)
|
||||
}
|
||||
// WAL checkpoint loop tunables (issue #49)
|
||||
if v, ok := settings["CLAUDE_MNEMONIC_WAL_CHECKPOINT_INTERVAL_SECONDS"].(float64); ok && v > 0 {
|
||||
cfg.WALCheckpointIntervalSeconds = int(v)
|
||||
}
|
||||
if v, ok := settings["CLAUDE_MNEMONIC_WAL_CHECKPOINT_THRESHOLD_BYTES"].(float64); ok && v > 0 {
|
||||
cfg.WALCheckpointThresholdBytes = int64(v)
|
||||
}
|
||||
// Deduplication settings
|
||||
if v, ok := settings["CLAUDE_MNEMONIC_DEDUP_ENABLED"].(bool); ok {
|
||||
cfg.DeduplicationEnabled = v
|
||||
|
||||
+145
-38
@@ -5,18 +5,80 @@ import (
|
||||
"context"
|
||||
"database/sql"
|
||||
"fmt"
|
||||
"os"
|
||||
"slices"
|
||||
"sync"
|
||||
"time"
|
||||
|
||||
sqlite_vec "github.com/asg017/sqlite-vec-go-bindings/cgo"
|
||||
_ "github.com/mattn/go-sqlite3" // Import SQLite driver with FTS5 support
|
||||
sqlite3 "github.com/mattn/go-sqlite3" // SQLite driver with FTS5 support
|
||||
"github.com/rs/zerolog/log"
|
||||
"gorm.io/driver/sqlite"
|
||||
"gorm.io/gorm"
|
||||
"gorm.io/gorm/logger"
|
||||
)
|
||||
|
||||
// driverName is the name of the custom mattn/go-sqlite3 driver registered with a
|
||||
// ConnectHook that applies ALL connection pragmas (correctness + best-effort) to EVERY
|
||||
// pooled connection at open time. The stock "sqlite3" driver has no hook, so pragmas set
|
||||
// via a single post-open Exec only reach one arbitrary pooled connection (issue #49, F6).
|
||||
const driverName = "sqlite3_mnemonic"
|
||||
|
||||
// registerDriverOnce guards driver registration so it runs exactly once per process.
|
||||
// database/sql panics with "sql: Register called twice" on a duplicate name, and NewStore
|
||||
// may be called multiple times (e.g. after a config-change reinitialization).
|
||||
var registerDriverOnce sync.Once
|
||||
|
||||
// correctnessPragmas MUST succeed on every connection: getting any of them wrong changes
|
||||
// transactional/locking semantics, not just performance. A failure here aborts the open.
|
||||
var correctnessPragmas = []string{
|
||||
"PRAGMA foreign_keys=ON",
|
||||
"PRAGMA journal_mode=WAL",
|
||||
"PRAGMA synchronous=NORMAL",
|
||||
"PRAGMA busy_timeout=5000",
|
||||
"PRAGMA cache_size=-64000",
|
||||
}
|
||||
|
||||
// bestEffortPragmas are per-connection or database-wide optimizations. A failure is logged
|
||||
// and tolerated: the connection is still correct, just less tuned. (page_size only takes
|
||||
// effect on an empty database / next VACUUM, but applying it per-connection is harmless.)
|
||||
var bestEffortPragmas = []string{
|
||||
"PRAGMA temp_store=MEMORY", // Store temp tables in memory
|
||||
"PRAGMA mmap_size=268435456", // 256MB memory-mapped I/O
|
||||
"PRAGMA page_size=4096", // 4KB pages (optimal for most systems)
|
||||
"PRAGMA wal_autocheckpoint=1000", // Auto-checkpoint (PASSIVE) every 1000 WAL frames
|
||||
"PRAGMA journal_size_limit=8388608", // Backstop: cap -wal at 8MiB (issue #49)
|
||||
}
|
||||
|
||||
// connectHook applies all pragmas to a freshly opened connection. mattn/go-sqlite3 calls
|
||||
// it at the very end of Open, after DSN params and extensions, so it is authoritative.
|
||||
func connectHook(c *sqlite3.SQLiteConn) error {
|
||||
for _, pragma := range correctnessPragmas {
|
||||
if _, err := c.Exec(pragma, nil); err != nil {
|
||||
return fmt.Errorf("apply correctness pragma %q: %w", pragma, err)
|
||||
}
|
||||
}
|
||||
for _, pragma := range bestEffortPragmas {
|
||||
if _, err := c.Exec(pragma, nil); err != nil {
|
||||
log.Warn().Str("pragma", pragma).Err(err).Msg("Failed to set pragma (non-fatal)")
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// registerDriver registers the custom driver once. sqlite_vec.Auto() registers the vec
|
||||
// extension globally via sqlite3_auto_extension, which applies to connections from any
|
||||
// sqlite3-based driver, so the new driver still gets vec + FTS5.
|
||||
func registerDriver() {
|
||||
registerDriverOnce.Do(func() {
|
||||
sql.Register(driverName, &sqlite3.SQLiteDriver{
|
||||
ConnectHook: func(c *sqlite3.SQLiteConn) error {
|
||||
return connectHook(c)
|
||||
},
|
||||
})
|
||||
})
|
||||
}
|
||||
|
||||
// Store represents the GORM database connection with sqlite-vec support.
|
||||
type Store struct {
|
||||
healthCacheTime time.Time
|
||||
@@ -24,6 +86,7 @@ type Store struct {
|
||||
sqlDB *sql.DB
|
||||
metrics *PoolMetrics
|
||||
cachedHealth *HealthInfo
|
||||
path string
|
||||
healthCacheTTL time.Duration
|
||||
healthCacheMu sync.RWMutex
|
||||
}
|
||||
@@ -36,22 +99,32 @@ type Config struct {
|
||||
}
|
||||
|
||||
// NewStore creates a new Store with WAL mode enabled and sqlite-vec registered.
|
||||
// CRITICAL: WAL mode and foreign keys are enabled via pragmas for concurrent reads.
|
||||
// CRITICAL: all connection pragmas (WAL, foreign_keys, busy_timeout, etc.) are applied to
|
||||
// EVERY pooled connection via a driver ConnectHook (see registerDriver), so the pool is
|
||||
// uniformly configured and connections may be recycled safely (issue #49, F6).
|
||||
func NewStore(cfg Config) (*Store, error) {
|
||||
// 1. Register sqlite-vec extension (must be done before opening database)
|
||||
// 1. Register sqlite-vec extension (must be done before opening database).
|
||||
// sqlite_vec.Auto() uses sqlite3_auto_extension, which is global to all sqlite3-based
|
||||
// drivers, so connections from our custom driver also get the vec virtual table.
|
||||
sqlite_vec.Auto()
|
||||
|
||||
// 2. Build connection string (foreign keys enabled in DSN)
|
||||
// Use sqlite3 driver (mattn/go-sqlite3) which has FTS5 support
|
||||
// 2. Register the custom driver whose ConnectHook applies ALL pragmas to EVERY pooled
|
||||
// connection (issue #49, F6). Without this, pragmas set via a single post-open
|
||||
// sqlDB.Exec reach only one arbitrary pooled connection. The hook is authoritative.
|
||||
registerDriver()
|
||||
|
||||
// 3. Build a minimal DSN. _foreign_keys is kept as belt-and-suspenders (the hook sets
|
||||
// it too); all other pragmas are applied per-connection by the ConnectHook, so they no
|
||||
// longer need to live in the DSN.
|
||||
dsn := cfg.Path + "?_foreign_keys=ON"
|
||||
|
||||
// 3. Open raw database connection with mattn/go-sqlite3 (has FTS5 support)
|
||||
sqlDB, err := sql.Open("sqlite3", dsn)
|
||||
// 4. Open raw database connection with the custom driver (FTS5 + per-connection pragmas).
|
||||
sqlDB, err := sql.Open(driverName, dsn)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("open database: %w", err)
|
||||
}
|
||||
|
||||
// 4. Wrap with GORM using existing connection
|
||||
// 5. Wrap with GORM using existing connection
|
||||
db, err := gorm.Open(sqlite.Dialector{
|
||||
Conn: sqlDB,
|
||||
}, &gorm.Config{
|
||||
@@ -66,16 +139,25 @@ func NewStore(cfg Config) (*Store, error) {
|
||||
return nil, fmt.Errorf("open gorm: %w", err)
|
||||
}
|
||||
|
||||
// 5. Configure connection pool (same settings as current implementation)
|
||||
// 6. Configure connection pool.
|
||||
maxConns := cfg.MaxConns
|
||||
if maxConns <= 0 {
|
||||
maxConns = 4
|
||||
}
|
||||
sqlDB.SetMaxOpenConns(maxConns)
|
||||
sqlDB.SetMaxIdleConns(maxConns)
|
||||
sqlDB.SetConnMaxLifetime(0) // Never expire (SQLite connections are cheap)
|
||||
// Finite recycling (issue #49): previously SetConnMaxLifetime(0) meant connections
|
||||
// NEVER recycled, so a long-lived read connection could pin an old WAL read-mark for the
|
||||
// whole process lifetime and block TRUNCATE checkpoints from reclaiming the -wal file.
|
||||
// Recycling is safe now because the ConnectHook reapplies every correctness pragma on
|
||||
// each new connection — a recycled connection comes back fully configured, not with
|
||||
// defaults. 1h lifetime bounds read-mark staleness without churning the pool; 30m idle
|
||||
// time reclaims connections that sit unused (e.g. between sessions) so the pool shrinks
|
||||
// back to one warm connection during quiet periods, dropping their WAL read-marks.
|
||||
sqlDB.SetConnMaxLifetime(1 * time.Hour)
|
||||
sqlDB.SetConnMaxIdleTime(30 * time.Minute)
|
||||
|
||||
// 6. Verify connection
|
||||
// 7. Verify connection
|
||||
if err := sqlDB.Ping(); err != nil {
|
||||
return nil, fmt.Errorf("ping database: %w", err)
|
||||
}
|
||||
@@ -83,37 +165,18 @@ func NewStore(cfg Config) (*Store, error) {
|
||||
store := &Store{
|
||||
DB: db,
|
||||
sqlDB: sqlDB,
|
||||
path: cfg.Path,
|
||||
metrics: NewPoolMetrics(100), // Track last 100 latency samples
|
||||
healthCacheTTL: 5 * time.Second, // Cache health checks for 5 seconds
|
||||
}
|
||||
|
||||
// 7. Run migrations FIRST (before PRAGMA commands)
|
||||
// 8. Run migrations. All pragmas (correctness + best-effort) are applied per-connection
|
||||
// by the ConnectHook at open time, so there is no post-open pragma loop here anymore:
|
||||
// such a loop only ever reached one arbitrary pooled connection (issue #49, F6).
|
||||
if err := runMigrations(db, sqlDB); err != nil {
|
||||
return nil, fmt.Errorf("run migrations: %w", err)
|
||||
}
|
||||
|
||||
// 8. CRITICAL: Set WAL mode and other performance pragmas
|
||||
// Use raw sqlDB to avoid GORM transaction issues
|
||||
pragmas := []string{
|
||||
"PRAGMA journal_mode=WAL",
|
||||
"PRAGMA synchronous=NORMAL",
|
||||
"PRAGMA cache_size=-64000", // 64MB cache (negative = KB)
|
||||
"PRAGMA temp_store=MEMORY", // Store temp tables in memory
|
||||
"PRAGMA mmap_size=268435456", // 256MB memory-mapped I/O
|
||||
"PRAGMA page_size=4096", // 4KB pages (optimal for most systems)
|
||||
"PRAGMA wal_autocheckpoint=1000", // Explicit default; checkpoint every 1000 WAL frames
|
||||
}
|
||||
for _, pragma := range pragmas {
|
||||
if _, err := sqlDB.Exec(pragma); err != nil {
|
||||
log.Warn().Str("pragma", pragma).Err(err).Msg("Failed to set pragma (non-fatal)")
|
||||
}
|
||||
}
|
||||
// Set busy timeout to 5 seconds to handle concurrent writes
|
||||
// This allows SQLite to retry when database is locked instead of failing immediately
|
||||
if _, err := sqlDB.Exec("PRAGMA busy_timeout=5000"); err != nil {
|
||||
return nil, fmt.Errorf("set busy timeout: %w", err)
|
||||
}
|
||||
|
||||
// 9. Warm the connection pool
|
||||
store.WarmPool(maxConns)
|
||||
|
||||
@@ -148,11 +211,55 @@ func (s *Store) WarmPool(numConns int) {
|
||||
log.Debug().Int("connections", numConns).Msg("Connection pool warmed")
|
||||
}
|
||||
|
||||
// Close closes the database connection.
|
||||
// Close checkpoints the WAL (TRUNCATE) before closing the connection. Checkpointing on
|
||||
// shutdown prevents the WAL file from persisting in a large, dirty state across restarts
|
||||
// and config-change reinitializations, which otherwise leaves a multi-megabyte -wal file
|
||||
// on disk (issue #49). The checkpoint is best-effort: a failure is logged, not fatal.
|
||||
func (s *Store) Close() error {
|
||||
ctx, cancel := context.WithTimeout(context.Background(), 10*time.Second)
|
||||
defer cancel()
|
||||
if err := s.Checkpoint(ctx); err != nil {
|
||||
log.Warn().Err(err).Msg("WAL checkpoint on close failed (non-fatal)")
|
||||
}
|
||||
return s.sqlDB.Close()
|
||||
}
|
||||
|
||||
// Checkpoint runs a TRUNCATE WAL checkpoint: it flushes WAL frames into the main
|
||||
// database file and shrinks the -wal file back to zero. Unlike a PASSIVE checkpoint
|
||||
// (which never truncates the file and is all SQLite's auto-checkpoint ever performs), a
|
||||
// TRUNCATE checkpoint reclaims disk and is the mechanism that bounds WAL growth.
|
||||
// It waits up to the connection busy_timeout for the write lock and returns an error
|
||||
// rather than blocking indefinitely.
|
||||
func (s *Store) Checkpoint(ctx context.Context) error {
|
||||
if _, err := s.sqlDB.ExecContext(ctx, "PRAGMA wal_checkpoint(TRUNCATE)"); err != nil {
|
||||
return fmt.Errorf("wal checkpoint (truncate): %w", err)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// WALSize returns the size in bytes of the SQLite WAL sidecar file (<db>-wal), or 0 if
|
||||
// it does not exist or cannot be stat'd. Used to decide when a checkpoint is worthwhile.
|
||||
func (s *Store) WALSize() int64 {
|
||||
if s.path == "" {
|
||||
return 0
|
||||
}
|
||||
info, err := os.Stat(s.path + "-wal")
|
||||
if err != nil {
|
||||
return 0
|
||||
}
|
||||
return info.Size()
|
||||
}
|
||||
|
||||
// CheckpointIfLarge performs a TRUNCATE checkpoint only when the WAL file has grown to or
|
||||
// beyond threshold bytes. Returns true if a checkpoint was performed. This keeps the
|
||||
// periodic checkpoint cheap: it does no work while the WAL is small.
|
||||
func (s *Store) CheckpointIfLarge(ctx context.Context, threshold int64) (bool, error) {
|
||||
if s.WALSize() < threshold {
|
||||
return false, nil
|
||||
}
|
||||
return true, s.Checkpoint(ctx)
|
||||
}
|
||||
|
||||
// Ping verifies the database connection is alive.
|
||||
func (s *Store) Ping() error {
|
||||
return s.sqlDB.Ping()
|
||||
@@ -193,8 +300,9 @@ func (s *Store) Optimize(ctx context.Context) error {
|
||||
log.Warn().Err(err).Msg("PRAGMA optimize failed (non-fatal)")
|
||||
}
|
||||
|
||||
// Passive WAL checkpoint — doesn't block readers/writers
|
||||
if _, err := s.sqlDB.ExecContext(ctx, "PRAGMA wal_checkpoint(PASSIVE)"); err != nil {
|
||||
// TRUNCATE WAL checkpoint — reclaims the -wal file during low-activity optimization.
|
||||
// (PASSIVE never shrinks the file, so it cannot bound WAL growth — see issue #49.)
|
||||
if err := s.Checkpoint(ctx); err != nil {
|
||||
log.Warn().Err(err).Msg("WAL checkpoint failed (non-fatal)")
|
||||
}
|
||||
|
||||
@@ -519,7 +627,6 @@ func (s *Store) ExecWithTimeout(ctx context.Context, timeout time.Duration, quer
|
||||
return nil
|
||||
}
|
||||
|
||||
|
||||
// TransactionWithTimeout wraps a transaction function with timeout handling.
|
||||
// The transaction is automatically rolled back if the context times out.
|
||||
func (s *Store) TransactionWithTimeout(ctx context.Context, timeout time.Duration, fn func(*gorm.DB) error) error {
|
||||
|
||||
@@ -5,9 +5,12 @@ package gorm
|
||||
|
||||
import (
|
||||
"context"
|
||||
"database/sql"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"strings"
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"gorm.io/gorm/logger"
|
||||
)
|
||||
@@ -239,3 +242,321 @@ func TestOptimize_RespectsContextCancellation(t *testing.T) {
|
||||
t.Error("expected error with cancelled context, got nil")
|
||||
}
|
||||
}
|
||||
|
||||
// growWAL inserts sizeable rows to push the SQLite WAL well past a few hundred KB so
|
||||
// checkpoint behaviour can be observed. Returns the WAL file size after the inserts.
|
||||
func growWAL(t *testing.T, store *Store, rows int) int64 {
|
||||
t.Helper()
|
||||
bigTitle := strings.Repeat("x", 2048)
|
||||
for i := 0; i < rows; i++ {
|
||||
_, err := store.GetRawDB().Exec(
|
||||
"INSERT INTO observations (sdk_session_id, title, scope, project, type, created_at, created_at_epoch) "+
|
||||
"VALUES (?, ?, 'project', '/tmp/test', 'decision', '2026-01-01T00:00:00Z', 1735689600)",
|
||||
"sess", bigTitle)
|
||||
if err != nil {
|
||||
t.Fatalf("insert row %d: %v", i, err)
|
||||
}
|
||||
}
|
||||
return store.WALSize()
|
||||
}
|
||||
|
||||
func countObservations(t *testing.T, store *Store) int64 {
|
||||
t.Helper()
|
||||
var n int64
|
||||
if err := store.GetRawDB().QueryRow("SELECT COUNT(*) FROM observations").Scan(&n); err != nil {
|
||||
t.Fatalf("count observations: %v", err)
|
||||
}
|
||||
return n
|
||||
}
|
||||
|
||||
// TestCheckpoint_TruncateShrinksWAL verifies Checkpoint() performs a TRUNCATE checkpoint
|
||||
// that actually reclaims the -wal file. This is the load-bearing fix for issue #49: a
|
||||
// PASSIVE checkpoint drains frames but never shrinks the file, so reverting Checkpoint to
|
||||
// PASSIVE would leave the WAL grown and fail this test.
|
||||
func TestCheckpoint_TruncateShrinksWAL(t *testing.T) {
|
||||
tmpDir, err := os.MkdirTemp("", "gorm_checkpoint_truncate_*")
|
||||
if err != nil {
|
||||
t.Fatalf("create temp dir: %v", err)
|
||||
}
|
||||
defer os.RemoveAll(tmpDir)
|
||||
|
||||
dbPath := filepath.Join(tmpDir, "test.db")
|
||||
store, err := NewStore(Config{Path: dbPath, MaxConns: 2, LogLevel: logger.Silent})
|
||||
if err != nil {
|
||||
t.Fatalf("NewStore failed: %v", err)
|
||||
}
|
||||
defer store.Close()
|
||||
|
||||
walBefore := growWAL(t, store, 1000)
|
||||
if walBefore < 64*1024 {
|
||||
t.Fatalf("expected WAL to grow above 64KiB, got %d bytes", walBefore)
|
||||
}
|
||||
|
||||
if err := store.Checkpoint(context.Background()); err != nil {
|
||||
t.Fatalf("Checkpoint failed: %v", err)
|
||||
}
|
||||
|
||||
walAfter := store.WALSize()
|
||||
if walAfter >= walBefore {
|
||||
t.Errorf("expected WAL to shrink after TRUNCATE checkpoint: before=%d after=%d", walBefore, walAfter)
|
||||
}
|
||||
if walAfter > 64*1024 {
|
||||
t.Errorf("expected WAL truncated to near-zero, got %d bytes", walAfter)
|
||||
}
|
||||
}
|
||||
|
||||
// TestCheckpointIfLarge_GatesOnThreshold verifies the size-gated periodic checkpoint used
|
||||
// by the worker's walCheckpointLoop: a no-op below the threshold, a truncating checkpoint
|
||||
// at/above it.
|
||||
func TestCheckpointIfLarge_GatesOnThreshold(t *testing.T) {
|
||||
tmpDir, err := os.MkdirTemp("", "gorm_checkpoint_gated_*")
|
||||
if err != nil {
|
||||
t.Fatalf("create temp dir: %v", err)
|
||||
}
|
||||
defer os.RemoveAll(tmpDir)
|
||||
|
||||
dbPath := filepath.Join(tmpDir, "test.db")
|
||||
store, err := NewStore(Config{Path: dbPath, MaxConns: 2, LogLevel: logger.Silent})
|
||||
if err != nil {
|
||||
t.Fatalf("NewStore failed: %v", err)
|
||||
}
|
||||
defer store.Close()
|
||||
|
||||
// Below an enormous threshold -> no checkpoint.
|
||||
done, err := store.CheckpointIfLarge(context.Background(), 1<<30) // 1 GiB
|
||||
if err != nil {
|
||||
t.Fatalf("CheckpointIfLarge (small) failed: %v", err)
|
||||
}
|
||||
if done {
|
||||
t.Errorf("expected no checkpoint below threshold, but one was performed")
|
||||
}
|
||||
|
||||
// Grow the WAL, then a low threshold triggers a truncating checkpoint.
|
||||
walBefore := growWAL(t, store, 1000)
|
||||
if walBefore < 64*1024 {
|
||||
t.Fatalf("expected WAL to grow above 64KiB, got %d bytes", walBefore)
|
||||
}
|
||||
|
||||
done, err = store.CheckpointIfLarge(context.Background(), 64*1024)
|
||||
if err != nil {
|
||||
t.Fatalf("CheckpointIfLarge (large) failed: %v", err)
|
||||
}
|
||||
if !done {
|
||||
t.Errorf("expected checkpoint above threshold, but none was performed")
|
||||
}
|
||||
if walAfter := store.WALSize(); walAfter >= walBefore {
|
||||
t.Errorf("expected WAL to shrink after gated checkpoint: before=%d after=%d", walBefore, walAfter)
|
||||
}
|
||||
}
|
||||
|
||||
// TestClose_CheckpointsWAL verifies Close() reclaims the WAL and leaves the data intact on
|
||||
// the next open (issue #49: shutdown must not leave a large dirty WAL on disk).
|
||||
func TestClose_CheckpointsWAL(t *testing.T) {
|
||||
tmpDir, err := os.MkdirTemp("", "gorm_close_checkpoint_*")
|
||||
if err != nil {
|
||||
t.Fatalf("create temp dir: %v", err)
|
||||
}
|
||||
defer os.RemoveAll(tmpDir)
|
||||
|
||||
dbPath := filepath.Join(tmpDir, "test.db")
|
||||
store, err := NewStore(Config{Path: dbPath, MaxConns: 2, LogLevel: logger.Silent})
|
||||
if err != nil {
|
||||
t.Fatalf("NewStore failed: %v", err)
|
||||
}
|
||||
|
||||
if walBefore := growWAL(t, store, 800); walBefore == 0 {
|
||||
t.Fatalf("expected WAL to grow before close, got 0")
|
||||
}
|
||||
count := countObservations(t, store)
|
||||
if count < 800 {
|
||||
t.Fatalf("expected >=800 observations before close, got %d", count)
|
||||
}
|
||||
|
||||
if err := store.Close(); err != nil {
|
||||
t.Fatalf("Close failed: %v", err)
|
||||
}
|
||||
|
||||
// The -wal file must not persist large on disk after a clean shutdown.
|
||||
if info, statErr := os.Stat(dbPath + "-wal"); statErr == nil && info.Size() > 64*1024 {
|
||||
t.Errorf("expected WAL reclaimed on close, -wal still %d bytes", info.Size())
|
||||
}
|
||||
|
||||
// Reopen and verify data survived the checkpoint.
|
||||
store2, err := NewStore(Config{Path: dbPath, MaxConns: 2, LogLevel: logger.Silent})
|
||||
if err != nil {
|
||||
t.Fatalf("reopen NewStore failed: %v", err)
|
||||
}
|
||||
defer store2.Close()
|
||||
|
||||
if count2 := countObservations(t, store2); count2 != count {
|
||||
t.Errorf("expected %d observations after reopen, got %d", count, count2)
|
||||
}
|
||||
}
|
||||
|
||||
// TestBusyTimeoutAppliedToAllConnections verifies the issue #49 DSN fix: busy_timeout is
|
||||
// applied to EVERY pooled connection (not just one arbitrary connection as happened when
|
||||
// it was set via a single post-open sqlDB.Exec).
|
||||
func TestBusyTimeoutAppliedToAllConnections(t *testing.T) {
|
||||
tmpDir, err := os.MkdirTemp("", "gorm_busy_timeout_*")
|
||||
if err != nil {
|
||||
t.Fatalf("create temp dir: %v", err)
|
||||
}
|
||||
defer os.RemoveAll(tmpDir)
|
||||
|
||||
dbPath := filepath.Join(tmpDir, "test.db")
|
||||
const maxConns = 4
|
||||
store, err := NewStore(Config{Path: dbPath, MaxConns: maxConns, LogLevel: logger.Silent})
|
||||
if err != nil {
|
||||
t.Fatalf("NewStore failed: %v", err)
|
||||
}
|
||||
defer store.Close()
|
||||
|
||||
// Pin all connections concurrently so each distinct connection is inspected, then
|
||||
// assert every one reports busy_timeout=5000.
|
||||
raw := store.GetRawDB()
|
||||
conns := make([]*sql.Conn, 0, maxConns)
|
||||
defer func() {
|
||||
for _, c := range conns {
|
||||
_ = c.Close()
|
||||
}
|
||||
}()
|
||||
for i := 0; i < maxConns; i++ {
|
||||
c, err := raw.Conn(context.Background())
|
||||
if err != nil {
|
||||
t.Fatalf("acquire conn %d: %v", i, err)
|
||||
}
|
||||
conns = append(conns, c)
|
||||
}
|
||||
for i, c := range conns {
|
||||
var timeout int
|
||||
if err := c.QueryRowContext(context.Background(), "PRAGMA busy_timeout").Scan(&timeout); err != nil {
|
||||
t.Fatalf("query busy_timeout on conn %d: %v", i, err)
|
||||
}
|
||||
if timeout != 5000 {
|
||||
t.Errorf("conn %d: expected busy_timeout=5000, got %d", i, timeout)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// TestAllPragmasAppliedToAllConnections verifies the issue #49 (F6) ConnectHook fix: not
|
||||
// just busy_timeout but the full pragma set — including the best-effort pragmas that used
|
||||
// to be set via a single post-open sqlDB.Exec (journal_size_limit, temp_store,
|
||||
// wal_autocheckpoint) — is applied to EVERY pooled connection. It pins all connections so
|
||||
// each distinct connection is inspected, then asserts each reports the expected value.
|
||||
func TestAllPragmasAppliedToAllConnections(t *testing.T) {
|
||||
tmpDir, err := os.MkdirTemp("", "gorm_all_pragmas_*")
|
||||
if err != nil {
|
||||
t.Fatalf("create temp dir: %v", err)
|
||||
}
|
||||
defer os.RemoveAll(tmpDir)
|
||||
|
||||
dbPath := filepath.Join(tmpDir, "test.db")
|
||||
const maxConns = 4
|
||||
store, err := NewStore(Config{Path: dbPath, MaxConns: maxConns, LogLevel: logger.Silent})
|
||||
if err != nil {
|
||||
t.Fatalf("NewStore failed: %v", err)
|
||||
}
|
||||
defer store.Close()
|
||||
|
||||
// Expected per-connection pragma values. temp_store=MEMORY reports as 2; the others are
|
||||
// numeric. foreign_keys/journal_mode/synchronous are covered by TestNewStore and the
|
||||
// busy_timeout test; here we focus on the previously single-connection pragmas.
|
||||
checks := []struct {
|
||||
name string
|
||||
want int64
|
||||
}{
|
||||
{"busy_timeout", 5000},
|
||||
{"journal_size_limit", 8388608},
|
||||
{"temp_store", 2}, // 2 == MEMORY
|
||||
{"wal_autocheckpoint", 1000},
|
||||
{"foreign_keys", 1},
|
||||
}
|
||||
|
||||
raw := store.GetRawDB()
|
||||
conns := make([]*sql.Conn, 0, maxConns)
|
||||
defer func() {
|
||||
for _, c := range conns {
|
||||
_ = c.Close()
|
||||
}
|
||||
}()
|
||||
for i := 0; i < maxConns; i++ {
|
||||
c, err := raw.Conn(context.Background())
|
||||
if err != nil {
|
||||
t.Fatalf("acquire conn %d: %v", i, err)
|
||||
}
|
||||
conns = append(conns, c)
|
||||
}
|
||||
|
||||
for i, c := range conns {
|
||||
for _, chk := range checks {
|
||||
var got int64
|
||||
query := "PRAGMA " + chk.name
|
||||
if err := c.QueryRowContext(context.Background(), query).Scan(&got); err != nil {
|
||||
t.Fatalf("conn %d: query %q: %v", i, chk.name, err)
|
||||
}
|
||||
if got != chk.want {
|
||||
t.Errorf("conn %d: %s = %d, want %d", i, chk.name, got, chk.want)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// TestRecycledConnectionRetainsPragmas verifies that recycling a connection (which now
|
||||
// happens because SetConnMaxLifetime is finite, not 0) does NOT drop the correctness
|
||||
// pragmas: the ConnectHook reapplies them on every new connection. We force recycling by
|
||||
// setting a near-zero max lifetime so the next acquisition opens a fresh connection, then
|
||||
// assert the new connection still reports the safe values rather than SQLite defaults
|
||||
// (busy_timeout would default to 0 and journal_mode to "delete" without the hook).
|
||||
func TestRecycledConnectionRetainsPragmas(t *testing.T) {
|
||||
tmpDir, err := os.MkdirTemp("", "gorm_recycle_pragmas_*")
|
||||
if err != nil {
|
||||
t.Fatalf("create temp dir: %v", err)
|
||||
}
|
||||
defer os.RemoveAll(tmpDir)
|
||||
|
||||
dbPath := filepath.Join(tmpDir, "test.db")
|
||||
store, err := NewStore(Config{Path: dbPath, MaxConns: 2, LogLevel: logger.Silent})
|
||||
if err != nil {
|
||||
t.Fatalf("NewStore failed: %v", err)
|
||||
}
|
||||
defer store.Close()
|
||||
|
||||
raw := store.GetRawDB()
|
||||
|
||||
// Force aggressive recycling: any connection older than 1ns is expired on next use, so
|
||||
// database/sql opens a brand-new connection (running the ConnectHook again).
|
||||
raw.SetConnMaxLifetime(time.Nanosecond)
|
||||
time.Sleep(5 * time.Millisecond)
|
||||
|
||||
// Acquire a connection that is necessarily freshly opened (all prior ones are expired),
|
||||
// and verify the hook reapplied the correctness pragmas.
|
||||
conn, err := raw.Conn(context.Background())
|
||||
if err != nil {
|
||||
t.Fatalf("acquire recycled conn: %v", err)
|
||||
}
|
||||
defer conn.Close()
|
||||
|
||||
var busyTimeout int
|
||||
if err := conn.QueryRowContext(context.Background(), "PRAGMA busy_timeout").Scan(&busyTimeout); err != nil {
|
||||
t.Fatalf("query busy_timeout: %v", err)
|
||||
}
|
||||
if busyTimeout != 5000 {
|
||||
t.Errorf("recycled conn: busy_timeout = %d, want 5000 (hook did not reapply)", busyTimeout)
|
||||
}
|
||||
|
||||
var journalMode string
|
||||
if err := conn.QueryRowContext(context.Background(), "PRAGMA journal_mode").Scan(&journalMode); err != nil {
|
||||
t.Fatalf("query journal_mode: %v", err)
|
||||
}
|
||||
if journalMode != "wal" {
|
||||
t.Errorf("recycled conn: journal_mode = %q, want \"wal\" (hook did not reapply)", journalMode)
|
||||
}
|
||||
|
||||
var foreignKeys int
|
||||
if err := conn.QueryRowContext(context.Background(), "PRAGMA foreign_keys").Scan(&foreignKeys); err != nil {
|
||||
t.Fatalf("query foreign_keys: %v", err)
|
||||
}
|
||||
if foreignKeys != 1 {
|
||||
t.Errorf("recycled conn: foreign_keys = %d, want 1 (hook did not reapply)", foreignKeys)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -83,8 +83,20 @@ func (s *Service) Start(ctx context.Context) {
|
||||
Bool("cleanup_stale", s.config.CleanupStaleObservations).
|
||||
Msg("Starting maintenance scheduler")
|
||||
|
||||
// Initial run after 5 minutes (allow system to stabilize)
|
||||
time.Sleep(5 * time.Minute)
|
||||
// Initial run after 5 minutes (allow system to stabilize).
|
||||
// Use a cancellable timer so shutdown (ctx cancel / Stop) is not blocked for up to 5m.
|
||||
initialDelay := time.NewTimer(5 * time.Minute)
|
||||
select {
|
||||
case <-ctx.Done():
|
||||
initialDelay.Stop()
|
||||
s.log.Info().Msg("Maintenance shutting down before initial run (context cancellation)")
|
||||
return
|
||||
case <-s.stopCh:
|
||||
initialDelay.Stop()
|
||||
s.log.Info().Msg("Maintenance shutting down before initial run (stop signal)")
|
||||
return
|
||||
case <-initialDelay.C:
|
||||
}
|
||||
s.runMaintenance(ctx)
|
||||
|
||||
ticker := time.NewTicker(interval)
|
||||
@@ -288,7 +300,13 @@ func (s *Service) Stats() map[string]any {
|
||||
}
|
||||
}
|
||||
|
||||
// RunNow triggers an immediate maintenance run.
|
||||
// RunNow triggers an immediate maintenance run in the background.
|
||||
func (s *Service) RunNow(ctx context.Context) {
|
||||
go s.runMaintenance(ctx)
|
||||
}
|
||||
|
||||
// RunNowSync triggers an immediate maintenance run and blocks until it completes.
|
||||
// Use this when the caller needs to report a synchronous result (e.g. an HTTP handler).
|
||||
func (s *Service) RunNowSync(ctx context.Context) {
|
||||
s.runMaintenance(ctx)
|
||||
}
|
||||
|
||||
@@ -534,7 +534,7 @@ func (s *Server) handleToolsList(req *Request) *Response {
|
||||
},
|
||||
{
|
||||
Name: "trigger_maintenance",
|
||||
Description: "Trigger an immediate maintenance run (cleanup old observations, optimize database).",
|
||||
Description: "Trigger an immediate database maintenance run: optimize/checkpoint the database, clean up old prompts, apply any configured observation retention/stale cleanup, and recalculate importance scores.",
|
||||
InputSchema: map[string]any{
|
||||
"type": "object",
|
||||
"properties": map[string]any{},
|
||||
@@ -917,7 +917,7 @@ func (s *Server) callTool(ctx context.Context, name string, args json.RawMessage
|
||||
"project": s.project,
|
||||
})
|
||||
case "trigger_maintenance":
|
||||
return s.proxyPostRaw(ctx, "/api/scoring/recalculate", nil)
|
||||
return s.proxyPostRaw(ctx, "/api/maintenance/run", nil)
|
||||
case "analyze_observation_importance":
|
||||
return s.handleAnalyzeImportanceProxy(ctx, args)
|
||||
case "analyze_search_patterns":
|
||||
|
||||
@@ -314,6 +314,49 @@ func (s *Service) handleTriggerRecalculation(w http.ResponseWriter, r *http.Requ
|
||||
writeJSON(w, map[string]string{"status": "recalculation triggered"})
|
||||
}
|
||||
|
||||
// handleRunMaintenance triggers an immediate, synchronous database maintenance run
|
||||
// (Optimize/TRUNCATE checkpoint + prompt cleanup + any enabled retention/stale cleanup)
|
||||
// and also kicks off an importance-score recalculation in the background so the behavior
|
||||
// of the previous trigger_maintenance tool is preserved (issue #49).
|
||||
func (s *Service) handleRunMaintenance(w http.ResponseWriter, r *http.Request) {
|
||||
// initMu.RLock held by requireReady middleware
|
||||
maintSvc := s.maintenanceSvc
|
||||
recalculator := s.recalculator
|
||||
|
||||
if maintSvc == nil {
|
||||
http.Error(w, "maintenance service not available", http.StatusServiceUnavailable)
|
||||
return
|
||||
}
|
||||
|
||||
// Run maintenance synchronously with an independent, bounded context so the caller
|
||||
// receives a real completion status. Use context.Background so an HTTP client timeout
|
||||
// does not abort an in-progress DB maintenance pass.
|
||||
mctx, cancel := context.WithTimeout(context.Background(), 60*time.Second)
|
||||
defer cancel()
|
||||
maintSvc.RunNowSync(mctx)
|
||||
|
||||
// Preserve prior trigger_maintenance behavior: also recalculate importance scores.
|
||||
recalcTriggered := false
|
||||
if recalculator != nil {
|
||||
recalcTriggered = true
|
||||
s.wg.Add(1)
|
||||
go func() {
|
||||
defer s.wg.Done()
|
||||
ctx, cancel := context.WithTimeout(context.Background(), 60*time.Second)
|
||||
defer cancel()
|
||||
if err := recalculator.RecalculateNow(ctx); err != nil {
|
||||
log.Error().Err(err).Msg("Background recalculation during maintenance failed")
|
||||
}
|
||||
}()
|
||||
}
|
||||
|
||||
writeJSON(w, map[string]any{
|
||||
"status": "maintenance completed",
|
||||
"recalc_triggered": recalcTriggered,
|
||||
"maintenance_stats": maintSvc.Stats(),
|
||||
})
|
||||
}
|
||||
|
||||
// parseIntParam parses an integer query parameter with a default value.
|
||||
func parseIntParam(r *http.Request, name string, defaultVal int) int {
|
||||
if val := r.URL.Query().Get(name); val != "" {
|
||||
|
||||
@@ -16,6 +16,7 @@ import (
|
||||
"github.com/lukaszraczylo/claude-mnemonic/internal/config"
|
||||
"github.com/lukaszraczylo/claude-mnemonic/internal/db/gorm"
|
||||
"github.com/lukaszraczylo/claude-mnemonic/internal/embedding"
|
||||
"github.com/lukaszraczylo/claude-mnemonic/internal/maintenance"
|
||||
"github.com/lukaszraczylo/claude-mnemonic/internal/pattern"
|
||||
"github.com/lukaszraczylo/claude-mnemonic/internal/reranking"
|
||||
"github.com/lukaszraczylo/claude-mnemonic/internal/scoring"
|
||||
@@ -44,6 +45,14 @@ const (
|
||||
// QueueProcessInterval is how often the background queue processor runs.
|
||||
QueueProcessInterval = 2 * time.Second
|
||||
|
||||
// WALCheckpointInterval is how often the worker checks whether the SQLite WAL needs a
|
||||
// TRUNCATE checkpoint to reclaim disk and prevent unbounded growth (issue #49).
|
||||
WALCheckpointInterval = 60 * time.Second
|
||||
|
||||
// WALCheckpointThreshold is the WAL file size at or above which the periodic check
|
||||
// performs a TRUNCATE checkpoint. Keeps the steady-state WAL bounded to a few MB.
|
||||
WALCheckpointThreshold = 4 << 20 // 4 MiB
|
||||
|
||||
// reinitializationDrainDelay is the delay after marking the service as not ready
|
||||
// to allow in-flight requests to complete before reinitializing.
|
||||
reinitializationDrainDelay = 200 * time.Millisecond
|
||||
@@ -121,6 +130,7 @@ type Service struct {
|
||||
patternStore *gorm.PatternStore
|
||||
relationStore *gorm.RelationStore
|
||||
patternDetector *pattern.Detector
|
||||
maintenanceSvc *maintenance.Service
|
||||
sessionManager *session.Manager
|
||||
sseBroadcaster *sse.Broadcaster
|
||||
processor *sdk.Processor
|
||||
@@ -570,6 +580,34 @@ func (s *Service) initializeAsync() {
|
||||
go s.processQueue()
|
||||
}
|
||||
|
||||
// Start periodic WAL checkpoint loop to bound SQLite WAL file growth (issue #49).
|
||||
s.wg.Add(1)
|
||||
go s.walCheckpointLoop()
|
||||
|
||||
// Start the scheduled maintenance service (issue #49: was dead code, never instantiated).
|
||||
// vectorCleanupFn mirrors the observation store's cleanup hook so age/stale deletions done
|
||||
// directly via GORM still remove their vectors from sqlite-vec.
|
||||
var vectorCleanupFn func(ctx context.Context, deletedIDs []int64)
|
||||
if vectorSync != nil {
|
||||
vectorCleanupFn = func(ctx context.Context, deletedIDs []int64) {
|
||||
if err := retryWithBackoff(ctx, VectorSyncMaxRetries, VectorSyncInitialBackoff, func() error {
|
||||
return vectorSync.DeleteObservations(ctx, deletedIDs)
|
||||
}); err != nil {
|
||||
log.Warn().Err(err).Ints64("ids", deletedIDs).Msg("Failed to delete observations from sqlite-vec during maintenance")
|
||||
}
|
||||
}
|
||||
}
|
||||
maintSvc := maintenance.NewService(store, observationStore, summaryStore, promptStore, vectorCleanupFn, s.config, log.Logger)
|
||||
s.initMu.Lock()
|
||||
s.maintenanceSvc = maintSvc
|
||||
s.initMu.Unlock()
|
||||
s.wg.Add(1)
|
||||
go func() {
|
||||
defer s.wg.Done()
|
||||
maintSvc.Start(s.ctx)
|
||||
}()
|
||||
log.Info().Msg("Maintenance scheduler started")
|
||||
|
||||
// Start file watchers for auto-recreation on deletion
|
||||
s.startWatchers()
|
||||
|
||||
@@ -1290,6 +1328,9 @@ func (s *Service) setupRoutes() {
|
||||
r.Put("/api/scoring/concepts/{concept}", s.handleUpdateConceptWeight)
|
||||
r.Post("/api/scoring/recalculate", s.handleTriggerRecalculation)
|
||||
|
||||
// Maintenance: run an immediate synchronous DB maintenance pass (issue #49)
|
||||
r.Post("/api/maintenance/run", s.handleRunMaintenance)
|
||||
|
||||
// Context injection
|
||||
r.Get("/api/context/count", s.handleContextCount)
|
||||
r.Get("/api/context/inject", s.handleContextInject)
|
||||
@@ -1621,6 +1662,52 @@ func (s *Service) processQueue() {
|
||||
}
|
||||
}
|
||||
|
||||
// walCheckpointLoop periodically checkpoints the SQLite WAL so it cannot grow unbounded
|
||||
// during long-lived sessions. SQLite's internal auto-checkpoint is PASSIVE and never
|
||||
// shrinks the -wal file; under sustained writes with overlapping readers it can leave the
|
||||
// WAL growing without limit (issue #49). This loop performs a TRUNCATE checkpoint whenever
|
||||
// the WAL has grown to WALCheckpointThreshold, and does nothing while it is small.
|
||||
func (s *Service) walCheckpointLoop() {
|
||||
defer s.wg.Done()
|
||||
|
||||
// Tunable via config; fall back to the package constants when unset/<=0 (issue #49).
|
||||
interval := WALCheckpointInterval
|
||||
if s.config != nil && s.config.WALCheckpointIntervalSeconds > 0 {
|
||||
interval = time.Duration(s.config.WALCheckpointIntervalSeconds) * time.Second
|
||||
}
|
||||
threshold := int64(WALCheckpointThreshold)
|
||||
if s.config != nil && s.config.WALCheckpointThresholdBytes > 0 {
|
||||
threshold = s.config.WALCheckpointThresholdBytes
|
||||
}
|
||||
|
||||
ticker := time.NewTicker(interval)
|
||||
defer ticker.Stop()
|
||||
|
||||
for {
|
||||
select {
|
||||
case <-s.ctx.Done():
|
||||
return
|
||||
case <-ticker.C:
|
||||
s.initMu.RLock()
|
||||
store := s.store
|
||||
s.initMu.RUnlock()
|
||||
if store == nil {
|
||||
continue
|
||||
}
|
||||
|
||||
ctx, cancel := context.WithTimeout(s.ctx, 15*time.Second)
|
||||
done, err := store.CheckpointIfLarge(ctx, threshold)
|
||||
cancel()
|
||||
switch {
|
||||
case err != nil:
|
||||
log.Warn().Err(err).Msg("Periodic WAL checkpoint failed (non-fatal)")
|
||||
case done:
|
||||
log.Debug().Msg("Periodic WAL checkpoint (TRUNCATE) completed")
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// processAllSessions processes pending messages for all active sessions.
|
||||
// Messages are processed in parallel using goroutines, with concurrency
|
||||
// limited by a channel-based semaphore.
|
||||
@@ -1748,6 +1835,9 @@ func (s *Service) Shutdown(ctx context.Context) error {
|
||||
if s.patternDetector != nil {
|
||||
s.patternDetector.Stop()
|
||||
}
|
||||
if s.maintenanceSvc != nil {
|
||||
s.maintenanceSvc.Stop()
|
||||
}
|
||||
|
||||
// Phase 4: Shutdown sessions (flush pending work)
|
||||
log.Debug().Msg("Phase 4: Shutting down sessions...")
|
||||
|
||||
+10
-10
@@ -13,7 +13,7 @@
|
||||
{
|
||||
"name": "claude-mnemonic",
|
||||
"description": "Persistent memory system for Claude Code - Go implementation with SQLite and ChromaDB vector search",
|
||||
"version": "0.12.21",
|
||||
"version": "0.14.9",
|
||||
"author": {
|
||||
"name": "lukaszraczylo",
|
||||
"url": "https://github.com/lukaszraczylo"
|
||||
@@ -23,25 +23,25 @@
|
||||
"license": "MIT",
|
||||
"homepage": "https://github.com/lukaszraczylo/claude-mnemonic",
|
||||
"releases": {
|
||||
"latest": "0.12.21",
|
||||
"latest": "0.14.9",
|
||||
"versions": {
|
||||
"0.12.21": {
|
||||
"releaseDate": "2026-05-26",
|
||||
"0.14.9": {
|
||||
"releaseDate": "2026-06-06",
|
||||
"downloads": {
|
||||
"darwin-arm64": {
|
||||
"url": "https://github.com/lukaszraczylo/claude-mnemonic/releases/download/v0.12.21/claude-mnemonic_0.12.21_darwin_arm64.tar.gz",
|
||||
"url": "https://github.com/lukaszraczylo/claude-mnemonic/releases/download/v0.14.9/claude-mnemonic_0.14.9_darwin_arm64.tar.gz",
|
||||
"format": "tar.gz",
|
||||
"sha256": "3f4709de5fe129c1e6ad3fa4bc9347b650e06539ef89f10b77105ff1bc661dee"
|
||||
"sha256": "65d8bad5083f1dcbc86e67a6b6a3996492774829348857ebc6fc65bc5e3aef2a"
|
||||
},
|
||||
"linux-amd64": {
|
||||
"url": "https://github.com/lukaszraczylo/claude-mnemonic/releases/download/v0.12.21/claude-mnemonic_0.12.21_linux_amd64.tar.gz",
|
||||
"url": "https://github.com/lukaszraczylo/claude-mnemonic/releases/download/v0.14.9/claude-mnemonic_0.14.9_linux_amd64.tar.gz",
|
||||
"format": "tar.gz",
|
||||
"sha256": "0f761ec3eb6d34e51514d4f8f1ea6a62c1b7103ade23ce5ad057143515b8cc91"
|
||||
"sha256": "d857ed30e120d1c093e32c37431d5f4cc9c5d65a87fb75ddbecd5795bac7efb9"
|
||||
},
|
||||
"windows-amd64": {
|
||||
"url": "https://github.com/lukaszraczylo/claude-mnemonic/releases/download/v0.12.21/claude-mnemonic_0.12.21_windows_amd64.zip",
|
||||
"url": "https://github.com/lukaszraczylo/claude-mnemonic/releases/download/v0.14.9/claude-mnemonic_0.14.9_windows_amd64.zip",
|
||||
"format": "zip",
|
||||
"sha256": "34666da29446467bed14b3c6ab92e417bd1db9c8610bfea4edfe5325f69da690"
|
||||
"sha256": "3ac3ddba48768da6c7ff437f42e9dd4081514ab0633a647ae7391c8296ffef93"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -564,6 +564,44 @@ func POSTWithContext(ctx context.Context, port int, path string, body interface{
|
||||
return nil
|
||||
}
|
||||
|
||||
// POSTWithContextResult sends a POST request using the provided context and
|
||||
// decodes the JSON response body, mirroring POST but honoring ctx for
|
||||
// cancellation/deadline. Used on the prompt critical path so a wedged worker
|
||||
// aborts at the hook deadline instead of blocking for the full client timeout.
|
||||
// A non-JSON body is returned as (nil, nil), matching POST's behavior.
|
||||
func POSTWithContextResult(ctx context.Context, port int, path string, body interface{}) (map[string]interface{}, error) {
|
||||
jsonBody, err := json.Marshal(body)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
req, err := http.NewRequestWithContext(ctx, http.MethodPost,
|
||||
fmt.Sprintf("http://127.0.0.1:%d%s", port, path),
|
||||
bytes.NewReader(jsonBody))
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
req.Header.Set("Content-Type", "application/json")
|
||||
|
||||
resp, err := hookClient.Do(req)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
defer func() { _ = resp.Body.Close() }()
|
||||
|
||||
if resp.StatusCode >= 400 {
|
||||
return nil, fmt.Errorf("request failed: %s", resp.Status)
|
||||
}
|
||||
|
||||
var result map[string]interface{}
|
||||
if err := json.NewDecoder(resp.Body).Decode(&result); err != nil {
|
||||
// Not all endpoints return JSON
|
||||
return nil, nil
|
||||
}
|
||||
|
||||
return result, nil
|
||||
}
|
||||
|
||||
// GET sends a GET request to the worker.
|
||||
func GET(port int, path string) (map[string]interface{}, error) {
|
||||
resp, err := hookClient.Get(fmt.Sprintf("http://127.0.0.1:%d%s", port, path))
|
||||
@@ -584,6 +622,35 @@ func GET(port int, path string) (map[string]interface{}, error) {
|
||||
return result, nil
|
||||
}
|
||||
|
||||
// GETWithContext sends a GET request using the provided context and decodes the
|
||||
// JSON response body, mirroring GET but honoring ctx for cancellation/deadline.
|
||||
// Used on the prompt critical path so a wedged worker aborts at the hook
|
||||
// deadline instead of blocking for the full client timeout.
|
||||
func GETWithContext(ctx context.Context, port int, path string) (map[string]interface{}, error) {
|
||||
req, err := http.NewRequestWithContext(ctx, http.MethodGet,
|
||||
fmt.Sprintf("http://127.0.0.1:%d%s", port, path), nil)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
resp, err := hookClient.Do(req)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
defer func() { _ = resp.Body.Close() }()
|
||||
|
||||
if resp.StatusCode >= 400 {
|
||||
return nil, fmt.Errorf("request failed: %s", resp.Status)
|
||||
}
|
||||
|
||||
var result map[string]interface{}
|
||||
if err := json.NewDecoder(resp.Body).Decode(&result); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return result, nil
|
||||
}
|
||||
|
||||
// versionsCompatible checks if two versions are compatible for dev builds.
|
||||
// Returns true if both versions share the same base version (ignoring -dirty, -dev, commit suffixes).
|
||||
// This prevents unnecessary restarts during development.
|
||||
|
||||
@@ -952,6 +952,203 @@ func TestGET_Timeout(t *testing.T) {
|
||||
require.Error(t, err)
|
||||
}
|
||||
|
||||
// TestGETWithContext tests GETWithContext with a mock server.
|
||||
func TestGETWithContext(t *testing.T) {
|
||||
tests := []struct {
|
||||
serverHandler func(w http.ResponseWriter, r *http.Request)
|
||||
expectedResult map[string]interface{}
|
||||
name string
|
||||
expectError bool
|
||||
}{
|
||||
{
|
||||
name: "successful GET with JSON response",
|
||||
serverHandler: func(w http.ResponseWriter, r *http.Request) {
|
||||
assert.Equal(t, http.MethodGet, r.Method)
|
||||
w.WriteHeader(http.StatusOK)
|
||||
_ = json.NewEncoder(w).Encode(map[string]interface{}{"data": "test"})
|
||||
},
|
||||
expectError: false,
|
||||
expectedResult: map[string]interface{}{"data": "test"},
|
||||
},
|
||||
{
|
||||
name: "GET with 404 error",
|
||||
serverHandler: func(w http.ResponseWriter, r *http.Request) {
|
||||
w.WriteHeader(http.StatusNotFound)
|
||||
},
|
||||
expectError: true,
|
||||
},
|
||||
{
|
||||
name: "GET with invalid JSON",
|
||||
serverHandler: func(w http.ResponseWriter, r *http.Request) {
|
||||
w.WriteHeader(http.StatusOK)
|
||||
_, _ = w.Write([]byte("not valid json"))
|
||||
},
|
||||
expectError: true,
|
||||
},
|
||||
}
|
||||
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
server := httptest.NewServer(http.HandlerFunc(tt.serverHandler))
|
||||
defer server.Close()
|
||||
|
||||
var port int
|
||||
_, err := fmt.Sscanf(server.URL, "http://127.0.0.1:%d", &port)
|
||||
require.NoError(t, err)
|
||||
|
||||
result, err := GETWithContext(context.Background(), port, "/test")
|
||||
|
||||
if tt.expectError {
|
||||
assert.Error(t, err)
|
||||
} else {
|
||||
assert.NoError(t, err)
|
||||
if tt.expectedResult != nil {
|
||||
assert.Equal(t, tt.expectedResult["data"], result["data"])
|
||||
}
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
// TestGETWithContext_Timeout verifies the context deadline aborts a slow server
|
||||
// well before the hookClient timeout, so a wedged worker cannot stall the prompt.
|
||||
func TestGETWithContext_Timeout(t *testing.T) {
|
||||
// Server that blocks longer than the context deadline.
|
||||
blockUntil := make(chan struct{})
|
||||
server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||
<-blockUntil // never closed during the test -> server hangs
|
||||
}))
|
||||
defer server.Close()
|
||||
defer close(blockUntil)
|
||||
|
||||
var port int
|
||||
_, err := fmt.Sscanf(server.URL, "http://127.0.0.1:%d", &port)
|
||||
require.NoError(t, err)
|
||||
|
||||
ctx, cancel := context.WithTimeout(context.Background(), 100*time.Millisecond)
|
||||
defer cancel()
|
||||
|
||||
start := time.Now()
|
||||
_, err = GETWithContext(ctx, port, "/test")
|
||||
elapsed := time.Since(start)
|
||||
|
||||
require.Error(t, err)
|
||||
// Should abort near the 100ms deadline, far below hookClient's 10s timeout.
|
||||
assert.Less(t, elapsed, 2*time.Second, "context deadline must abort the request quickly")
|
||||
}
|
||||
|
||||
// TestGETWithContext_CancelledContext verifies an already-cancelled context
|
||||
// returns immediately without making a real request.
|
||||
func TestGETWithContext_CancelledContext(t *testing.T) {
|
||||
ctx, cancel := context.WithCancel(context.Background())
|
||||
cancel() // cancel immediately
|
||||
|
||||
start := time.Now()
|
||||
_, err := GETWithContext(ctx, 99994, "/test")
|
||||
elapsed := time.Since(start)
|
||||
|
||||
require.Error(t, err)
|
||||
assert.Less(t, elapsed, 1*time.Second, "cancelled context should return immediately")
|
||||
}
|
||||
|
||||
// TestPOSTWithContextResult tests POSTWithContextResult with a mock server.
|
||||
func TestPOSTWithContextResult(t *testing.T) {
|
||||
tests := []struct {
|
||||
body interface{}
|
||||
serverHandler func(w http.ResponseWriter, r *http.Request)
|
||||
expectedResult map[string]interface{}
|
||||
name string
|
||||
expectError bool
|
||||
}{
|
||||
{
|
||||
name: "successful POST with JSON response",
|
||||
serverHandler: func(w http.ResponseWriter, r *http.Request) {
|
||||
assert.Equal(t, http.MethodPost, r.Method)
|
||||
assert.Equal(t, "application/json", r.Header.Get("Content-Type"))
|
||||
w.WriteHeader(http.StatusOK)
|
||||
_ = json.NewEncoder(w).Encode(map[string]interface{}{"status": "ok"})
|
||||
},
|
||||
body: map[string]string{"key": "value"},
|
||||
expectError: false,
|
||||
expectedResult: map[string]interface{}{"status": "ok"},
|
||||
},
|
||||
{
|
||||
name: "POST with 400 error",
|
||||
serverHandler: func(w http.ResponseWriter, r *http.Request) {
|
||||
w.WriteHeader(http.StatusBadRequest)
|
||||
},
|
||||
body: map[string]string{"key": "value"},
|
||||
expectError: true,
|
||||
},
|
||||
{
|
||||
name: "POST with non-JSON response returns nil",
|
||||
serverHandler: func(w http.ResponseWriter, r *http.Request) {
|
||||
w.WriteHeader(http.StatusOK)
|
||||
_, _ = w.Write([]byte("not json"))
|
||||
},
|
||||
body: map[string]string{"key": "value"},
|
||||
expectError: false,
|
||||
expectedResult: nil,
|
||||
},
|
||||
}
|
||||
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
server := httptest.NewServer(http.HandlerFunc(tt.serverHandler))
|
||||
defer server.Close()
|
||||
|
||||
var port int
|
||||
_, err := fmt.Sscanf(server.URL, "http://127.0.0.1:%d", &port)
|
||||
require.NoError(t, err)
|
||||
|
||||
result, err := POSTWithContextResult(context.Background(), port, "/test", tt.body)
|
||||
|
||||
if tt.expectError {
|
||||
assert.Error(t, err)
|
||||
} else {
|
||||
assert.NoError(t, err)
|
||||
if tt.expectedResult != nil {
|
||||
assert.Equal(t, tt.expectedResult["status"], result["status"])
|
||||
} else {
|
||||
assert.Nil(t, result)
|
||||
}
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
// TestPOSTWithContextResult_MarshalError tests POSTWithContextResult with an unmarshalable body.
|
||||
func TestPOSTWithContextResult_MarshalError(t *testing.T) {
|
||||
badValue := make(chan int)
|
||||
_, err := POSTWithContextResult(context.Background(), 99999, "/test", badValue)
|
||||
require.Error(t, err)
|
||||
}
|
||||
|
||||
// TestPOSTWithContextResult_Timeout verifies the context deadline aborts a slow
|
||||
// server before the hookClient timeout.
|
||||
func TestPOSTWithContextResult_Timeout(t *testing.T) {
|
||||
blockUntil := make(chan struct{})
|
||||
server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||
<-blockUntil
|
||||
}))
|
||||
defer server.Close()
|
||||
defer close(blockUntil)
|
||||
|
||||
var port int
|
||||
_, err := fmt.Sscanf(server.URL, "http://127.0.0.1:%d", &port)
|
||||
require.NoError(t, err)
|
||||
|
||||
ctx, cancel := context.WithTimeout(context.Background(), 100*time.Millisecond)
|
||||
defer cancel()
|
||||
|
||||
start := time.Now()
|
||||
_, err = POSTWithContextResult(ctx, port, "/test", map[string]string{"k": "v"})
|
||||
elapsed := time.Since(start)
|
||||
|
||||
require.Error(t, err)
|
||||
assert.Less(t, elapsed, 2*time.Second, "context deadline must abort the request quickly")
|
||||
}
|
||||
|
||||
// TestIsWorkerRunning_Timeout tests IsWorkerRunning with timeout.
|
||||
func TestIsWorkerRunning_Timeout(t *testing.T) {
|
||||
// Non-existent port should quickly return false
|
||||
|
||||
@@ -0,0 +1,4 @@
|
||||
# Resolve the FontAwesome *free* package from the public npm registry rather than
|
||||
# the authenticated npm.fontawesome.com. This lets `npm ci` work in CI without a
|
||||
# FontAwesome token (the free package is published to registry.npmjs.org too).
|
||||
@fortawesome:registry=https://registry.npmjs.org/
|
||||
Generated
+980
-1980
File diff suppressed because it is too large
Load Diff
+12
-12
@@ -10,19 +10,19 @@
|
||||
"type-check": "vue-tsc --noEmit"
|
||||
},
|
||||
"dependencies": {
|
||||
"vis-data": "^7.1.9",
|
||||
"vis-network": "^9.1.9",
|
||||
"vue": "^3.5.13"
|
||||
"vis-data": "^8.0.4",
|
||||
"vis-network": "^10.1.0",
|
||||
"vue": "^3.5.34"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@fortawesome/fontawesome-free": "^6.7.2",
|
||||
"@types/node": "^22.10.2",
|
||||
"@vitejs/plugin-vue": "^5.2.1",
|
||||
"autoprefixer": "^10.4.20",
|
||||
"postcss": "^8.4.49",
|
||||
"tailwindcss": "^3.4.17",
|
||||
"typescript": "~5.7.2",
|
||||
"vite": "^6.0.5",
|
||||
"vue-tsc": "^2.2.0"
|
||||
"@fortawesome/fontawesome-free": "^7.2.0",
|
||||
"@tailwindcss/postcss": "^4.3.0",
|
||||
"@types/node": "^25.9.1",
|
||||
"@vitejs/plugin-vue": "^6.0.7",
|
||||
"postcss": "^8.5.15",
|
||||
"tailwindcss": "^4.3.0",
|
||||
"typescript": "~6.0.3",
|
||||
"vite": "^8.0.14",
|
||||
"vue-tsc": "^3.3.1"
|
||||
}
|
||||
}
|
||||
|
||||
+12
-12
@@ -10,19 +10,19 @@
|
||||
"type-check": "vue-tsc --noEmit"
|
||||
},
|
||||
"dependencies": {
|
||||
"vis-data": "^7.1.9",
|
||||
"vis-network": "^9.1.9",
|
||||
"vue": "^3.5.13"
|
||||
"vis-data": "^8.0.4",
|
||||
"vis-network": "^10.1.0",
|
||||
"vue": "^3.5.34"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@fortawesome/fontawesome-free": "^6.7.2",
|
||||
"@types/node": "^22.10.2",
|
||||
"@vitejs/plugin-vue": "^5.2.1",
|
||||
"autoprefixer": "^10.4.20",
|
||||
"postcss": "^8.4.49",
|
||||
"tailwindcss": "^3.4.17",
|
||||
"typescript": "~5.7.2",
|
||||
"vite": "^6.0.5",
|
||||
"vue-tsc": "^2.2.0"
|
||||
"@fortawesome/fontawesome-free": "^7.2.0",
|
||||
"@tailwindcss/postcss": "^4.3.0",
|
||||
"@types/node": "^25.9.1",
|
||||
"@vitejs/plugin-vue": "^6.0.7",
|
||||
"postcss": "^8.5.15",
|
||||
"tailwindcss": "^4.3.0",
|
||||
"typescript": "~6.0.3",
|
||||
"vite": "^8.0.14",
|
||||
"vue-tsc": "^3.3.1"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,6 +1,5 @@
|
||||
export default {
|
||||
plugins: {
|
||||
tailwindcss: {},
|
||||
autoprefixer: {},
|
||||
'@tailwindcss/postcss': {},
|
||||
},
|
||||
}
|
||||
|
||||
@@ -1,6 +1,5 @@
|
||||
@tailwind base;
|
||||
@tailwind components;
|
||||
@tailwind utilities;
|
||||
@import "tailwindcss";
|
||||
@config "../../tailwind.config.js";
|
||||
|
||||
@layer components {
|
||||
/* Glass morphism effect */
|
||||
|
||||
+1
-2
@@ -16,9 +16,8 @@
|
||||
"noUnusedParameters": true,
|
||||
"noFallthroughCasesInSwitch": true,
|
||||
"noUncheckedSideEffectImports": true,
|
||||
"baseUrl": ".",
|
||||
"paths": {
|
||||
"@/*": ["src/*"]
|
||||
"@/*": ["./src/*"]
|
||||
}
|
||||
},
|
||||
"include": ["src/**/*.ts", "src/**/*.tsx", "src/**/*.vue"]
|
||||
|
||||
@@ -1 +1 @@
|
||||
{"root":["./src/main.ts","./src/vite-env.d.ts","./src/components/index.ts","./src/composables/index.ts","./src/composables/usegraphmetrics.ts","./src/composables/usehealth.ts","./src/composables/usesse.ts","./src/composables/usestats.ts","./src/composables/usetimeline.ts","./src/composables/usetypes.ts","./src/composables/useupdate.ts","./src/types/api.ts","./src/types/index.ts","./src/types/observation.ts","./src/types/prompt.ts","./src/types/relation.ts","./src/types/summary.ts","./src/utils/api.ts","./src/utils/formatters.ts","./src/app.vue","./src/components/badge.vue","./src/components/card.vue","./src/components/filtertabs.vue","./src/components/header.vue","./src/components/iconbox.vue","./src/components/observationcard.vue","./src/components/projectfilter.vue","./src/components/promptcard.vue","./src/components/relationgraph.vue","./src/components/scorebreakdown.vue","./src/components/searchanalytics.vue","./src/components/sidebar.vue","./src/components/statscards.vue","./src/components/summarycard.vue","./src/components/systemhealthdetails.vue","./src/components/timeline.vue","./src/components/topobservations.vue"],"version":"5.7.3"}
|
||||
{"root":["./src/main.ts","./src/vite-env.d.ts","./src/components/index.ts","./src/composables/index.ts","./src/composables/usegraphmetrics.ts","./src/composables/usehealth.ts","./src/composables/usesse.ts","./src/composables/usestats.ts","./src/composables/usetimeline.ts","./src/composables/usetypes.ts","./src/composables/useupdate.ts","./src/types/api.ts","./src/types/index.ts","./src/types/observation.ts","./src/types/prompt.ts","./src/types/relation.ts","./src/types/summary.ts","./src/utils/api.ts","./src/utils/formatters.ts","./src/app.vue","./src/components/badge.vue","./src/components/card.vue","./src/components/filtertabs.vue","./src/components/header.vue","./src/components/iconbox.vue","./src/components/observationcard.vue","./src/components/projectfilter.vue","./src/components/promptcard.vue","./src/components/relationgraph.vue","./src/components/scorebreakdown.vue","./src/components/searchanalytics.vue","./src/components/sidebar.vue","./src/components/statscards.vue","./src/components/summarycard.vue","./src/components/systemhealthdetails.vue","./src/components/timeline.vue","./src/components/topobservations.vue"],"version":"6.0.3"}
|
||||
Reference in New Issue
Block a user