mirror of
https://github.com/lukaszraczylo/claude-mnemonic.git
synced 2026-06-11 00:09:28 +00:00
Add the statusline. Fix the installation.
This commit is contained in:
+8
-1
@@ -12,6 +12,13 @@ before:
|
|||||||
- bash -c "cd ui && npm ci --silent && npm run build"
|
- bash -c "cd ui && npm ci --silent && npm run build"
|
||||||
- bash -c "rm -rf internal/worker/static && mkdir -p internal/worker/static && cp -r ui/dist/* internal/worker/static/"
|
- bash -c "rm -rf internal/worker/static && mkdir -p internal/worker/static && cp -r ui/dist/* internal/worker/static/"
|
||||||
|
|
||||||
|
# Generate versioned plugin configuration files (GoReleaser Pro feature)
|
||||||
|
template_files:
|
||||||
|
- src: plugin/.claude-plugin/plugin.json.tpl
|
||||||
|
dst: .claude-plugin/plugin.json
|
||||||
|
- src: plugin/.claude-plugin/marketplace.json.tpl
|
||||||
|
dst: .claude-plugin/marketplace.json
|
||||||
|
|
||||||
builds:
|
builds:
|
||||||
# Worker service
|
# Worker service
|
||||||
- id: worker
|
- id: worker
|
||||||
@@ -211,7 +218,7 @@ archives:
|
|||||||
{{- .Os }}_
|
{{- .Os }}_
|
||||||
{{- .Arch }}
|
{{- .Arch }}
|
||||||
files:
|
files:
|
||||||
- src: plugin/.claude-plugin/*
|
- src: dist/.claude-plugin/*
|
||||||
dst: .claude-plugin
|
dst: .claude-plugin
|
||||||
strip_parent: true
|
strip_parent: true
|
||||||
- src: plugin/hooks/hooks.json
|
- src: plugin/hooks/hooks.json
|
||||||
|
|||||||
@@ -45,6 +45,7 @@ hooks:
|
|||||||
go build $(BUILD_TAGS) $(LDFLAGS) -o $(BUILD_DIR)/hooks/post-tool-use ./cmd/hooks/post-tool-use
|
go build $(BUILD_TAGS) $(LDFLAGS) -o $(BUILD_DIR)/hooks/post-tool-use ./cmd/hooks/post-tool-use
|
||||||
go build $(BUILD_TAGS) $(LDFLAGS) -o $(BUILD_DIR)/hooks/subagent-stop ./cmd/hooks/subagent-stop
|
go build $(BUILD_TAGS) $(LDFLAGS) -o $(BUILD_DIR)/hooks/subagent-stop ./cmd/hooks/subagent-stop
|
||||||
go build $(BUILD_TAGS) $(LDFLAGS) -o $(BUILD_DIR)/hooks/stop ./cmd/hooks/stop
|
go build $(BUILD_TAGS) $(LDFLAGS) -o $(BUILD_DIR)/hooks/stop ./cmd/hooks/stop
|
||||||
|
go build $(BUILD_TAGS) $(LDFLAGS) -o $(BUILD_DIR)/hooks/statusline ./cmd/hooks/statusline
|
||||||
|
|
||||||
# Build MCP server
|
# Build MCP server
|
||||||
mcp:
|
mcp:
|
||||||
@@ -65,6 +66,7 @@ build-linux:
|
|||||||
GOOS=linux GOARCH=amd64 go build $(LDFLAGS) -o $(BUILD_DIR)/linux-amd64/hooks/post-tool-use ./cmd/hooks/post-tool-use
|
GOOS=linux GOARCH=amd64 go build $(LDFLAGS) -o $(BUILD_DIR)/linux-amd64/hooks/post-tool-use ./cmd/hooks/post-tool-use
|
||||||
GOOS=linux GOARCH=amd64 go build $(LDFLAGS) -o $(BUILD_DIR)/linux-amd64/hooks/subagent-stop ./cmd/hooks/subagent-stop
|
GOOS=linux GOARCH=amd64 go build $(LDFLAGS) -o $(BUILD_DIR)/linux-amd64/hooks/subagent-stop ./cmd/hooks/subagent-stop
|
||||||
GOOS=linux GOARCH=amd64 go build $(LDFLAGS) -o $(BUILD_DIR)/linux-amd64/hooks/stop ./cmd/hooks/stop
|
GOOS=linux GOARCH=amd64 go build $(LDFLAGS) -o $(BUILD_DIR)/linux-amd64/hooks/stop ./cmd/hooks/stop
|
||||||
|
GOOS=linux GOARCH=amd64 go build $(LDFLAGS) -o $(BUILD_DIR)/linux-amd64/hooks/statusline ./cmd/hooks/statusline
|
||||||
|
|
||||||
build-darwin:
|
build-darwin:
|
||||||
@echo "Building for macOS..."
|
@echo "Building for macOS..."
|
||||||
@@ -83,6 +85,8 @@ build-darwin:
|
|||||||
GOOS=darwin GOARCH=arm64 go build $(LDFLAGS) -o $(BUILD_DIR)/darwin-arm64/hooks/subagent-stop ./cmd/hooks/subagent-stop
|
GOOS=darwin GOARCH=arm64 go build $(LDFLAGS) -o $(BUILD_DIR)/darwin-arm64/hooks/subagent-stop ./cmd/hooks/subagent-stop
|
||||||
GOOS=darwin GOARCH=amd64 go build $(LDFLAGS) -o $(BUILD_DIR)/darwin-amd64/hooks/stop ./cmd/hooks/stop
|
GOOS=darwin GOARCH=amd64 go build $(LDFLAGS) -o $(BUILD_DIR)/darwin-amd64/hooks/stop ./cmd/hooks/stop
|
||||||
GOOS=darwin GOARCH=arm64 go build $(LDFLAGS) -o $(BUILD_DIR)/darwin-arm64/hooks/stop ./cmd/hooks/stop
|
GOOS=darwin GOARCH=arm64 go build $(LDFLAGS) -o $(BUILD_DIR)/darwin-arm64/hooks/stop ./cmd/hooks/stop
|
||||||
|
GOOS=darwin GOARCH=amd64 go build $(LDFLAGS) -o $(BUILD_DIR)/darwin-amd64/hooks/statusline ./cmd/hooks/statusline
|
||||||
|
GOOS=darwin GOARCH=arm64 go build $(LDFLAGS) -o $(BUILD_DIR)/darwin-arm64/hooks/statusline ./cmd/hooks/statusline
|
||||||
|
|
||||||
build-windows:
|
build-windows:
|
||||||
@echo "Building for Windows..."
|
@echo "Building for Windows..."
|
||||||
@@ -94,6 +98,7 @@ build-windows:
|
|||||||
GOOS=windows GOARCH=amd64 go build $(LDFLAGS) -o $(BUILD_DIR)/windows-amd64/hooks/post-tool-use.exe ./cmd/hooks/post-tool-use
|
GOOS=windows GOARCH=amd64 go build $(LDFLAGS) -o $(BUILD_DIR)/windows-amd64/hooks/post-tool-use.exe ./cmd/hooks/post-tool-use
|
||||||
GOOS=windows GOARCH=amd64 go build $(LDFLAGS) -o $(BUILD_DIR)/windows-amd64/hooks/subagent-stop.exe ./cmd/hooks/subagent-stop
|
GOOS=windows GOARCH=amd64 go build $(LDFLAGS) -o $(BUILD_DIR)/windows-amd64/hooks/subagent-stop.exe ./cmd/hooks/subagent-stop
|
||||||
GOOS=windows GOARCH=amd64 go build $(LDFLAGS) -o $(BUILD_DIR)/windows-amd64/hooks/stop.exe ./cmd/hooks/stop
|
GOOS=windows GOARCH=amd64 go build $(LDFLAGS) -o $(BUILD_DIR)/windows-amd64/hooks/stop.exe ./cmd/hooks/stop
|
||||||
|
GOOS=windows GOARCH=amd64 go build $(LDFLAGS) -o $(BUILD_DIR)/windows-amd64/hooks/statusline.exe ./cmd/hooks/statusline
|
||||||
|
|
||||||
# Stop any running worker
|
# Stop any running worker
|
||||||
stop-worker:
|
stop-worker:
|
||||||
@@ -107,8 +112,8 @@ stop-worker:
|
|||||||
start-worker:
|
start-worker:
|
||||||
@echo "Starting worker..."
|
@echo "Starting worker..."
|
||||||
@# Prefer cache directory (where Claude Code looks), fall back to marketplaces
|
@# Prefer cache directory (where Claude Code looks), fall back to marketplaces
|
||||||
@if [ -f "$(HOME)/.claude/plugins/cache/claude-mnemonic/claude-mnemonic/1.0.0/worker" ]; then \
|
@if [ -f "$(HOME)/.claude/plugins/cache/claude-mnemonic/claude-mnemonic/$(VERSION)/worker" ]; then \
|
||||||
nohup $(HOME)/.claude/plugins/cache/claude-mnemonic/claude-mnemonic/1.0.0/worker > /tmp/claude-mnemonic-worker.log 2>&1 & \
|
nohup $(HOME)/.claude/plugins/cache/claude-mnemonic/claude-mnemonic/$(VERSION)/worker > /tmp/claude-mnemonic-worker.log 2>&1 & \
|
||||||
else \
|
else \
|
||||||
nohup $(HOME)/.claude/plugins/marketplaces/claude-mnemonic/worker > /tmp/claude-mnemonic-worker.log 2>&1 & \
|
nohup $(HOME)/.claude/plugins/marketplaces/claude-mnemonic/worker > /tmp/claude-mnemonic-worker.log 2>&1 & \
|
||||||
fi
|
fi
|
||||||
@@ -132,22 +137,11 @@ install: build stop-worker
|
|||||||
cp $(BUILD_DIR)/mcp-server $(HOME)/.claude/plugins/marketplaces/claude-mnemonic/
|
cp $(BUILD_DIR)/mcp-server $(HOME)/.claude/plugins/marketplaces/claude-mnemonic/
|
||||||
cp $(BUILD_DIR)/hooks/* $(HOME)/.claude/plugins/marketplaces/claude-mnemonic/hooks/
|
cp $(BUILD_DIR)/hooks/* $(HOME)/.claude/plugins/marketplaces/claude-mnemonic/hooks/
|
||||||
cp $(PLUGIN_DIR)/hooks/hooks.json $(HOME)/.claude/plugins/marketplaces/claude-mnemonic/hooks/
|
cp $(PLUGIN_DIR)/hooks/hooks.json $(HOME)/.claude/plugins/marketplaces/claude-mnemonic/hooks/
|
||||||
cp $(PLUGIN_DIR)/.claude-plugin/plugin.json $(HOME)/.claude/plugins/marketplaces/claude-mnemonic/.claude-plugin/
|
@# Update plugin.json and marketplace.json with current version to prevent stale version directories
|
||||||
cp $(PLUGIN_DIR)/.claude-plugin/marketplace.json $(HOME)/.claude/plugins/marketplaces/claude-mnemonic/.claude-plugin/
|
@sed 's/"version": "[^"]*"/"version": "$(VERSION)"/g' $(PLUGIN_DIR)/.claude-plugin/plugin.json > $(HOME)/.claude/plugins/marketplaces/claude-mnemonic/.claude-plugin/plugin.json
|
||||||
@# Also install to cache directory (where Claude Code looks for plugins)
|
@sed 's/"version": "[^"]*"/"version": "$(VERSION)"/g' $(PLUGIN_DIR)/.claude-plugin/marketplace.json > $(HOME)/.claude/plugins/marketplaces/claude-mnemonic/.claude-plugin/marketplace.json
|
||||||
@if [ -d "$(HOME)/.claude/plugins/cache/claude-mnemonic" ]; then \
|
|
||||||
echo "Updating plugin in cache directory..."; \
|
|
||||||
CACHE_DIR=$$(find $(HOME)/.claude/plugins/cache/claude-mnemonic -type d -name "hooks" -exec dirname {} \; 2>/dev/null | head -1); \
|
|
||||||
if [ -n "$$CACHE_DIR" ]; then \
|
|
||||||
cp $(BUILD_DIR)/worker "$$CACHE_DIR/"; \
|
|
||||||
cp $(BUILD_DIR)/mcp-server "$$CACHE_DIR/"; \
|
|
||||||
cp $(BUILD_DIR)/hooks/* "$$CACHE_DIR/hooks/"; \
|
|
||||||
cp $(PLUGIN_DIR)/hooks/hooks.json "$$CACHE_DIR/hooks/"; \
|
|
||||||
echo "Cache directory updated: $$CACHE_DIR"; \
|
|
||||||
fi; \
|
|
||||||
fi
|
|
||||||
@echo "Registering plugin with Claude Code..."
|
@echo "Registering plugin with Claude Code..."
|
||||||
@./scripts/register-plugin.sh
|
@./scripts/register-plugin.sh "$(VERSION)"
|
||||||
@$(MAKE) start-worker
|
@$(MAKE) start-worker
|
||||||
@echo "Installation complete!"
|
@echo "Installation complete!"
|
||||||
|
|
||||||
|
|||||||
@@ -100,6 +100,7 @@ cosign verify-blob \
|
|||||||
| **Project Isolation** | Each project has its own knowledge base |
|
| **Project Isolation** | Each project has its own knowledge base |
|
||||||
| **Global Patterns** | Best practices are shared across all projects |
|
| **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 |
|
||||||
|
| **Live Statusline** | Real-time metrics in Claude Code: `[mnemonic] ● served:42 | project:28 memories` |
|
||||||
| **Web Dashboard** | Browse and manage memories at `localhost:37777` |
|
| **Web Dashboard** | Browse and manage memories at `localhost:37777` |
|
||||||
|
|
||||||
### How knowledge flows
|
### How knowledge flows
|
||||||
|
|||||||
@@ -0,0 +1,307 @@
|
|||||||
|
// Package main provides the statusline hook for Claude Code.
|
||||||
|
// This binary outputs a status line showing claude-mnemonic metrics.
|
||||||
|
package main
|
||||||
|
|
||||||
|
import (
|
||||||
|
"encoding/json"
|
||||||
|
"fmt"
|
||||||
|
"io"
|
||||||
|
"net/http"
|
||||||
|
"net/url"
|
||||||
|
"os"
|
||||||
|
"time"
|
||||||
|
|
||||||
|
"github.com/lukaszraczylo/claude-mnemonic/pkg/hooks"
|
||||||
|
)
|
||||||
|
|
||||||
|
// StatusInput is the JSON input from Claude Code's statusline feature.
|
||||||
|
type StatusInput struct {
|
||||||
|
HookEventName string `json:"hook_event_name"`
|
||||||
|
SessionID string `json:"session_id"`
|
||||||
|
CWD string `json:"cwd"`
|
||||||
|
Model struct {
|
||||||
|
ID string `json:"id"`
|
||||||
|
DisplayName string `json:"display_name"`
|
||||||
|
} `json:"model"`
|
||||||
|
Workspace struct {
|
||||||
|
CurrentDir string `json:"current_dir"`
|
||||||
|
ProjectDir string `json:"project_dir"`
|
||||||
|
} `json:"workspace"`
|
||||||
|
Version string `json:"version"`
|
||||||
|
Cost struct {
|
||||||
|
TotalCostUSD float64 `json:"total_cost_usd"`
|
||||||
|
TotalDurationMS int64 `json:"total_duration_ms"`
|
||||||
|
TotalAPIDurationMS int64 `json:"total_api_duration_ms"`
|
||||||
|
TotalLinesAdded int `json:"total_lines_added"`
|
||||||
|
TotalLinesRemoved int `json:"total_lines_removed"`
|
||||||
|
} `json:"cost"`
|
||||||
|
ContextWindow struct {
|
||||||
|
TotalInputTokens int `json:"total_input_tokens"`
|
||||||
|
TotalOutputTokens int `json:"total_output_tokens"`
|
||||||
|
ContextWindowSize int `json:"context_window_size"`
|
||||||
|
} `json:"context_window"`
|
||||||
|
}
|
||||||
|
|
||||||
|
// WorkerStats is the response from the worker's /api/stats endpoint.
|
||||||
|
type WorkerStats struct {
|
||||||
|
Uptime string `json:"uptime"`
|
||||||
|
ActiveSessions int `json:"activeSessions"`
|
||||||
|
QueueDepth int `json:"queueDepth"`
|
||||||
|
IsProcessing bool `json:"isProcessing"`
|
||||||
|
ConnectedClients int `json:"connectedClients"`
|
||||||
|
SessionsToday int `json:"sessionsToday"`
|
||||||
|
Ready bool `json:"ready"`
|
||||||
|
Project string `json:"project,omitempty"`
|
||||||
|
ProjectObservations int `json:"projectObservations,omitempty"`
|
||||||
|
Retrieval struct {
|
||||||
|
TotalRequests int64 `json:"TotalRequests"`
|
||||||
|
ObservationsServed int64 `json:"ObservationsServed"`
|
||||||
|
SearchRequests int64 `json:"SearchRequests"`
|
||||||
|
ContextInjections int64 `json:"ContextInjections"`
|
||||||
|
} `json:"retrieval"`
|
||||||
|
}
|
||||||
|
|
||||||
|
// ANSI color codes
|
||||||
|
const (
|
||||||
|
colorReset = "\033[0m"
|
||||||
|
colorGreen = "\033[32m"
|
||||||
|
colorYellow = "\033[33m"
|
||||||
|
colorCyan = "\033[36m"
|
||||||
|
colorGray = "\033[90m"
|
||||||
|
colorRed = "\033[31m"
|
||||||
|
)
|
||||||
|
|
||||||
|
func main() {
|
||||||
|
// Read input from stdin
|
||||||
|
inputData, err := io.ReadAll(os.Stdin)
|
||||||
|
if err != nil {
|
||||||
|
// On error, output minimal status
|
||||||
|
fmt.Println(formatOffline())
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
var input StatusInput
|
||||||
|
if err := json.Unmarshal(inputData, &input); err != nil {
|
||||||
|
fmt.Println(formatOffline())
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// Determine project directory
|
||||||
|
projectDir := input.Workspace.ProjectDir
|
||||||
|
if projectDir == "" {
|
||||||
|
projectDir = input.Workspace.CurrentDir
|
||||||
|
}
|
||||||
|
if projectDir == "" {
|
||||||
|
projectDir = input.CWD
|
||||||
|
}
|
||||||
|
|
||||||
|
// Generate project ID
|
||||||
|
project := ""
|
||||||
|
if projectDir != "" {
|
||||||
|
project = hooks.ProjectIDWithName(projectDir)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Get worker stats
|
||||||
|
stats := getWorkerStats(project)
|
||||||
|
|
||||||
|
// Format and output statusline
|
||||||
|
fmt.Println(formatStatusLine(stats, input))
|
||||||
|
}
|
||||||
|
|
||||||
|
// getWorkerStats fetches stats from the worker service.
|
||||||
|
func getWorkerStats(project string) *WorkerStats {
|
||||||
|
port := hooks.GetWorkerPort()
|
||||||
|
|
||||||
|
// Build URL with optional project parameter
|
||||||
|
endpoint := fmt.Sprintf("http://127.0.0.1:%d/api/stats", port)
|
||||||
|
if project != "" {
|
||||||
|
endpoint += "?project=" + url.QueryEscape(project)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Create HTTP client with short timeout (statusline must be fast)
|
||||||
|
client := &http.Client{Timeout: 100 * time.Millisecond}
|
||||||
|
|
||||||
|
resp, err := client.Get(endpoint)
|
||||||
|
if err != nil {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
defer resp.Body.Close()
|
||||||
|
|
||||||
|
if resp.StatusCode != http.StatusOK {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
var stats WorkerStats
|
||||||
|
if err := json.NewDecoder(resp.Body).Decode(&stats); err != nil {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
return &stats
|
||||||
|
}
|
||||||
|
|
||||||
|
// formatStatusLine formats the status line output.
|
||||||
|
func formatStatusLine(stats *WorkerStats, input StatusInput) string {
|
||||||
|
// Check if colors are enabled (default: yes, unless TERM is dumb or NO_COLOR is set)
|
||||||
|
useColors := os.Getenv("NO_COLOR") == "" && os.Getenv("TERM") != "dumb"
|
||||||
|
if os.Getenv("CLAUDE_MNEMONIC_STATUSLINE_COLORS") == "false" {
|
||||||
|
useColors = false
|
||||||
|
} else if os.Getenv("CLAUDE_MNEMONIC_STATUSLINE_COLORS") == "true" {
|
||||||
|
useColors = true
|
||||||
|
}
|
||||||
|
|
||||||
|
// Check format preference
|
||||||
|
format := os.Getenv("CLAUDE_MNEMONIC_STATUSLINE_FORMAT")
|
||||||
|
if format == "" {
|
||||||
|
format = "default"
|
||||||
|
}
|
||||||
|
|
||||||
|
if stats == nil {
|
||||||
|
return formatOfflineColored(useColors)
|
||||||
|
}
|
||||||
|
|
||||||
|
if !stats.Ready {
|
||||||
|
return formatStartingColored(useColors)
|
||||||
|
}
|
||||||
|
|
||||||
|
switch format {
|
||||||
|
case "compact":
|
||||||
|
return formatCompact(stats, useColors)
|
||||||
|
case "minimal":
|
||||||
|
return formatMinimal(stats, useColors)
|
||||||
|
default:
|
||||||
|
return formatDefault(stats, useColors)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// formatDefault returns the default status line format.
|
||||||
|
func formatDefault(stats *WorkerStats, useColors bool) string {
|
||||||
|
// [mnemonic] ● served:42 | injected:5 | searches:3 | project:28 memories
|
||||||
|
var prefix, indicator, reset string
|
||||||
|
if useColors {
|
||||||
|
prefix = colorCyan + "[mnemonic]" + colorReset
|
||||||
|
indicator = colorGreen + "●" + colorReset
|
||||||
|
reset = colorReset
|
||||||
|
} else {
|
||||||
|
prefix = "[mnemonic]"
|
||||||
|
indicator = "●"
|
||||||
|
}
|
||||||
|
|
||||||
|
// Build status parts with clear labels
|
||||||
|
parts := []string{}
|
||||||
|
|
||||||
|
// Total memories served to Claude this session
|
||||||
|
parts = append(parts, fmt.Sprintf("served:%d", stats.Retrieval.ObservationsServed))
|
||||||
|
|
||||||
|
// Context injections (memories auto-loaded at session start)
|
||||||
|
if stats.Retrieval.ContextInjections > 0 {
|
||||||
|
parts = append(parts, fmt.Sprintf("injected:%d", stats.Retrieval.ContextInjections))
|
||||||
|
}
|
||||||
|
|
||||||
|
// Semantic searches performed
|
||||||
|
if stats.Retrieval.SearchRequests > 0 {
|
||||||
|
parts = append(parts, fmt.Sprintf("searches:%d", stats.Retrieval.SearchRequests))
|
||||||
|
}
|
||||||
|
|
||||||
|
// Project-specific memory count
|
||||||
|
if stats.ProjectObservations > 0 {
|
||||||
|
if useColors {
|
||||||
|
parts = append(parts, fmt.Sprintf("%sproject:%d memories%s", colorYellow, stats.ProjectObservations, reset))
|
||||||
|
} else {
|
||||||
|
parts = append(parts, fmt.Sprintf("project:%d memories", stats.ProjectObservations))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Processing indicator
|
||||||
|
if stats.IsProcessing || stats.QueueDepth > 0 {
|
||||||
|
if useColors {
|
||||||
|
parts = append(parts, colorYellow+"processing..."+colorReset)
|
||||||
|
} else {
|
||||||
|
parts = append(parts, "processing...")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
result := prefix + " " + indicator
|
||||||
|
for i, part := range parts {
|
||||||
|
if i == 0 {
|
||||||
|
result += " " + part
|
||||||
|
} else {
|
||||||
|
result += " | " + part
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return result
|
||||||
|
}
|
||||||
|
|
||||||
|
// formatCompact returns a compact status line.
|
||||||
|
func formatCompact(stats *WorkerStats, useColors bool) string {
|
||||||
|
// [m] ● 42/5/3 (28)
|
||||||
|
var prefix, indicator string
|
||||||
|
if useColors {
|
||||||
|
prefix = colorCyan + "[m]" + colorReset
|
||||||
|
indicator = colorGreen + "●" + colorReset
|
||||||
|
} else {
|
||||||
|
prefix = "[m]"
|
||||||
|
indicator = "●"
|
||||||
|
}
|
||||||
|
|
||||||
|
result := fmt.Sprintf("%s %s %d/%d/%d",
|
||||||
|
prefix, indicator,
|
||||||
|
stats.Retrieval.ObservationsServed,
|
||||||
|
stats.Retrieval.ContextInjections,
|
||||||
|
stats.Retrieval.SearchRequests,
|
||||||
|
)
|
||||||
|
|
||||||
|
if stats.ProjectObservations > 0 {
|
||||||
|
result += fmt.Sprintf(" (%d)", stats.ProjectObservations)
|
||||||
|
}
|
||||||
|
|
||||||
|
if stats.IsProcessing || stats.QueueDepth > 0 {
|
||||||
|
if useColors {
|
||||||
|
result += " " + colorYellow + "⚙" + colorReset
|
||||||
|
} else {
|
||||||
|
result += " ⚙"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return result
|
||||||
|
}
|
||||||
|
|
||||||
|
// formatMinimal returns a minimal status line.
|
||||||
|
func formatMinimal(stats *WorkerStats, useColors bool) string {
|
||||||
|
// ● 42 obs
|
||||||
|
var indicator string
|
||||||
|
if useColors {
|
||||||
|
indicator = colorGreen + "●" + colorReset
|
||||||
|
} else {
|
||||||
|
indicator = "●"
|
||||||
|
}
|
||||||
|
|
||||||
|
result := fmt.Sprintf("%s %d", indicator, stats.Retrieval.ObservationsServed)
|
||||||
|
|
||||||
|
if stats.ProjectObservations > 0 {
|
||||||
|
result += fmt.Sprintf("/%d", stats.ProjectObservations)
|
||||||
|
}
|
||||||
|
|
||||||
|
return result
|
||||||
|
}
|
||||||
|
|
||||||
|
// formatOffline returns the offline status.
|
||||||
|
func formatOffline() string {
|
||||||
|
return formatOfflineColored(true)
|
||||||
|
}
|
||||||
|
|
||||||
|
// formatOfflineColored returns the offline status with optional colors.
|
||||||
|
func formatOfflineColored(useColors bool) string {
|
||||||
|
if useColors {
|
||||||
|
return colorCyan + "[mnemonic]" + colorReset + " " + colorGray + "○" + colorReset
|
||||||
|
}
|
||||||
|
return "[mnemonic] ○"
|
||||||
|
}
|
||||||
|
|
||||||
|
// formatStartingColored returns the starting status with optional colors.
|
||||||
|
func formatStartingColored(useColors bool) string {
|
||||||
|
if useColors {
|
||||||
|
return colorCyan + "[mnemonic]" + colorReset + " " + colorYellow + "◐" + colorReset + " starting"
|
||||||
|
}
|
||||||
|
return "[mnemonic] ◐ starting"
|
||||||
|
}
|
||||||
+1
-1
@@ -431,7 +431,7 @@ const features = [
|
|||||||
{ 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-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-globe', title: 'Shared best practices', description: 'Security patterns, performance tips, and universal learnings automatically available across all your projects.' },
|
{ icon: 'fas fa-globe', title: 'Shared best practices', description: 'Security patterns, performance tips, and universal learnings automatically available across all your projects.' },
|
||||||
{ icon: 'fas fa-search', title: 'Finds what matters', description: 'Semantic search finds relevant memories even when you don\'t remember the exact words. "That auth thing" just works.' },
|
{ icon: 'fas fa-search', title: 'Finds what matters', description: 'Semantic search finds relevant memories even when you don\'t remember the exact words. "That auth thing" just works.' },
|
||||||
{ icon: 'fas fa-credit-card', title: 'No API keys needed', description: 'Uses Claude Code CLI - works with your existing Claude Pro/Max subscription. No separate API costs.' },
|
{ icon: 'fas fa-chart-line', title: 'Live statusline', description: 'Real-time metrics right in Claude Code: memories served, searches performed, and project memory count at a glance.' },
|
||||||
{ icon: 'fas fa-lock', title: '100% private', description: 'Your code context never leaves your machine. No telemetry. No cloud sync. Your memories are yours.' },
|
{ icon: 'fas fa-lock', title: '100% private', description: 'Your code context never leaves your machine. No telemetry. No cloud sync. Your memories are yours.' },
|
||||||
]
|
]
|
||||||
|
|
||||||
|
|||||||
@@ -74,12 +74,16 @@ func (s *ObservationStore) StoreObservation(ctx context.Context, sdkSessionID, p
|
|||||||
|
|
||||||
id, _ := result.LastInsertId()
|
id, _ := result.LastInsertId()
|
||||||
|
|
||||||
// Cleanup old observations beyond the limit for this project
|
// Cleanup old observations beyond the limit for this project (async to not block handler)
|
||||||
if project != "" {
|
if project != "" {
|
||||||
deletedIDs, _ := s.CleanupOldObservations(ctx, project)
|
go func(proj string) {
|
||||||
if len(deletedIDs) > 0 && s.cleanupFunc != nil {
|
cleanupCtx, cancel := context.WithTimeout(context.Background(), 10*time.Second)
|
||||||
s.cleanupFunc(ctx, deletedIDs)
|
defer cancel()
|
||||||
}
|
deletedIDs, _ := s.CleanupOldObservations(cleanupCtx, proj)
|
||||||
|
if len(deletedIDs) > 0 && s.cleanupFunc != nil {
|
||||||
|
s.cleanupFunc(cleanupCtx, deletedIDs)
|
||||||
|
}
|
||||||
|
}(project)
|
||||||
}
|
}
|
||||||
|
|
||||||
return id, nowEpoch, nil
|
return id, nowEpoch, nil
|
||||||
|
|||||||
@@ -51,11 +51,15 @@ func (s *PromptStore) SaveUserPromptWithMatches(ctx context.Context, claudeSessi
|
|||||||
|
|
||||||
id, _ := result.LastInsertId()
|
id, _ := result.LastInsertId()
|
||||||
|
|
||||||
// Cleanup old prompts beyond the global limit
|
// Cleanup old prompts beyond the global limit (async to not block handler)
|
||||||
deletedIDs, _ := s.CleanupOldPrompts(ctx)
|
go func() {
|
||||||
if len(deletedIDs) > 0 && s.cleanupFunc != nil {
|
cleanupCtx, cancel := context.WithTimeout(context.Background(), 10*time.Second)
|
||||||
s.cleanupFunc(ctx, deletedIDs)
|
defer cancel()
|
||||||
}
|
deletedIDs, _ := s.CleanupOldPrompts(cleanupCtx)
|
||||||
|
if len(deletedIDs) > 0 && s.cleanupFunc != nil {
|
||||||
|
s.cleanupFunc(cleanupCtx, deletedIDs)
|
||||||
|
}
|
||||||
|
}()
|
||||||
|
|
||||||
return id, nil
|
return id, nil
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -514,7 +514,7 @@ func (s *Service) handleGetStats(w http.ResponseWriter, r *http.Request) {
|
|||||||
retrievalStats := s.GetRetrievalStats()
|
retrievalStats := s.GetRetrievalStats()
|
||||||
sessionsToday, _ := s.sessionStore.GetSessionsToday(r.Context())
|
sessionsToday, _ := s.sessionStore.GetSessionsToday(r.Context())
|
||||||
|
|
||||||
writeJSON(w, map[string]interface{}{
|
response := map[string]interface{}{
|
||||||
"uptime": time.Since(s.startTime).String(),
|
"uptime": time.Since(s.startTime).String(),
|
||||||
"activeSessions": s.sessionManager.GetActiveSessionCount(),
|
"activeSessions": s.sessionManager.GetActiveSessionCount(),
|
||||||
"queueDepth": s.sessionManager.GetTotalQueueDepth(),
|
"queueDepth": s.sessionManager.GetTotalQueueDepth(),
|
||||||
@@ -522,7 +522,19 @@ func (s *Service) handleGetStats(w http.ResponseWriter, r *http.Request) {
|
|||||||
"connectedClients": s.sseBroadcaster.ClientCount(),
|
"connectedClients": s.sseBroadcaster.ClientCount(),
|
||||||
"sessionsToday": sessionsToday,
|
"sessionsToday": sessionsToday,
|
||||||
"retrieval": retrievalStats,
|
"retrieval": retrievalStats,
|
||||||
})
|
"ready": s.ready.Load(),
|
||||||
|
}
|
||||||
|
|
||||||
|
// Include project-specific observation count if project is specified
|
||||||
|
if project := r.URL.Query().Get("project"); project != "" {
|
||||||
|
count, err := s.observationStore.GetObservationCount(r.Context(), project)
|
||||||
|
if err == nil {
|
||||||
|
response["projectObservations"] = count
|
||||||
|
response["project"] = project
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
writeJSON(w, response)
|
||||||
}
|
}
|
||||||
|
|
||||||
// handleGetRetrievalStats returns detailed retrieval statistics.
|
// handleGetRetrievalStats returns detailed retrieval statistics.
|
||||||
|
|||||||
@@ -6,10 +6,17 @@ import (
|
|||||||
"fmt"
|
"fmt"
|
||||||
"net/http"
|
"net/http"
|
||||||
"sync"
|
"sync"
|
||||||
|
"time"
|
||||||
|
|
||||||
"github.com/rs/zerolog/log"
|
"github.com/rs/zerolog/log"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
const (
|
||||||
|
// WriteTimeout is the timeout for writing to SSE clients.
|
||||||
|
// Prevents blocking on stale connections.
|
||||||
|
WriteTimeout = 2 * time.Second
|
||||||
|
)
|
||||||
|
|
||||||
// Client represents a connected SSE client.
|
// Client represents a connected SSE client.
|
||||||
type Client struct {
|
type Client struct {
|
||||||
ID string
|
ID string
|
||||||
@@ -101,6 +108,7 @@ func (b *Broadcaster) removeClientByID(id string) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Broadcast sends a message to all connected clients.
|
// Broadcast sends a message to all connected clients.
|
||||||
|
// Uses non-blocking writes with timeout to prevent stale connections from blocking.
|
||||||
func (b *Broadcaster) Broadcast(data interface{}) {
|
func (b *Broadcaster) Broadcast(data interface{}) {
|
||||||
jsonData, err := json.Marshal(data)
|
jsonData, err := json.Marshal(data)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
@@ -117,30 +125,67 @@ func (b *Broadcaster) Broadcast(data interface{}) {
|
|||||||
}
|
}
|
||||||
b.mu.RUnlock()
|
b.mu.RUnlock()
|
||||||
|
|
||||||
// Track dead clients for removal
|
if len(clients) == 0 {
|
||||||
var deadClients []*Client
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// Use a channel to collect dead clients from concurrent writes
|
||||||
|
deadClientsCh := make(chan string, len(clients))
|
||||||
|
var wg sync.WaitGroup
|
||||||
|
|
||||||
for _, client := range clients {
|
for _, client := range clients {
|
||||||
select {
|
select {
|
||||||
case <-client.Done:
|
case <-client.Done:
|
||||||
continue
|
continue
|
||||||
default:
|
default:
|
||||||
_, err := client.Writer.Write([]byte(message))
|
wg.Add(1)
|
||||||
if err != nil {
|
go func(c *Client) {
|
||||||
log.Debug().
|
defer wg.Done()
|
||||||
Str("clientId", client.ID).
|
b.writeToClient(c, message, deadClientsCh)
|
||||||
Err(err).
|
}(client)
|
||||||
Msg("Failed to write to SSE client, marking for removal")
|
|
||||||
deadClients = append(deadClients, client)
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
client.Flusher.Flush()
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Remove dead clients outside the iteration
|
// Wait for all writes to complete (with their individual timeouts)
|
||||||
for _, client := range deadClients {
|
wg.Wait()
|
||||||
b.removeClientByID(client.ID)
|
close(deadClientsCh)
|
||||||
|
|
||||||
|
// Remove dead clients
|
||||||
|
for clientID := range deadClientsCh {
|
||||||
|
b.removeClientByID(clientID)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// writeToClient writes a message to a single client with timeout.
|
||||||
|
func (b *Broadcaster) writeToClient(client *Client, message string, deadCh chan<- string) {
|
||||||
|
// Use a timeout channel to prevent blocking on stale connections
|
||||||
|
done := make(chan struct{})
|
||||||
|
|
||||||
|
go func() {
|
||||||
|
defer close(done)
|
||||||
|
_, err := client.Writer.Write([]byte(message))
|
||||||
|
if err != nil {
|
||||||
|
log.Debug().
|
||||||
|
Str("clientId", client.ID).
|
||||||
|
Err(err).
|
||||||
|
Msg("Failed to write to SSE client, marking for removal")
|
||||||
|
deadCh <- client.ID
|
||||||
|
return
|
||||||
|
}
|
||||||
|
client.Flusher.Flush()
|
||||||
|
}()
|
||||||
|
|
||||||
|
select {
|
||||||
|
case <-done:
|
||||||
|
// Write completed successfully
|
||||||
|
case <-time.After(WriteTimeout):
|
||||||
|
log.Warn().
|
||||||
|
Str("clientId", client.ID).
|
||||||
|
Dur("timeout", WriteTimeout).
|
||||||
|
Msg("SSE write timed out, marking client for removal")
|
||||||
|
deadCh <- client.ID
|
||||||
|
case <-client.Done:
|
||||||
|
// Client disconnected during write
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -0,0 +1,22 @@
|
|||||||
|
{
|
||||||
|
"$schema": "https://anthropic.com/claude-code/marketplace.schema.json",
|
||||||
|
"name": "claude-mnemonic",
|
||||||
|
"version": "{{ .Version }}",
|
||||||
|
"description": "Persistent memory system for Claude Code - stores observations, session summaries, and user prompts with semantic search",
|
||||||
|
"owner": {
|
||||||
|
"name": "lukaszraczylo",
|
||||||
|
"email": "lukaszraczylo@users.noreply.github.com"
|
||||||
|
},
|
||||||
|
"plugins": [
|
||||||
|
{
|
||||||
|
"name": "claude-mnemonic",
|
||||||
|
"description": "Persistent memory system for Claude Code - Go implementation with SQLite and ChromaDB",
|
||||||
|
"version": "{{ .Version }}",
|
||||||
|
"author": {
|
||||||
|
"name": "lukaszraczylo"
|
||||||
|
},
|
||||||
|
"source": "./",
|
||||||
|
"category": "productivity"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
@@ -0,0 +1,9 @@
|
|||||||
|
{
|
||||||
|
"name": "claude-mnemonic",
|
||||||
|
"version": "{{ .Version }}",
|
||||||
|
"description": "Persistent memory system for Claude Code - Go implementation with SQLite and ChromaDB",
|
||||||
|
"author": {
|
||||||
|
"name": "lukaszraczylo",
|
||||||
|
"email": "lukaszraczylo@users.noreply.github.com"
|
||||||
|
}
|
||||||
|
}
|
||||||
+44
-2
@@ -27,9 +27,28 @@ function Write-Error { param($Message) Write-Host "[ERROR] $Message" -Foreground
|
|||||||
|
|
||||||
function Get-LatestVersion {
|
function Get-LatestVersion {
|
||||||
try {
|
try {
|
||||||
$release = Invoke-RestMethod -Uri "https://api.github.com/repos/$GitHubRepo/releases/latest"
|
$headers = @{}
|
||||||
|
if ($env:GITHUB_TOKEN) {
|
||||||
|
$headers["Authorization"] = "token $env:GITHUB_TOKEN"
|
||||||
|
}
|
||||||
|
$release = Invoke-RestMethod -Uri "https://api.github.com/repos/$GitHubRepo/releases/latest" -Headers $headers
|
||||||
return $release.tag_name
|
return $release.tag_name
|
||||||
} catch {
|
} catch {
|
||||||
|
$errorMsg = $_.Exception.Message
|
||||||
|
if ($errorMsg -match "rate limit" -or $_.Exception.Response.StatusCode -eq 403) {
|
||||||
|
Write-Host ""
|
||||||
|
Write-Host "[ERROR] GitHub API rate limit exceeded." -ForegroundColor Red
|
||||||
|
Write-Host ""
|
||||||
|
Write-Host "You have a few options:" -ForegroundColor Yellow
|
||||||
|
Write-Host " 1. Wait ~1 hour for the rate limit to reset"
|
||||||
|
Write-Host " 2. Specify a version manually:"
|
||||||
|
Write-Host " `$env:MNEMONIC_VERSION = 'v0.6.1'; irm https://raw.githubusercontent.com/$GitHubRepo/main/scripts/install.ps1 | iex" -ForegroundColor Cyan
|
||||||
|
Write-Host " 3. Use a GitHub token (set `$env:GITHUB_TOKEN)"
|
||||||
|
Write-Host " 4. Clone and build from source:"
|
||||||
|
Write-Host " git clone https://github.com/$GitHubRepo.git" -ForegroundColor Cyan
|
||||||
|
Write-Host " cd claude-mnemonic; make build; make install" -ForegroundColor Cyan
|
||||||
|
exit 1
|
||||||
|
}
|
||||||
Write-Error "Failed to fetch latest version from GitHub: $_"
|
Write-Error "Failed to fetch latest version from GitHub: $_"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -88,6 +107,14 @@ function Register-Plugin {
|
|||||||
|
|
||||||
# Ensure directories exist
|
# Ensure directories exist
|
||||||
New-Item -ItemType Directory -Path "$env:USERPROFILE\.claude\plugins" -Force | Out-Null
|
New-Item -ItemType Directory -Path "$env:USERPROFILE\.claude\plugins" -Force | Out-Null
|
||||||
|
|
||||||
|
# Clean up old cache versions to prevent stale binaries
|
||||||
|
$CacheBase = Split-Path $CachePath -Parent
|
||||||
|
if (Test-Path $CacheBase) {
|
||||||
|
Write-Info "Cleaning up old cache versions..."
|
||||||
|
Get-ChildItem -Path $CacheBase -Directory | Where-Object { $_.Name -ne $VersionClean } | Remove-Item -Recurse -Force -ErrorAction SilentlyContinue
|
||||||
|
}
|
||||||
|
|
||||||
New-Item -ItemType Directory -Path $CachePath -Force | Out-Null
|
New-Item -ItemType Directory -Path $CachePath -Force | Out-Null
|
||||||
|
|
||||||
# Create JSON files if they don't exist
|
# Create JSON files if they don't exist
|
||||||
@@ -132,8 +159,19 @@ function Register-Plugin {
|
|||||||
$Settings | Add-Member -NotePropertyName "enabledPlugins" -NotePropertyValue @{} -Force
|
$Settings | Add-Member -NotePropertyName "enabledPlugins" -NotePropertyValue @{} -Force
|
||||||
}
|
}
|
||||||
$Settings.enabledPlugins | Add-Member -NotePropertyName $PluginKey -NotePropertyValue $true -Force
|
$Settings.enabledPlugins | Add-Member -NotePropertyName $PluginKey -NotePropertyValue $true -Force
|
||||||
|
|
||||||
|
# Configure statusline
|
||||||
|
$StatuslineCmd = "$InstallDir\hooks\statusline.exe"
|
||||||
|
$StatuslineEntry = @{
|
||||||
|
type = "command"
|
||||||
|
command = $StatuslineCmd
|
||||||
|
padding = 0
|
||||||
|
}
|
||||||
|
$Settings | Add-Member -NotePropertyName "statusLine" -NotePropertyValue $StatuslineEntry -Force
|
||||||
|
|
||||||
$Settings | ConvertTo-Json -Depth 10 | Out-File -Encoding UTF8 $SettingsFile
|
$Settings | ConvertTo-Json -Depth 10 | Out-File -Encoding UTF8 $SettingsFile
|
||||||
Write-Success "Plugin enabled in settings.json"
|
Write-Success "Plugin enabled in settings.json"
|
||||||
|
Write-Success "Statusline configured in settings.json"
|
||||||
|
|
||||||
# Update known_marketplaces.json
|
# Update known_marketplaces.json
|
||||||
$Marketplaces = Get-Content $MarketplacesFile -Raw | ConvertFrom-Json
|
$Marketplaces = Get-Content $MarketplacesFile -Raw | ConvertFrom-Json
|
||||||
@@ -194,8 +232,12 @@ function Uninstall-ClaudeMnemonic {
|
|||||||
$Settings = Get-Content $SettingsFile -Raw | ConvertFrom-Json
|
$Settings = Get-Content $SettingsFile -Raw | ConvertFrom-Json
|
||||||
if ($Settings.enabledPlugins) {
|
if ($Settings.enabledPlugins) {
|
||||||
$Settings.enabledPlugins.PSObject.Properties.Remove($PluginKey)
|
$Settings.enabledPlugins.PSObject.Properties.Remove($PluginKey)
|
||||||
$Settings | ConvertTo-Json -Depth 10 | Out-File -Encoding UTF8 $SettingsFile
|
|
||||||
}
|
}
|
||||||
|
# Remove statusline if it's ours
|
||||||
|
if ($Settings.statusLine -and $Settings.statusLine.command -match "claude-mnemonic") {
|
||||||
|
$Settings.PSObject.Properties.Remove("statusLine")
|
||||||
|
}
|
||||||
|
$Settings | ConvertTo-Json -Depth 10 | Out-File -Encoding UTF8 $SettingsFile
|
||||||
}
|
}
|
||||||
if (Test-Path $MarketplacesFile) {
|
if (Test-Path $MarketplacesFile) {
|
||||||
$Marketplaces = Get-Content $MarketplacesFile -Raw | ConvertFrom-Json
|
$Marketplaces = Get-Content $MarketplacesFile -Raw | ConvertFrom-Json
|
||||||
|
|||||||
+62
-7
@@ -81,11 +81,44 @@ detect_platform() {
|
|||||||
|
|
||||||
# Get the latest release version from GitHub
|
# Get the latest release version from GitHub
|
||||||
get_latest_version() {
|
get_latest_version() {
|
||||||
local version
|
local response version curl_opts
|
||||||
version=$(curl -sS "https://api.github.com/repos/${GITHUB_REPO}/releases/latest" | grep '"tag_name":' | sed -E 's/.*"([^"]+)".*/\1/')
|
|
||||||
|
# Use GitHub token if available (higher rate limit)
|
||||||
|
curl_opts=(-sS)
|
||||||
|
if [[ -n "${GITHUB_TOKEN:-}" ]]; then
|
||||||
|
curl_opts+=(-H "Authorization: token ${GITHUB_TOKEN}")
|
||||||
|
fi
|
||||||
|
|
||||||
|
# Fetch with error handling
|
||||||
|
response=$(curl "${curl_opts[@]}" "https://api.github.com/repos/${GITHUB_REPO}/releases/latest" 2>&1)
|
||||||
|
|
||||||
|
# Check for rate limiting
|
||||||
|
if echo "$response" | grep -q "API rate limit exceeded"; then
|
||||||
|
echo ""
|
||||||
|
error "GitHub API rate limit exceeded.
|
||||||
|
|
||||||
|
You have a few options:
|
||||||
|
1. Wait ~1 hour for the rate limit to reset
|
||||||
|
2. Specify a version manually:
|
||||||
|
curl -sSL https://raw.githubusercontent.com/${GITHUB_REPO}/main/scripts/install.sh | bash -s -- v0.6.1
|
||||||
|
3. Use a GitHub token (set GITHUB_TOKEN environment variable)
|
||||||
|
4. Clone and build from source:
|
||||||
|
git clone https://github.com/${GITHUB_REPO}.git
|
||||||
|
cd claude-mnemonic && make build && make install"
|
||||||
|
fi
|
||||||
|
|
||||||
|
# Check for other API errors
|
||||||
|
if echo "$response" | grep -q '"message":'; then
|
||||||
|
local msg
|
||||||
|
msg=$(echo "$response" | grep '"message":' | sed -E 's/.*"message": *"([^"]+)".*/\1/')
|
||||||
|
error "GitHub API error: $msg"
|
||||||
|
fi
|
||||||
|
|
||||||
|
# Extract version
|
||||||
|
version=$(echo "$response" | grep '"tag_name":' | sed -E 's/.*"([^"]+)".*/\1/')
|
||||||
|
|
||||||
if [[ -z "$version" ]]; then
|
if [[ -z "$version" ]]; then
|
||||||
error "Failed to fetch latest version from GitHub"
|
error "Failed to fetch latest version from GitHub. Response: $response"
|
||||||
fi
|
fi
|
||||||
|
|
||||||
echo "$version"
|
echo "$version"
|
||||||
@@ -151,6 +184,15 @@ register_plugin() {
|
|||||||
|
|
||||||
# Ensure directories exist
|
# Ensure directories exist
|
||||||
mkdir -p "$HOME/.claude/plugins"
|
mkdir -p "$HOME/.claude/plugins"
|
||||||
|
|
||||||
|
# Clean up old cache versions to prevent stale binaries
|
||||||
|
local cache_base
|
||||||
|
cache_base=$(dirname "$CACHE_DIR")
|
||||||
|
if [[ -d "$cache_base" ]]; then
|
||||||
|
info "Cleaning up old cache versions..."
|
||||||
|
find "$cache_base" -mindepth 1 -maxdepth 1 -type d ! -name "${version#v}" -exec rm -rf {} \; 2>/dev/null || true
|
||||||
|
fi
|
||||||
|
|
||||||
mkdir -p "${CACHE_DIR}/${version}"
|
mkdir -p "${CACHE_DIR}/${version}"
|
||||||
|
|
||||||
# Create JSON files if they don't exist
|
# Create JSON files if they don't exist
|
||||||
@@ -193,12 +235,24 @@ EOF
|
|||||||
|
|
||||||
success "Plugin registered in installed_plugins.json"
|
success "Plugin registered in installed_plugins.json"
|
||||||
|
|
||||||
# Enable in settings.json
|
# Enable in settings.json and configure statusline
|
||||||
jq --arg key "$PLUGIN_KEY" \
|
local statusline_cmd="$INSTALL_DIR/hooks/statusline"
|
||||||
'.enabledPlugins //= {} | .enabledPlugins[$key] = true' "$SETTINGS_FILE" > "${SETTINGS_FILE}.tmp" \
|
local statusline_entry
|
||||||
|
statusline_entry=$(cat <<EOF
|
||||||
|
{
|
||||||
|
"type": "command",
|
||||||
|
"command": "$statusline_cmd",
|
||||||
|
"padding": 0
|
||||||
|
}
|
||||||
|
EOF
|
||||||
|
)
|
||||||
|
|
||||||
|
jq --arg key "$PLUGIN_KEY" --argjson statusline "$statusline_entry" \
|
||||||
|
'.enabledPlugins //= {} | .enabledPlugins[$key] = true | .statusLine = $statusline' "$SETTINGS_FILE" > "${SETTINGS_FILE}.tmp" \
|
||||||
&& mv "${SETTINGS_FILE}.tmp" "$SETTINGS_FILE"
|
&& mv "${SETTINGS_FILE}.tmp" "$SETTINGS_FILE"
|
||||||
|
|
||||||
success "Plugin enabled in settings.json"
|
success "Plugin enabled in settings.json"
|
||||||
|
success "Statusline configured in settings.json"
|
||||||
|
|
||||||
# Register marketplace
|
# Register marketplace
|
||||||
local marketplace_entry
|
local marketplace_entry
|
||||||
@@ -394,7 +448,8 @@ if [[ "${1:-}" == "--uninstall" ]]; then
|
|||||||
jq 'del(.plugins["'"$PLUGIN_KEY"'"])' "$PLUGINS_FILE" > "${PLUGINS_FILE}.tmp" && mv "${PLUGINS_FILE}.tmp" "$PLUGINS_FILE"
|
jq 'del(.plugins["'"$PLUGIN_KEY"'"])' "$PLUGINS_FILE" > "${PLUGINS_FILE}.tmp" && mv "${PLUGINS_FILE}.tmp" "$PLUGINS_FILE"
|
||||||
fi
|
fi
|
||||||
if [[ -f "$SETTINGS_FILE" ]]; then
|
if [[ -f "$SETTINGS_FILE" ]]; then
|
||||||
jq 'del(.enabledPlugins["'"$PLUGIN_KEY"'"])' "$SETTINGS_FILE" > "${SETTINGS_FILE}.tmp" && mv "${SETTINGS_FILE}.tmp" "$SETTINGS_FILE"
|
# Remove plugin from enabled plugins and remove statusline if it's ours
|
||||||
|
jq 'del(.enabledPlugins["'"$PLUGIN_KEY"'"]) | if .statusLine.command | test("claude-mnemonic") then del(.statusLine) else . end' "$SETTINGS_FILE" > "${SETTINGS_FILE}.tmp" && mv "${SETTINGS_FILE}.tmp" "$SETTINGS_FILE"
|
||||||
fi
|
fi
|
||||||
if [[ -f "$MARKETPLACES_FILE" ]]; then
|
if [[ -f "$MARKETPLACES_FILE" ]]; then
|
||||||
jq 'del(.["claude-mnemonic"])' "$MARKETPLACES_FILE" > "${MARKETPLACES_FILE}.tmp" && mv "${MARKETPLACES_FILE}.tmp" "$MARKETPLACES_FILE"
|
jq 'del(.["claude-mnemonic"])' "$MARKETPLACES_FILE" > "${MARKETPLACES_FILE}.tmp" && mv "${MARKETPLACES_FILE}.tmp" "$MARKETPLACES_FILE"
|
||||||
|
|||||||
@@ -9,13 +9,22 @@ MARKETPLACES_FILE="$HOME/.claude/plugins/known_marketplaces.json"
|
|||||||
PLUGIN_KEY="claude-mnemonic@claude-mnemonic"
|
PLUGIN_KEY="claude-mnemonic@claude-mnemonic"
|
||||||
MARKETPLACE_NAME="claude-mnemonic"
|
MARKETPLACE_NAME="claude-mnemonic"
|
||||||
MARKETPLACE_PATH="$HOME/.claude/plugins/marketplaces/claude-mnemonic"
|
MARKETPLACE_PATH="$HOME/.claude/plugins/marketplaces/claude-mnemonic"
|
||||||
CACHE_PATH="$HOME/.claude/plugins/cache/claude-mnemonic/claude-mnemonic/1.0.0"
|
|
||||||
VERSION="1.0.0"
|
# Get version from git tags (same as Makefile), or use argument if provided
|
||||||
|
VERSION="${1:-$(git describe --tags --always --dirty 2>/dev/null || echo "dev")}"
|
||||||
|
CACHE_BASE="$HOME/.claude/plugins/cache/claude-mnemonic/claude-mnemonic"
|
||||||
|
CACHE_PATH="$CACHE_BASE/$VERSION"
|
||||||
TIMESTAMP=$(date -u +"%Y-%m-%dT%H:%M:%S.000Z")
|
TIMESTAMP=$(date -u +"%Y-%m-%dT%H:%M:%S.000Z")
|
||||||
|
|
||||||
# Ensure plugins directory exists
|
# Ensure plugins directory exists
|
||||||
mkdir -p "$HOME/.claude/plugins"
|
mkdir -p "$HOME/.claude/plugins"
|
||||||
|
|
||||||
|
# Clean up old cache versions to prevent stale binaries
|
||||||
|
if [ -d "$CACHE_BASE" ]; then
|
||||||
|
echo "Cleaning up old cache versions..."
|
||||||
|
find "$CACHE_BASE" -mindepth 1 -maxdepth 1 -type d ! -name "$VERSION" -exec rm -rf {} \; 2>/dev/null || true
|
||||||
|
fi
|
||||||
|
|
||||||
# Create installed_plugins.json if it doesn't exist
|
# Create installed_plugins.json if it doesn't exist
|
||||||
if [ ! -f "$PLUGINS_FILE" ]; then
|
if [ ! -f "$PLUGINS_FILE" ]; then
|
||||||
echo '{"version": 2, "plugins": {}}' > "$PLUGINS_FILE"
|
echo '{"version": 2, "plugins": {}}' > "$PLUGINS_FILE"
|
||||||
@@ -60,13 +69,24 @@ EOF
|
|||||||
|
|
||||||
echo "Plugin registered in installed_plugins.json"
|
echo "Plugin registered in installed_plugins.json"
|
||||||
|
|
||||||
# Enable the plugin in settings.json
|
# Enable the plugin in settings.json and configure statusline
|
||||||
# First ensure enabledPlugins object exists, then add our plugin
|
# First ensure enabledPlugins object exists, then add our plugin
|
||||||
jq --arg key "$PLUGIN_KEY" \
|
STATUSLINE_CMD="$MARKETPLACE_PATH/hooks/statusline"
|
||||||
'.enabledPlugins //= {} | .enabledPlugins[$key] = true' "$SETTINGS_FILE" > "${SETTINGS_FILE}.tmp" \
|
STATUSLINE_ENTRY=$(cat <<EOF
|
||||||
|
{
|
||||||
|
"type": "command",
|
||||||
|
"command": "$STATUSLINE_CMD",
|
||||||
|
"padding": 0
|
||||||
|
}
|
||||||
|
EOF
|
||||||
|
)
|
||||||
|
|
||||||
|
jq --arg key "$PLUGIN_KEY" --argjson statusline "$STATUSLINE_ENTRY" \
|
||||||
|
'.enabledPlugins //= {} | .enabledPlugins[$key] = true | .statusLine = $statusline' "$SETTINGS_FILE" > "${SETTINGS_FILE}.tmp" \
|
||||||
&& mv "${SETTINGS_FILE}.tmp" "$SETTINGS_FILE"
|
&& mv "${SETTINGS_FILE}.tmp" "$SETTINGS_FILE"
|
||||||
|
|
||||||
echo "Plugin enabled in settings.json"
|
echo "Plugin enabled in settings.json"
|
||||||
|
echo "Statusline configured in settings.json"
|
||||||
|
|
||||||
# Register the marketplace in known_marketplaces.json
|
# Register the marketplace in known_marketplaces.json
|
||||||
MARKETPLACE_ENTRY=$(cat <<EOF
|
MARKETPLACE_ENTRY=$(cat <<EOF
|
||||||
|
|||||||
Reference in New Issue
Block a user