mirror of
https://github.com/lukaszraczylo/claude-mnemonic.git
synced 2026-10-12 04:55:18 +00:00
Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
cce1c62184 | ||
|
|
9629cff306 | ||
|
|
bc336a892c |
@@ -12,6 +12,15 @@ Claude Code forgets everything when your session ends. Claude Mnemonic fixes tha
|
||||
|
||||
It captures what Claude learns during your coding sessions - bug fixes, architecture decisions, patterns that work - and brings that knowledge back in future conversations. No more re-explaining your codebase.
|
||||
|
||||
## What's New in v0.6
|
||||
|
||||
- **Auto-Updates** - Automatically stays up-to-date with the latest version
|
||||
- **Slash Command: `/restart`** - Restart the worker directly from Claude Code
|
||||
- **Local Embeddings** - All semantic search runs locally via ONNX Runtime (no external API calls)
|
||||
- **Async Queue Processing** - Non-blocking observation capture for faster sessions
|
||||
- **Smarter Storage** - Filters out system/agent summaries to keep knowledge relevant
|
||||
- **Improved Reliability** - Better handling of connectivity issues and dead connections
|
||||
|
||||
## Requirements
|
||||
|
||||
| Dependency | Required | Purpose |
|
||||
@@ -73,9 +82,11 @@ cosign verify-blob \
|
||||
| **Persistent Memory** | Observations survive across sessions and restarts |
|
||||
| **Project Isolation** | Each project has its own knowledge base |
|
||||
| **Global Patterns** | Best practices are shared across all projects |
|
||||
| **Semantic Search** | Find relevant context with natural language |
|
||||
| **Semantic Search** | Find relevant context with natural language (local embeddings) |
|
||||
| **Live Statusline** | Real-time metrics in Claude Code: `[mnemonic] ● served:42 | project:28 memories` |
|
||||
| **Web Dashboard** | Browse and manage memories at `localhost:37777` |
|
||||
| **Auto-Updates** | Automatically downloads and applies new versions |
|
||||
| **Slash Commands** | Control the worker directly from Claude Code |
|
||||
|
||||
### How knowledge flows
|
||||
|
||||
@@ -131,6 +142,25 @@ These search tools are available via MCP:
|
||||
- `changes` - find code modifications
|
||||
- `how_it_works` - system understanding queries
|
||||
|
||||
## Slash Commands
|
||||
|
||||
Available commands within Claude Code:
|
||||
|
||||
| Command | Description |
|
||||
|---------|-------------|
|
||||
| `/restart` | Restart the worker process when experiencing issues |
|
||||
|
||||
## Auto-Updates
|
||||
|
||||
Claude Mnemonic automatically checks for updates and applies them. Updates are downloaded in the background and applied on restart.
|
||||
|
||||
- Automatic update checks on startup
|
||||
- Background downloads (up to 250MB)
|
||||
- Seamless restart after update
|
||||
- Manual trigger: `curl -X POST http://127.0.0.1:37777/api/update/apply`
|
||||
|
||||
Check update status: `curl http://127.0.0.1:37777/api/update/status`
|
||||
|
||||
## Troubleshooting
|
||||
|
||||
**Worker won't start?**
|
||||
@@ -144,6 +174,20 @@ cat /tmp/claude-mnemonic-worker.log # view logs
|
||||
rm -f ~/.claude-mnemonic/*.db-wal ~/.claude-mnemonic/*.db-shm
|
||||
```
|
||||
|
||||
**Worker unresponsive?**
|
||||
```bash
|
||||
# Restart via API
|
||||
curl -X POST http://127.0.0.1:37777/api/restart
|
||||
|
||||
# Or use the slash command in Claude Code
|
||||
/restart
|
||||
```
|
||||
|
||||
**Check health status:**
|
||||
```bash
|
||||
curl http://127.0.0.1:37777/api/selfcheck
|
||||
```
|
||||
|
||||
## Uninstall
|
||||
|
||||
```bash
|
||||
|
||||
@@ -643,7 +643,8 @@ func (s *Service) handleGetTypes(w http.ResponseWriter, r *http.Request) {
|
||||
|
||||
// handleGetStats returns worker statistics.
|
||||
func (s *Service) handleGetStats(w http.ResponseWriter, r *http.Request) {
|
||||
retrievalStats := s.GetRetrievalStats()
|
||||
project := r.URL.Query().Get("project")
|
||||
retrievalStats := s.GetRetrievalStats(project)
|
||||
sessionsToday, _ := s.sessionStore.GetSessionsToday(r.Context())
|
||||
|
||||
response := map[string]interface{}{
|
||||
@@ -658,7 +659,7 @@ func (s *Service) handleGetStats(w http.ResponseWriter, r *http.Request) {
|
||||
}
|
||||
|
||||
// Include project-specific observation count if project is specified
|
||||
if project := r.URL.Query().Get("project"); project != "" {
|
||||
if project != "" {
|
||||
count, err := s.observationStore.GetObservationCount(r.Context(), project)
|
||||
if err == nil {
|
||||
response["projectObservations"] = count
|
||||
@@ -671,7 +672,8 @@ func (s *Service) handleGetStats(w http.ResponseWriter, r *http.Request) {
|
||||
|
||||
// handleGetRetrievalStats returns detailed retrieval statistics.
|
||||
func (s *Service) handleGetRetrievalStats(w http.ResponseWriter, r *http.Request) {
|
||||
stats := s.GetRetrievalStats()
|
||||
project := r.URL.Query().Get("project")
|
||||
stats := s.GetRetrievalStats(project)
|
||||
writeJSON(w, stats)
|
||||
}
|
||||
|
||||
@@ -775,7 +777,7 @@ func (s *Service) handleSearchByPrompt(w http.ResponseWriter, r *http.Request) {
|
||||
clusteredObservations := clusterObservations(freshObservations, 0.4)
|
||||
|
||||
// Record retrieval stats (no verification done, so verified=0, deleted=0)
|
||||
s.recordRetrievalStats(int64(len(clusteredObservations)), 0, 0, true)
|
||||
s.recordRetrievalStats(project, int64(len(clusteredObservations)), 0, 0, true)
|
||||
|
||||
log.Info().
|
||||
Str("project", project).
|
||||
@@ -853,7 +855,7 @@ func (s *Service) handleContextInject(w http.ResponseWriter, r *http.Request) {
|
||||
duplicatesRemoved := len(freshObservations) - len(clusteredObservations)
|
||||
|
||||
// Record retrieval stats (no verification done)
|
||||
s.recordRetrievalStats(int64(len(clusteredObservations)), 0, 0, false)
|
||||
s.recordRetrievalStats(project, int64(len(clusteredObservations)), 0, 0, false)
|
||||
|
||||
log.Info().
|
||||
Str("project", project).
|
||||
|
||||
@@ -65,6 +65,7 @@ func testService(t *testing.T) (*Service, func()) {
|
||||
ctx: ctx,
|
||||
cancel: cancel,
|
||||
startTime: time.Now(),
|
||||
retrievalStats: make(map[string]*RetrievalStats),
|
||||
}
|
||||
|
||||
svc.setupRoutes()
|
||||
@@ -436,8 +437,8 @@ func TestRetrievalStats(t *testing.T) {
|
||||
|
||||
assert.Equal(t, http.StatusOK, rec.Code)
|
||||
|
||||
// Check stats
|
||||
stats := svc.GetRetrievalStats()
|
||||
// Check stats for the specific project
|
||||
stats := svc.GetRetrievalStats(project)
|
||||
assert.Equal(t, int64(1), stats.TotalRequests)
|
||||
assert.Equal(t, int64(1), stats.SearchRequests)
|
||||
assert.GreaterOrEqual(t, stats.ObservationsServed, int64(1))
|
||||
@@ -1516,21 +1517,22 @@ func TestGetRetrievalStats(t *testing.T) {
|
||||
svc, cleanup := testService(t)
|
||||
defer cleanup()
|
||||
|
||||
// Initially all zeros
|
||||
stats := svc.GetRetrievalStats()
|
||||
project := "stats-test"
|
||||
|
||||
// Initially all zeros for this project
|
||||
stats := svc.GetRetrievalStats(project)
|
||||
assert.Equal(t, int64(0), stats.TotalRequests)
|
||||
assert.Equal(t, int64(0), stats.SearchRequests)
|
||||
|
||||
// Make some requests to increment stats
|
||||
project := "stats-test"
|
||||
createTestObservation(t, svc.observationStore, project, "Test", "Content", []string{"test"})
|
||||
|
||||
req := httptest.NewRequest(http.MethodGet, "/api/context/search?project="+project+"&query=test", nil)
|
||||
rec := httptest.NewRecorder()
|
||||
svc.router.ServeHTTP(rec, req)
|
||||
|
||||
// Stats should be updated
|
||||
stats = svc.GetRetrievalStats()
|
||||
// Stats should be updated for this project
|
||||
stats = svc.GetRetrievalStats(project)
|
||||
assert.GreaterOrEqual(t, stats.TotalRequests, int64(1))
|
||||
}
|
||||
|
||||
@@ -1786,6 +1788,81 @@ func TestHandleGetStats_AllProjects(t *testing.T) {
|
||||
assert.Equal(t, http.StatusOK, rec.Code)
|
||||
}
|
||||
|
||||
// TestStatuslineRetrievalStatsAreProjectSpecific verifies that the stats endpoint
|
||||
// (used by statusline hook) returns project-specific retrieval statistics.
|
||||
// This is critical for correct statusline display in multi-project environments.
|
||||
func TestStatuslineRetrievalStatsAreProjectSpecific(t *testing.T) {
|
||||
svc, cleanup := testService(t)
|
||||
defer cleanup()
|
||||
|
||||
projectA := "statusline-project-a"
|
||||
projectB := "statusline-project-b"
|
||||
|
||||
// Create observations in both projects
|
||||
createTestObservation(t, svc.observationStore, projectA, "Observation A", "Content for project A", []string{"test"})
|
||||
createTestObservation(t, svc.observationStore, projectB, "Observation B", "Content for project B", []string{"test"})
|
||||
|
||||
// Make search requests for projectA (simulates user activity in that project)
|
||||
reqA := httptest.NewRequest(http.MethodGet, "/api/context/search?project="+projectA+"&query=test", nil)
|
||||
recA := httptest.NewRecorder()
|
||||
svc.router.ServeHTTP(recA, reqA)
|
||||
assert.Equal(t, http.StatusOK, recA.Code)
|
||||
|
||||
// Make multiple search requests for projectB
|
||||
for i := 0; i < 3; i++ {
|
||||
reqB := httptest.NewRequest(http.MethodGet, "/api/context/search?project="+projectB+"&query=test", nil)
|
||||
recB := httptest.NewRecorder()
|
||||
svc.router.ServeHTTP(recB, reqB)
|
||||
assert.Equal(t, http.StatusOK, recB.Code)
|
||||
}
|
||||
|
||||
// Now verify stats endpoint returns project-specific stats (as statusline hook does)
|
||||
// Check projectA stats
|
||||
reqStatsA := httptest.NewRequest(http.MethodGet, "/api/stats?project="+projectA, nil)
|
||||
recStatsA := httptest.NewRecorder()
|
||||
svc.router.ServeHTTP(recStatsA, reqStatsA)
|
||||
|
||||
assert.Equal(t, http.StatusOK, recStatsA.Code)
|
||||
var responseA map[string]interface{}
|
||||
err := json.Unmarshal(recStatsA.Body.Bytes(), &responseA)
|
||||
require.NoError(t, err)
|
||||
|
||||
// Verify projectA has 1 search request
|
||||
retrievalA := responseA["retrieval"].(map[string]interface{})
|
||||
assert.Equal(t, float64(1), retrievalA["SearchRequests"], "projectA should have 1 search request")
|
||||
assert.Equal(t, float64(1), retrievalA["TotalRequests"], "projectA should have 1 total request")
|
||||
|
||||
// Check projectB stats
|
||||
reqStatsB := httptest.NewRequest(http.MethodGet, "/api/stats?project="+projectB, nil)
|
||||
recStatsB := httptest.NewRecorder()
|
||||
svc.router.ServeHTTP(recStatsB, reqStatsB)
|
||||
|
||||
assert.Equal(t, http.StatusOK, recStatsB.Code)
|
||||
var responseB map[string]interface{}
|
||||
err = json.Unmarshal(recStatsB.Body.Bytes(), &responseB)
|
||||
require.NoError(t, err)
|
||||
|
||||
// Verify projectB has 3 search requests
|
||||
retrievalB := responseB["retrieval"].(map[string]interface{})
|
||||
assert.Equal(t, float64(3), retrievalB["SearchRequests"], "projectB should have 3 search requests")
|
||||
assert.Equal(t, float64(3), retrievalB["TotalRequests"], "projectB should have 3 total requests")
|
||||
|
||||
// Check aggregate stats (no project filter)
|
||||
reqStatsAll := httptest.NewRequest(http.MethodGet, "/api/stats", nil)
|
||||
recStatsAll := httptest.NewRecorder()
|
||||
svc.router.ServeHTTP(recStatsAll, reqStatsAll)
|
||||
|
||||
assert.Equal(t, http.StatusOK, recStatsAll.Code)
|
||||
var responseAll map[string]interface{}
|
||||
err = json.Unmarshal(recStatsAll.Body.Bytes(), &responseAll)
|
||||
require.NoError(t, err)
|
||||
|
||||
// Verify aggregate has sum of all requests (1 + 3 = 4)
|
||||
retrievalAll := responseAll["retrieval"].(map[string]interface{})
|
||||
assert.Equal(t, float64(4), retrievalAll["SearchRequests"], "aggregate should have 4 search requests")
|
||||
assert.Equal(t, float64(4), retrievalAll["TotalRequests"], "aggregate should have 4 total requests")
|
||||
}
|
||||
|
||||
// TestHandleSubagentComplete_WithSession tests subagent completion with existing session.
|
||||
func TestHandleSubagentComplete_WithSession(t *testing.T) {
|
||||
svc, cleanup := testService(t)
|
||||
@@ -2249,25 +2326,35 @@ func TestRecordRetrievalStats(t *testing.T) {
|
||||
svc, cleanup := testService(t)
|
||||
defer cleanup()
|
||||
|
||||
// Initially all zeros
|
||||
stats := svc.GetRetrievalStats()
|
||||
project := "test-project"
|
||||
|
||||
// Initially all zeros for this project
|
||||
stats := svc.GetRetrievalStats(project)
|
||||
assert.Equal(t, int64(0), stats.TotalRequests)
|
||||
|
||||
// Record a search request
|
||||
svc.recordRetrievalStats(10, 1, 0, true)
|
||||
stats = svc.GetRetrievalStats()
|
||||
// Record a search request for the project
|
||||
svc.recordRetrievalStats(project, 10, 1, 0, true)
|
||||
stats = svc.GetRetrievalStats(project)
|
||||
assert.Equal(t, int64(1), stats.TotalRequests)
|
||||
assert.Equal(t, int64(10), stats.ObservationsServed)
|
||||
assert.Equal(t, int64(1), stats.VerifiedStale)
|
||||
assert.Equal(t, int64(1), stats.SearchRequests)
|
||||
|
||||
// Record a context injection
|
||||
svc.recordRetrievalStats(5, 0, 1, false)
|
||||
stats = svc.GetRetrievalStats()
|
||||
// Record a context injection for the project
|
||||
svc.recordRetrievalStats(project, 5, 0, 1, false)
|
||||
stats = svc.GetRetrievalStats(project)
|
||||
assert.Equal(t, int64(2), stats.TotalRequests)
|
||||
assert.Equal(t, int64(15), stats.ObservationsServed)
|
||||
assert.Equal(t, int64(1), stats.DeletedInvalid)
|
||||
assert.Equal(t, int64(1), stats.ContextInjections)
|
||||
|
||||
// Different project should have zero stats
|
||||
otherStats := svc.GetRetrievalStats("other-project")
|
||||
assert.Equal(t, int64(0), otherStats.TotalRequests)
|
||||
|
||||
// Aggregate stats (empty project) should show all
|
||||
aggStats := svc.GetRetrievalStats("")
|
||||
assert.Equal(t, int64(2), aggStats.TotalRequests)
|
||||
}
|
||||
|
||||
// TestHandleSelfCheck_WithInitError tests self-check with init error.
|
||||
|
||||
+52
-19
@@ -80,8 +80,9 @@ type Service struct {
|
||||
server *http.Server
|
||||
startTime time.Time
|
||||
|
||||
// Retrieval statistics
|
||||
retrievalStats RetrievalStats
|
||||
// Retrieval statistics (per-project)
|
||||
retrievalStats map[string]*RetrievalStats
|
||||
retrievalStatsMu sync.RWMutex
|
||||
|
||||
// Lifecycle
|
||||
ctx context.Context
|
||||
@@ -137,6 +138,7 @@ func NewService(version string) (*Service, error) {
|
||||
cancel: cancel,
|
||||
startTime: time.Now(),
|
||||
updater: update.New(version, installDir),
|
||||
retrievalStats: make(map[string]*RetrievalStats),
|
||||
}
|
||||
|
||||
// Setup middleware and routes (health endpoint works immediately)
|
||||
@@ -673,29 +675,60 @@ func (s *Service) setupRoutes() {
|
||||
})
|
||||
}
|
||||
|
||||
// recordRetrievalStats atomically updates retrieval statistics.
|
||||
func (s *Service) recordRetrievalStats(served, verified, deleted int64, isSearch bool) {
|
||||
atomic.AddInt64(&s.retrievalStats.TotalRequests, 1)
|
||||
atomic.AddInt64(&s.retrievalStats.ObservationsServed, served)
|
||||
atomic.AddInt64(&s.retrievalStats.VerifiedStale, verified)
|
||||
atomic.AddInt64(&s.retrievalStats.DeletedInvalid, deleted)
|
||||
// recordRetrievalStats atomically updates retrieval statistics for a project.
|
||||
func (s *Service) recordRetrievalStats(project string, served, verified, deleted int64, isSearch bool) {
|
||||
s.retrievalStatsMu.Lock()
|
||||
stats := s.retrievalStats[project]
|
||||
if stats == nil {
|
||||
stats = &RetrievalStats{}
|
||||
s.retrievalStats[project] = stats
|
||||
}
|
||||
s.retrievalStatsMu.Unlock()
|
||||
|
||||
atomic.AddInt64(&stats.TotalRequests, 1)
|
||||
atomic.AddInt64(&stats.ObservationsServed, served)
|
||||
atomic.AddInt64(&stats.VerifiedStale, verified)
|
||||
atomic.AddInt64(&stats.DeletedInvalid, deleted)
|
||||
if isSearch {
|
||||
atomic.AddInt64(&s.retrievalStats.SearchRequests, 1)
|
||||
atomic.AddInt64(&stats.SearchRequests, 1)
|
||||
} else {
|
||||
atomic.AddInt64(&s.retrievalStats.ContextInjections, 1)
|
||||
atomic.AddInt64(&stats.ContextInjections, 1)
|
||||
}
|
||||
}
|
||||
|
||||
// GetRetrievalStats returns a copy of the retrieval stats.
|
||||
func (s *Service) GetRetrievalStats() RetrievalStats {
|
||||
return RetrievalStats{
|
||||
TotalRequests: atomic.LoadInt64(&s.retrievalStats.TotalRequests),
|
||||
ObservationsServed: atomic.LoadInt64(&s.retrievalStats.ObservationsServed),
|
||||
VerifiedStale: atomic.LoadInt64(&s.retrievalStats.VerifiedStale),
|
||||
DeletedInvalid: atomic.LoadInt64(&s.retrievalStats.DeletedInvalid),
|
||||
SearchRequests: atomic.LoadInt64(&s.retrievalStats.SearchRequests),
|
||||
ContextInjections: atomic.LoadInt64(&s.retrievalStats.ContextInjections),
|
||||
// GetRetrievalStats returns a copy of the retrieval stats for a project.
|
||||
// If project is empty, returns aggregate stats across all projects.
|
||||
func (s *Service) GetRetrievalStats(project string) RetrievalStats {
|
||||
s.retrievalStatsMu.RLock()
|
||||
defer s.retrievalStatsMu.RUnlock()
|
||||
|
||||
if project != "" {
|
||||
// Return stats for specific project
|
||||
stats := s.retrievalStats[project]
|
||||
if stats == nil {
|
||||
return RetrievalStats{}
|
||||
}
|
||||
return RetrievalStats{
|
||||
TotalRequests: atomic.LoadInt64(&stats.TotalRequests),
|
||||
ObservationsServed: atomic.LoadInt64(&stats.ObservationsServed),
|
||||
VerifiedStale: atomic.LoadInt64(&stats.VerifiedStale),
|
||||
DeletedInvalid: atomic.LoadInt64(&stats.DeletedInvalid),
|
||||
SearchRequests: atomic.LoadInt64(&stats.SearchRequests),
|
||||
ContextInjections: atomic.LoadInt64(&stats.ContextInjections),
|
||||
}
|
||||
}
|
||||
|
||||
// Aggregate stats across all projects
|
||||
var result RetrievalStats
|
||||
for _, stats := range s.retrievalStats {
|
||||
result.TotalRequests += atomic.LoadInt64(&stats.TotalRequests)
|
||||
result.ObservationsServed += atomic.LoadInt64(&stats.ObservationsServed)
|
||||
result.VerifiedStale += atomic.LoadInt64(&stats.VerifiedStale)
|
||||
result.DeletedInvalid += atomic.LoadInt64(&stats.DeletedInvalid)
|
||||
result.SearchRequests += atomic.LoadInt64(&stats.SearchRequests)
|
||||
result.ContextInjections += atomic.LoadInt64(&stats.ContextInjections)
|
||||
}
|
||||
return result
|
||||
}
|
||||
|
||||
// Start starts the worker service.
|
||||
|
||||
+9
-9
@@ -13,7 +13,7 @@
|
||||
{
|
||||
"name": "claude-mnemonic",
|
||||
"description": "Persistent memory system for Claude Code - Go implementation with SQLite and ChromaDB vector search",
|
||||
"version": "0.6.26",
|
||||
"version": "0.6.33",
|
||||
"author": {
|
||||
"name": "lukaszraczylo",
|
||||
"url": "https://github.com/lukaszraczylo"
|
||||
@@ -23,25 +23,25 @@
|
||||
"license": "MIT",
|
||||
"homepage": "https://github.com/lukaszraczylo/claude-mnemonic",
|
||||
"releases": {
|
||||
"latest": "0.6.26",
|
||||
"latest": "0.6.33",
|
||||
"versions": {
|
||||
"0.6.26": {
|
||||
"0.6.33": {
|
||||
"releaseDate": "2025-12-17",
|
||||
"downloads": {
|
||||
"darwin-arm64": {
|
||||
"url": "https://github.com/lukaszraczylo/claude-mnemonic/releases/download/v0.6.26/claude-mnemonic_0.6.26_darwin_arm64.tar.gz",
|
||||
"url": "https://github.com/lukaszraczylo/claude-mnemonic/releases/download/v0.6.33/claude-mnemonic_0.6.33_darwin_arm64.tar.gz",
|
||||
"format": "tar.gz",
|
||||
"sha256": "47ad8d07e8c422be6a42a562a78ed23cd39adbdc04d774a65eb65ba5a3bce639"
|
||||
"sha256": "fe42634c859b4146fb1953f1d895872fd827f7e95b6510b49770ac4e2f4e4e4f"
|
||||
},
|
||||
"linux-amd64": {
|
||||
"url": "https://github.com/lukaszraczylo/claude-mnemonic/releases/download/v0.6.26/claude-mnemonic_0.6.26_linux_amd64.tar.gz",
|
||||
"url": "https://github.com/lukaszraczylo/claude-mnemonic/releases/download/v0.6.33/claude-mnemonic_0.6.33_linux_amd64.tar.gz",
|
||||
"format": "tar.gz",
|
||||
"sha256": "0bd8cf771f21c93a0310e815bd783876b7a6ff623dceeab36f8d8c5f243385c7"
|
||||
"sha256": "be0665ee52ed56aafc7154d7f223f236f347aa88babc49126b5459341b1158f1"
|
||||
},
|
||||
"windows-amd64": {
|
||||
"url": "https://github.com/lukaszraczylo/claude-mnemonic/releases/download/v0.6.26/claude-mnemonic_0.6.26_windows_amd64.zip",
|
||||
"url": "https://github.com/lukaszraczylo/claude-mnemonic/releases/download/v0.6.33/claude-mnemonic_0.6.33_windows_amd64.zip",
|
||||
"format": "zip",
|
||||
"sha256": "281ee9ae4d371a8fa4aea5d6e7aeae40aae9baa62e9b3b2b5a54f18a4613d4a1"
|
||||
"sha256": "09c5e1ddf5ea04f58ad148cebf973c21d8ee8a15c6dd09a9609000a1caadfb5e"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Generated
+2
-2
@@ -1,12 +1,12 @@
|
||||
{
|
||||
"name": "claude-mnemonic-dashboard",
|
||||
"version": "v0.6.15-2-gd8a4939-dirty",
|
||||
"version": "v0.6.33-3-gf38ce5c-dirty",
|
||||
"lockfileVersion": 3,
|
||||
"requires": true,
|
||||
"packages": {
|
||||
"": {
|
||||
"name": "claude-mnemonic-dashboard",
|
||||
"version": "v0.6.15-2-gd8a4939-dirty",
|
||||
"version": "v0.6.33-3-gf38ce5c-dirty",
|
||||
"dependencies": {
|
||||
"vue": "^3.5.13"
|
||||
},
|
||||
|
||||
+1
-1
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "claude-mnemonic-dashboard",
|
||||
"version": "v0.6.15-2-gd8a4939-dirty",
|
||||
"version": "v0.6.33-3-gf38ce5c-dirty",
|
||||
"private": true,
|
||||
"type": "module",
|
||||
"scripts": {
|
||||
|
||||
+3
-1
@@ -8,9 +8,9 @@ import Sidebar from '@/components/Sidebar.vue'
|
||||
|
||||
// Composables
|
||||
const { isConnected, isReconnecting, reconnectCountdown, isProcessing, queueDepth } = useSSE()
|
||||
const { stats } = useStats()
|
||||
const { updateInfo, updateStatus, isUpdating, applyUpdate } = useUpdate()
|
||||
const { health } = useHealth()
|
||||
// Initialize useTimeline first to get currentProject ref
|
||||
const {
|
||||
filteredItems,
|
||||
loading,
|
||||
@@ -27,6 +27,8 @@ const {
|
||||
setTypeFilter,
|
||||
setConceptFilter
|
||||
} = useTimeline()
|
||||
// Pass currentProject ref to useStats for project-specific retrieval stats
|
||||
const { stats } = useStats(currentProject)
|
||||
|
||||
// Note: Timeline refresh is handled by useTimeline's SSE watcher
|
||||
</script>
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { ref, onMounted, onUnmounted, watch } from 'vue'
|
||||
import { ref, onMounted, onUnmounted, watch, type Ref } from 'vue'
|
||||
import type { Stats } from '@/types'
|
||||
import { fetchStats } from '@/utils/api'
|
||||
import { useSSE } from './useSSE'
|
||||
@@ -6,7 +6,7 @@ import { useSSE } from './useSSE'
|
||||
// Fallback poll interval when SSE is disconnected
|
||||
const FALLBACK_POLL_INTERVAL = 10000 // 10 seconds
|
||||
|
||||
export function useStats() {
|
||||
export function useStats(projectRef?: Ref<string | null>) {
|
||||
const stats = ref<Stats | null>(null)
|
||||
const loading = ref(false)
|
||||
const error = ref<string | null>(null)
|
||||
@@ -21,7 +21,8 @@ export function useStats() {
|
||||
error.value = null
|
||||
|
||||
try {
|
||||
stats.value = await fetchStats()
|
||||
const project = projectRef?.value ?? null
|
||||
stats.value = await fetchStats(project)
|
||||
} catch (err) {
|
||||
error.value = err instanceof Error ? err.message : 'Failed to fetch stats'
|
||||
console.error('[Stats] Error:', err)
|
||||
@@ -54,6 +55,14 @@ export function useStats() {
|
||||
}
|
||||
})
|
||||
|
||||
// Watch for project filter changes
|
||||
if (projectRef) {
|
||||
watch(projectRef, () => {
|
||||
console.log('[Stats] Project filter changed, refreshing stats')
|
||||
refresh()
|
||||
})
|
||||
}
|
||||
|
||||
// Switch between SSE-driven and fallback polling based on connection status
|
||||
watch(isConnected, (connected) => {
|
||||
if (connected) {
|
||||
|
||||
+5
-2
@@ -107,8 +107,11 @@ export async function fetchSummaries(limit: number = 50, project?: string, signa
|
||||
return fetchWithRetry<SessionSummary[]>(`${API_BASE}/summaries?${params}`, { signal })
|
||||
}
|
||||
|
||||
export async function fetchStats(): Promise<Stats> {
|
||||
return fetchWithRetry<Stats>(`${API_BASE}/stats`)
|
||||
export async function fetchStats(project?: string | null): Promise<Stats> {
|
||||
const params = new URLSearchParams()
|
||||
if (project) params.append('project', project)
|
||||
const query = params.toString()
|
||||
return fetchWithRetry<Stats>(`${API_BASE}/stats${query ? '?' + query : ''}`)
|
||||
}
|
||||
|
||||
export async function fetchProjects(): Promise<string[]> {
|
||||
|
||||
Reference in New Issue
Block a user