Compare commits

...
10 Commits
Author SHA1 Message Date
lukaszraczylo d8a4939201 Prevent endless loops during connectivity issues. 2025-12-17 02:39:04 +00:00
github-actions[bot] 50f078bc6a chore: update marketplace for v0.6.15 2025-12-17 01:13:47 +00:00
lukaszraczylo 8ff08732c6 Increase automatic update size limit to 250MB 2025-12-17 01:04:24 +00:00
github-actions[bot] 433d165f17 chore: update marketplace for v0.6.11 2025-12-16 19:20:58 +00:00
lukaszraczylo 378e7dc489 Don't store system / agent summaries. 2025-12-16 19:15:31 +00:00
github-actions[bot] c2c31c8c5d chore: update marketplace for v0.6.8 2025-12-16 18:34:05 +00:00
lukaszraczylo b396d05c0f Fixes onnx version mismatch, vectordb not liking it and dashboard
Fixes summaries not being generated and timeline showing all observations
despite of project filter being applied.
2025-12-16 18:29:09 +00:00
github-actions[bot] 66c53deb02 chore: update marketplace for v0.6.4 2025-12-16 16:58:43 +00:00
lukaszraczylo 372942ec8b Add deployment of statusline. 2025-12-16 16:53:40 +00:00
github-actions[bot] e3d3f2bb7b chore: update marketplace for v0.6.1 2025-12-16 15:24:12 +00:00
18 changed files with 504 additions and 53 deletions
+29
View File
@@ -221,6 +221,35 @@ builds:
- goos: windows
goarch: arm64
# Hook: statusline
- id: hook-statusline
main: ./cmd/hooks/statusline
binary: hooks/statusline
env:
- CGO_ENABLED=1
tags:
- fts5
flags:
- -trimpath
ldflags:
- -s -w
- -X main.Version={{.Version}}
- -X github.com/lukaszraczylo/claude-mnemonic/pkg/hooks.Version={{.Version}}
goos:
- darwin
- linux
- windows
goarch:
- amd64
- arm64
ignore:
- goos: darwin
goarch: amd64
- goos: linux
goarch: arm64
- goos: windows
goarch: arm64
archives:
- id: default
formats:
+39
View File
@@ -17,11 +17,50 @@ type Input struct {
ToolUseID string `json:"tool_use_id"`
}
// skipTools lists tools that never produce useful observations.
// Skip the HTTP call entirely for these to reduce overhead during heavy tool usage.
var skipTools = map[string]bool{
// Internal tracking tools (but NOT TodoWrite - it captures planned work)
"Task": true,
"TaskOutput": true,
// File discovery tools (just listings, no insights)
"Glob": true,
"ListDir": true,
"LS": true,
"KillShell": true,
// Question/interaction tools (no code insights)
"AskUserQuestion": true,
// Plan mode tools (planning, not execution)
"EnterPlanMode": true,
"ExitPlanMode": true,
// Skill/command execution (meta-operations)
"Skill": true,
"SlashCommand": true,
// Read is high-volume and rarely produces insights worth the overhead
// The processor would skip most reads anyway after filtering
"Read": true,
// Search tools are for finding, not modifying
"Grep": true,
"WebSearch": true,
}
func main() {
hooks.RunHook("PostToolUse", handlePostToolUse)
}
func handlePostToolUse(ctx *hooks.HookContext, input *Input) (string, error) {
// Skip HTTP call entirely for tools that never produce useful observations.
// This significantly reduces overhead during heavy tool usage.
if skipTools[input.ToolName] {
return "", nil
}
fmt.Fprintf(os.Stderr, "[post-tool-use] %s\n", input.ToolName)
// Send observation to worker
+19 -2
View File
@@ -75,13 +75,16 @@ func parseTranscript(path string) (lastUser, lastAssistant string) {
continue
}
if msg.Type == "message" {
// Transcript entries have type: "user" or "assistant" (not "message")
// Check if this is a user/assistant message with content
if msg.Type == "user" || msg.Type == "assistant" {
text := extractTextContent(msg.Message.Content)
if text == "" {
continue
}
switch msg.Message.Role {
// Use the outer type field for role (message.role may differ)
switch msg.Type {
case "user":
lastUser = text
case "assistant":
@@ -98,6 +101,9 @@ func main() {
}
func handleStop(ctx *hooks.HookContext, input *Input) (string, error) {
// Debug: dump raw input
fmt.Fprintf(os.Stderr, "[stop] Raw input: %s\n", string(ctx.RawInput))
// Find session
result, err := hooks.GET(ctx.Port, fmt.Sprintf("/api/sessions?claudeSessionId=%s", ctx.SessionID))
if err != nil || result == nil {
@@ -116,6 +122,17 @@ func handleStop(ctx *hooks.HookContext, input *Input) (string, error) {
lastUser, lastAssistant = parseTranscript(input.TranscriptPath)
}
// Debug: log what we extracted
fmt.Fprintf(os.Stderr, "[stop] Transcript path: %s\n", input.TranscriptPath)
fmt.Fprintf(os.Stderr, "[stop] Last user message length: %d\n", len(lastUser))
fmt.Fprintf(os.Stderr, "[stop] Last assistant message length: %d\n", len(lastAssistant))
if len(lastAssistant) > 0 {
preview := lastAssistant
if len(preview) > 300 {
preview = preview[:300] + "..."
}
fmt.Fprintf(os.Stderr, "[stop] Last assistant preview: %s\n", preview)
}
fmt.Fprintf(os.Stderr, "[stop] Requesting summary for session %d (transcript: %v)\n", int64(sessionID), input.TranscriptPath != "")
// Request summary with message context from transcript
+11
View File
@@ -272,6 +272,17 @@ var Migrations = []Migration{
);
`,
},
{
Version: 18,
Name: "user_prompts_unique_constraint",
SQL: `
-- Add unique constraint to prevent duplicate prompts
-- This fixes a bug where the user-prompt hook could fire multiple times
-- creating duplicate prompt records with incrementing numbers
CREATE UNIQUE INDEX IF NOT EXISTS idx_user_prompts_session_number_unique
ON user_prompts(claude_session_id, prompt_number);
`,
},
}
// MigrationManager handles database schema migrations.
+23
View File
@@ -175,6 +175,29 @@ func (s *ObservationStore) GetRecentObservations(ctx context.Context, project st
return scanObservationRows(rows)
}
// GetObservationsByProjectStrict retrieves observations strictly for a specific project.
// Unlike GetRecentObservations, this does NOT include global observations from other projects.
// Use this for dashboard filtering where the user expects to see only that project's data.
func (s *ObservationStore) GetObservationsByProjectStrict(ctx context.Context, project string, limit int) ([]*models.Observation, error) {
const query = `
SELECT id, sdk_session_id, project, COALESCE(scope, 'project') as scope, type, title, subtitle, facts, narrative,
concepts, files_read, files_modified, file_mtimes, prompt_number, discovery_tokens,
created_at, created_at_epoch
FROM observations
WHERE project = ?
ORDER BY created_at_epoch DESC
LIMIT ?
`
rows, err := s.store.QueryContext(ctx, query, project, limit)
if err != nil {
return nil, err
}
defer rows.Close()
return scanObservationRows(rows)
}
// GetObservationCount returns the count of observations for a project (including global).
func (s *ObservationStore) GetObservationCount(ctx context.Context, project string) (int, error) {
const query = `
+41 -1
View File
@@ -32,11 +32,15 @@ func (s *PromptStore) SetCleanupFunc(fn PromptCleanupFunc) {
}
// SaveUserPromptWithMatches saves a user prompt with matched observation count.
// Uses INSERT OR IGNORE to be idempotent - duplicate (session, prompt_number) pairs are silently ignored.
// This prevents duplicate prompts when the user-prompt hook fires multiple times.
func (s *PromptStore) SaveUserPromptWithMatches(ctx context.Context, claudeSessionID string, promptNumber int, promptText string, matchedObservations int) (int64, error) {
now := time.Now()
// Use INSERT OR IGNORE for idempotency - if (claude_session_id, prompt_number) already exists,
// the insert is silently ignored. This handles concurrent/duplicate hook invocations.
const query = `
INSERT INTO user_prompts
INSERT OR IGNORE INTO user_prompts
(claude_session_id, prompt_number, prompt_text, matched_observations, created_at, created_at_epoch)
VALUES (?, ?, ?, ?, ?, ?)
`
@@ -51,6 +55,17 @@ func (s *PromptStore) SaveUserPromptWithMatches(ctx context.Context, claudeSessi
id, _ := result.LastInsertId()
// If id is 0, the insert was ignored (duplicate) - fetch the existing ID
if id == 0 {
const selectQuery = `SELECT id FROM user_prompts WHERE claude_session_id = ? AND prompt_number = ?`
row := s.store.QueryRowContext(ctx, selectQuery, claudeSessionID, promptNumber)
if err := row.Scan(&id); err != nil {
return 0, err
}
// Return existing ID without triggering cleanup (already handled when first inserted)
return id, nil
}
// Cleanup old prompts beyond the global limit (async to not block handler)
go func() {
cleanupCtx, cancel := context.WithTimeout(context.Background(), 10*time.Second)
@@ -184,6 +199,31 @@ func (s *PromptStore) GetAllRecentUserPrompts(ctx context.Context, limit int) ([
return scanPromptWithSessionRows(rows)
}
// FindRecentPromptByText finds a prompt with the same text for a session within the last few seconds.
// This is used to detect duplicate hook invocations.
// Returns (promptID, promptNumber, found).
func (s *PromptStore) FindRecentPromptByText(ctx context.Context, claudeSessionID, promptText string, withinSeconds int) (int64, int, bool) {
// Look for an existing prompt with the same text within the time window
// This catches duplicate hook invocations that happen in quick succession
const query = `
SELECT id, prompt_number FROM user_prompts
WHERE claude_session_id = ? AND prompt_text = ?
AND created_at_epoch > ?
ORDER BY created_at_epoch DESC
LIMIT 1
`
cutoff := time.Now().Add(-time.Duration(withinSeconds) * time.Second).UnixMilli()
var id int64
var promptNumber int
err := s.store.QueryRowContext(ctx, query, claudeSessionID, promptText, cutoff).Scan(&id, &promptNumber)
if err != nil {
return 0, 0, false
}
return id, promptNumber, true
}
// GetRecentUserPromptsByProject retrieves recent user prompts for a specific project.
func (s *PromptStore) GetRecentUserPromptsByProject(ctx context.Context, project string, limit int) ([]*models.UserPromptWithSession, error) {
const query = `
@@ -0,0 +1 @@
1.23.2
@@ -0,0 +1 @@
1.23.2
@@ -0,0 +1 @@
1.23.2
@@ -0,0 +1 @@
1.23.2
+34 -16
View File
@@ -27,7 +27,7 @@ const (
GitHubRepo = "lukaszraczylo/claude-mnemonic"
ReleasesAPI = "https://api.github.com/repos/" + GitHubRepo + "/releases/latest"
CheckInterval = 24 * time.Hour
MaxExtractedSize = 100 * 1024 * 1024 // 100MB max per extracted file
MaxExtractedSize = 250 * 1024 * 1024 // 250MB max per extracted file
RestartDelay = 500 * time.Millisecond
)
@@ -49,22 +49,36 @@ type Asset struct {
// UpdateInfo contains information about an available update.
type UpdateInfo struct {
Available bool `json:"available"`
CurrentVersion string `json:"current_version"`
LatestVersion string `json:"latest_version"`
ReleaseNotes string `json:"release_notes,omitempty"`
PublishedAt time.Time `json:"published_at,omitempty"`
DownloadURL string `json:"download_url,omitempty"`
ChecksumsURL string `json:"checksums_url,omitempty"`
BundleURL string `json:"bundle_url,omitempty"` // Sigstore bundle (.sigstore.json)
Available bool `json:"available"`
CurrentVersion string `json:"current_version"`
LatestVersion string `json:"latest_version"`
ReleaseNotes string `json:"release_notes,omitempty"`
PublishedAt time.Time `json:"published_at,omitempty"`
DownloadURL string `json:"download_url,omitempty"`
ChecksumsURL string `json:"checksums_url,omitempty"`
BundleURL string `json:"bundle_url,omitempty"` // Sigstore bundle (.sigstore.json)
ManualUpdateCommand string `json:"manual_update_command,omitempty"`
}
// InstallScriptURL is the URL to the remote installation script.
const InstallScriptURL = "https://raw.githubusercontent.com/" + GitHubRepo + "/main/scripts/install.sh"
// GetManualUpdateCommand returns the curl command for manual update.
// If version is empty, it installs the latest version.
func GetManualUpdateCommand(version string) string {
if version == "" {
return fmt.Sprintf("curl -sSL %s | bash", InstallScriptURL)
}
return fmt.Sprintf("curl -sSL %s | bash -s -- %s", InstallScriptURL, version)
}
// UpdateStatus represents the current update status.
type UpdateStatus struct {
State string `json:"state"` // "idle", "checking", "downloading", "verifying", "applying", "done", "error"
Progress float64 `json:"progress"`
Message string `json:"message"`
Error string `json:"error,omitempty"`
State string `json:"state"` // "idle", "checking", "downloading", "verifying", "applying", "done", "error"
Progress float64 `json:"progress"`
Message string `json:"message"`
Error string `json:"error,omitempty"`
ManualUpdateCommand string `json:"manual_update_command,omitempty"` // Shown when update fails
}
// Updater handles self-updates.
@@ -112,9 +126,10 @@ func (u *Updater) setError(err error) {
u.mu.Lock()
defer u.mu.Unlock()
u.status = UpdateStatus{
State: "error",
Message: "Update failed",
Error: err.Error(),
State: "error",
Message: "Update failed",
Error: err.Error(),
ManualUpdateCommand: GetManualUpdateCommand(""), // Always provide fallback command
}
}
@@ -169,6 +184,9 @@ func (u *Updater) CheckForUpdate(ctx context.Context) (*UpdateInfo, error) {
// Compare versions
info.Available = isNewerVersion(info.LatestVersion, u.currentVersion)
// Always include manual update command as an alternative option
info.ManualUpdateCommand = GetManualUpdateCommand("v" + info.LatestVersion)
if info.Available {
// Find download URLs for current platform
platform := getPlatform()
+32 -3
View File
@@ -159,7 +159,13 @@ type SessionInitResponse struct {
Reason string `json:"reason,omitempty"`
}
// DuplicatePromptWindowSeconds is the time window for detecting duplicate prompt submissions.
// If the same prompt text is seen within this window, it's considered a duplicate hook invocation.
const DuplicatePromptWindowSeconds = 10
// handleSessionInit handles session initialization from user-prompt hook.
// This handler is idempotent - duplicate requests within a short time window
// return the existing prompt data without creating duplicates.
func (s *Service) handleSessionInit(w http.ResponseWriter, r *http.Request) {
var req SessionInitRequest
if err := json.NewDecoder(r.Body).Decode(&req); err != nil {
@@ -182,8 +188,31 @@ func (s *Service) handleSessionInit(w http.ResponseWriter, r *http.Request) {
return
}
// Clean prompt and create session
// Clean prompt
cleanedPrompt := privacy.Clean(req.Prompt)
// DUPLICATE DETECTION: Check if this exact prompt was already saved recently.
// This prevents the bug where the hook fires multiple times for the same user action,
// creating many duplicate prompts with incrementing numbers.
if existingID, existingNum, found := s.promptStore.FindRecentPromptByText(r.Context(), req.ClaudeSessionID, cleanedPrompt, DuplicatePromptWindowSeconds); found {
// Get or create session (idempotent)
sessionID, _ := s.sessionStore.CreateSDKSession(r.Context(), req.ClaudeSessionID, req.Project, cleanedPrompt)
log.Debug().
Int64("sessionId", sessionID).
Int("promptNumber", existingNum).
Int64("promptId", existingID).
Msg("Duplicate prompt detected - returning existing")
// Return existing prompt data without incrementing or saving again
writeJSON(w, SessionInitResponse{
SessionDBID: sessionID,
PromptNumber: existingNum,
})
return
}
// Create session (idempotent)
sessionID, err := s.sessionStore.CreateSDKSession(r.Context(), req.ClaudeSessionID, req.Project, cleanedPrompt)
if err != nil {
http.Error(w, err.Error(), http.StatusInternalServerError)
@@ -478,8 +507,8 @@ func (s *Service) handleGetObservations(w http.ResponseWriter, r *http.Request)
// Fall back to SQLite if vector search not used
if !usedVector {
if project != "" {
// Filter by project - includes project-scoped and global observations
observations, err = s.observationStore.GetRecentObservations(r.Context(), project, limit)
// Strict project filtering for dashboard - only observations from this project
observations, err = s.observationStore.GetObservationsByProjectStrict(r.Context(), project, limit)
} else {
// All projects
observations, err = s.observationStore.GetAllRecentObservations(r.Context(), limit)
+63 -2
View File
@@ -234,11 +234,19 @@ func (p *Processor) ProcessObservation(ctx context.Context, sdkSessionID, projec
// ProcessSummary processes a session summary request.
func (p *Processor) ProcessSummary(ctx context.Context, sessionDBID int64, sdkSessionID, project, userPrompt, lastUserMsg, lastAssistantMsg string) error {
// Debug: log what we received
log.Debug().
Int64("sessionId", sessionDBID).
Int("lastAssistantMsgLen", len(lastAssistantMsg)).
Str("lastAssistantMsgPreview", truncateForLog(lastAssistantMsg, 200)).
Msg("ProcessSummary called")
// Skip summary generation if there's no meaningful assistant response
// This prevents generic "initial session setup" summaries
if !hasMeaningfulContent(lastAssistantMsg) {
log.Info().
Int64("sessionId", sessionDBID).
Int("msgLen", len(lastAssistantMsg)).
Msg("Skipping summary - no meaningful assistant response")
return nil
}
@@ -634,25 +642,57 @@ Your response:`,
// rather than actual user work. These summaries should be filtered out.
func isSelfReferentialSummary(summary *models.ParsedSummary) bool {
// Combine all summary fields for checking
content := strings.ToLower(summary.Request + " " + summary.Completed + " " + summary.Learned + " " + summary.NextSteps)
content := strings.ToLower(summary.Request + " " + summary.Completed + " " + summary.Learned + " " + summary.NextSteps + " " + summary.Investigated + " " + summary.Notes)
// Indicators that the summary is about the memory agent, not user work
selfReferentialPhrases := []string{
// Agent references
"memory extraction",
"memory agent",
"extraction agent",
"hook execution",
"hook mechanism",
// Session meta-state
"session initialization",
"session setup",
"session has just started",
"session just started",
"agent initialization",
"no technical learnings",
"no code or project work",
// Waiting states
"waiting for the user",
"waiting for user",
"awaiting actual",
"awaiting claude code",
"awaiting tool",
"awaiting user",
// Meta checkpoint references
"progress checkpoint",
"checkpoint request",
// Common no-work phrases
"no work has been completed",
"no work completed",
"no work done",
"no actual work",
"nothing has been completed",
"nothing completed",
// Role/guideline parroting
"role definition",
"operational guidelines",
"providing role",
"providing guidelines",
// System prompt echoes
"extract meaningful observations",
"meaningful learnings",
"analyze tool executions",
"observations for future sessions",
// Empty session indicators
"empty session",
"no substantive work",
"no meaningful work",
"just beginning",
"just begun",
}
matchCount := 0
@@ -677,20 +717,33 @@ func hasMeaningfulContent(assistantMsg string) bool {
lowerMsg := strings.ToLower(assistantMsg)
// Skip messages that are primarily about system/hook status
// Skip messages that are primarily about system/hook status or meta-instructions
skipIndicators := []string{
// System/hook markers
"hook success",
"callback hook",
"session start",
"sessionstart",
"system-reminder",
// Agent self-references
"memory extraction agent",
"memory agent",
"extraction agent",
// No-work indicators
"no technical learnings",
"waiting for",
"waiting to",
"no code or project work",
"no substantive",
"no work has been completed",
"no work done",
"awaiting tool",
"awaiting user",
// Meta-instruction echoes
"role definition",
"operational guidelines",
"analyze tool executions",
"extract meaningful observations",
}
skipCount := 0
@@ -727,6 +780,14 @@ func hasMeaningfulContent(assistantMsg string) bool {
return matchCount >= 2
}
// truncateForLog truncates a string for logging purposes.
func truncateForLog(s string, maxLen int) string {
if len(s) <= maxLen {
return s
}
return s[:maxLen] + "..."
}
const systemPrompt = `You are a memory extraction agent for Claude Code sessions. Your job is to analyze tool executions and extract meaningful observations that would be useful for future sessions.
GUIDELINES:
+126
View File
@@ -0,0 +1,126 @@
package sdk
import (
"testing"
"github.com/lukaszraczylo/claude-mnemonic/pkg/models"
)
func TestIsSelfReferentialSummary(t *testing.T) {
tests := []struct {
name string
summary *models.ParsedSummary
expected bool
}{
{
name: "meta summary about memory agent role",
summary: &models.ParsedSummary{
Request: "Memory extraction agent role - analyze tool executions and extract meaningful observations for future sessions",
Completed: "No work has been completed yet. The session has just started with the user providing role definition and operational guidelines.",
Learned: "The system expects observations to be created from meaningful learnings during Claude Code sessions, with focus on decisions, bugs fixed, patterns discovered, project structure changes, and code modifications.",
NextSteps: "Awaiting tool executions or user requests that contain actual work performed in a Claude Code session.",
},
expected: true,
},
{
name: "legitimate summary about code changes",
summary: &models.ParsedSummary{
Request: "Fix authentication bug in login handler",
Completed: "Updated the auth middleware to properly validate JWT tokens and fixed the session expiry check.",
Learned: "The JWT library requires explicit algorithm validation to prevent token substitution attacks.",
NextSteps: "Add unit tests for the authentication flow.",
},
expected: false,
},
{
name: "awaiting user summary",
summary: &models.ParsedSummary{
Request: "Session initialization",
Completed: "No work completed yet.",
Learned: "Awaiting user input to begin work.",
NextSteps: "Waiting for the user to provide instructions.",
},
expected: true,
},
{
name: "summary about refactoring",
summary: &models.ParsedSummary{
Request: "Refactor database connection pooling",
Completed: "Implemented connection pooling using pgxpool with max 10 connections.",
Learned: "pgxpool automatically handles connection reuse and health checks.",
NextSteps: "Run benchmarks to verify performance improvement.",
},
expected: false,
},
{
name: "meta summary with extraction agent mention",
summary: &models.ParsedSummary{
Request: "Extraction agent initialization",
Completed: "No substantive work has been done.",
Learned: "The memory extraction agent analyzes tool executions.",
NextSteps: "Awaiting tool results to extract observations.",
},
expected: true,
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
result := isSelfReferentialSummary(tt.summary)
if result != tt.expected {
t.Errorf("isSelfReferentialSummary() = %v, want %v", result, tt.expected)
}
})
}
}
func TestHasMeaningfulContent(t *testing.T) {
tests := []struct {
name string
content string
expected bool
}{
{
name: "empty content",
content: "",
expected: false,
},
{
name: "too short content",
content: "Hello world",
expected: false,
},
{
name: "meta content about memory agent",
content: `This is the memory extraction agent role definition.
The system expects you to analyze tool executions and extract meaningful observations.
No work has been completed yet. Awaiting tool results from the user's session.`,
expected: false,
},
{
name: "legitimate code discussion",
content: `I've updated the handler.go file to fix the authentication bug.
The function validateToken() was not checking token expiry correctly.
I've added a check for exp claim and implemented proper error handling.
The changes have been tested and the build passes.`,
expected: true,
},
{
name: "hook status messages",
content: `SessionStart:Callback hook success: Success
The memory agent is waiting for user input.
System-reminder about available tools.
No substantive work performed yet.`,
expected: false,
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
result := hasMeaningfulContent(tt.content)
if result != tt.expected {
t.Errorf("hasMeaningfulContent() = %v, want %v", result, tt.expected)
}
})
}
}
+10 -15
View File
@@ -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.1",
"version": "0.6.15",
"author": {
"name": "lukaszraczylo",
"url": "https://github.com/lukaszraczylo"
@@ -23,30 +23,25 @@
"license": "MIT",
"homepage": "https://github.com/lukaszraczylo/claude-mnemonic",
"releases": {
"latest": "0.6.1",
"latest": "0.6.15",
"versions": {
"0.6.1": {
"releaseDate": "2025-12-15",
"0.6.15": {
"releaseDate": "2025-12-17",
"downloads": {
"darwin-amd64": {
"url": "https://github.com/lukaszraczylo/claude-mnemonic/releases/download/v0.6.1/claude-mnemonic_0.6.1_darwin_amd64.tar.gz",
"format": "tar.gz",
"sha256": "98622f7606d5f1ec451c17bb5d3181b05ec4d0e7dd73d49d2a6493a9fe1ac911"
},
"darwin-arm64": {
"url": "https://github.com/lukaszraczylo/claude-mnemonic/releases/download/v0.6.1/claude-mnemonic_0.6.1_darwin_arm64.tar.gz",
"url": "https://github.com/lukaszraczylo/claude-mnemonic/releases/download/v0.6.15/claude-mnemonic_0.6.15_darwin_arm64.tar.gz",
"format": "tar.gz",
"sha256": "bf9c060bf2a5759b9f28d30436673bf555ae614cfa0e3a0ebf2debca136e8dfd"
"sha256": "f26095d797a563803def30d18c9bfab0c2277e3e64d81c97ba8baf26e415a603"
},
"linux-amd64": {
"url": "https://github.com/lukaszraczylo/claude-mnemonic/releases/download/v0.6.1/claude-mnemonic_0.6.1_linux_amd64.tar.gz",
"url": "https://github.com/lukaszraczylo/claude-mnemonic/releases/download/v0.6.15/claude-mnemonic_0.6.15_linux_amd64.tar.gz",
"format": "tar.gz",
"sha256": "f917be7e2f9f7b2dceee61649ad65c9f51b1d14acc3c76a307aacd0b89b77523"
"sha256": "d90097495a44bc4599458b61d4bf01d2337d19d0017418f278aec9b028350c66"
},
"windows-amd64": {
"url": "https://github.com/lukaszraczylo/claude-mnemonic/releases/download/v0.6.1/claude-mnemonic_0.6.1_windows_amd64.zip",
"url": "https://github.com/lukaszraczylo/claude-mnemonic/releases/download/v0.6.15/claude-mnemonic_0.6.15_windows_amd64.zip",
"format": "zip",
"sha256": "51fd45ca2bb6c66c07b72a84ccb35ae4b09a64a7f28cc223543dcbc51ccfdb17"
"sha256": "fd1d82e207098caf100edcb94cc22f14ffb50cd89a3576522720861ba798955b"
}
}
}
+70 -11
View File
@@ -1,13 +1,22 @@
#!/bin/bash
# Download ONNX Runtime libraries for embedding
# Usage: ./download-onnx-libs.sh [platform]
# Usage: ./download-onnx-libs.sh [platform] [--force]
# Platform: darwin-arm64, linux-amd64, windows-amd64, or "all" (default)
# Use --force to re-download even if libraries exist
set -e
ONNX_VERSION="1.23.2"
ASSETS_DIR="internal/embedding/assets/lib"
PLATFORM="${1:-all}"
FORCE_DOWNLOAD=false
# Check for --force flag
for arg in "$@"; do
if [ "$arg" = "--force" ]; then
FORCE_DOWNLOAD=true
fi
done
# Auto-detect platform if not specified
if [ "$PLATFORM" = "auto" ]; then
@@ -31,12 +40,38 @@ fi
TEMP_DIR=$(mktemp -d)
trap "rm -rf ${TEMP_DIR}" EXIT
# Get the installed version for a platform
get_installed_version() {
local plat="$1"
local version_file="${ASSETS_DIR}/${plat}/.version"
if [ -f "$version_file" ]; then
cat "$version_file"
else
echo ""
fi
}
# Write version file after successful download
write_version_file() {
local plat="$1"
echo "${ONNX_VERSION}" > "${ASSETS_DIR}/${plat}/.version"
}
# Check if version matches
version_matches() {
local plat="$1"
local installed_version
installed_version=$(get_installed_version "$plat")
[ "$installed_version" = "$ONNX_VERSION" ]
}
download_darwin_arm64() {
echo "Downloading darwin-arm64..."
mkdir -p "${ASSETS_DIR}/darwin-arm64"
curl -fsSL "https://github.com/microsoft/onnxruntime/releases/download/v${ONNX_VERSION}/onnxruntime-osx-arm64-${ONNX_VERSION}.tgz" -o "${TEMP_DIR}/darwin-arm64.tgz"
tar -xzf "${TEMP_DIR}/darwin-arm64.tgz" -C "${TEMP_DIR}"
cp "${TEMP_DIR}/onnxruntime-osx-arm64-${ONNX_VERSION}/lib/libonnxruntime.${ONNX_VERSION}.dylib" "${ASSETS_DIR}/darwin-arm64/libonnxruntime.dylib"
write_version_file "darwin-arm64"
}
download_linux_amd64() {
@@ -46,6 +81,7 @@ download_linux_amd64() {
tar -xzf "${TEMP_DIR}/linux-amd64.tgz" -C "${TEMP_DIR}"
cp "${TEMP_DIR}/onnxruntime-linux-x64-${ONNX_VERSION}/lib/libonnxruntime.so.${ONNX_VERSION}" "${ASSETS_DIR}/linux-amd64/libonnxruntime.so"
cp "${TEMP_DIR}/onnxruntime-linux-x64-${ONNX_VERSION}/lib/libonnxruntime_providers_shared.so" "${ASSETS_DIR}/linux-amd64/libonnxruntime_providers_shared.so" 2>/dev/null || true
write_version_file "linux-amd64"
}
download_linux_arm64() {
@@ -55,6 +91,7 @@ download_linux_arm64() {
tar -xzf "${TEMP_DIR}/linux-arm64.tgz" -C "${TEMP_DIR}"
cp "${TEMP_DIR}/onnxruntime-linux-aarch64-${ONNX_VERSION}/lib/libonnxruntime.so.${ONNX_VERSION}" "${ASSETS_DIR}/linux-arm64/libonnxruntime.so"
cp "${TEMP_DIR}/onnxruntime-linux-aarch64-${ONNX_VERSION}/lib/libonnxruntime_providers_shared.so" "${ASSETS_DIR}/linux-arm64/libonnxruntime_providers_shared.so" 2>/dev/null || true
write_version_file "linux-arm64"
}
download_windows_amd64() {
@@ -66,6 +103,7 @@ download_windows_amd64() {
echo "Downloaded file size: $(wc -c < "${TEMP_DIR}/windows-amd64.zip") bytes"
unzip -q "${TEMP_DIR}/windows-amd64.zip" -d "${TEMP_DIR}"
cp "${TEMP_DIR}/onnxruntime-win-x64-${ONNX_VERSION}/lib/onnxruntime.dll" "${ASSETS_DIR}/windows-amd64/onnxruntime.dll"
write_version_file "windows-amd64"
}
# Check if library already exists for a platform
@@ -79,19 +117,40 @@ lib_exists() {
esac
}
# Download only if not present
# Download only if not present or version mismatch
download_if_needed() {
local plat="$1"
if lib_exists "$plat"; then
echo "Library for ${plat} already exists, skipping download"
return 0
local need_download=false
local reason=""
if [ "$FORCE_DOWNLOAD" = true ]; then
need_download=true
reason="forced"
elif ! lib_exists "$plat"; then
need_download=true
reason="not found"
elif ! version_matches "$plat"; then
local installed_version
installed_version=$(get_installed_version "$plat")
need_download=true
reason="version mismatch (installed: ${installed_version:-unknown}, required: ${ONNX_VERSION})"
fi
if [ "$need_download" = true ]; then
if [ -n "$reason" ] && [ "$reason" != "not found" ]; then
echo "Re-downloading ${plat}: ${reason}"
fi
# Remove old library before downloading
rm -rf "${ASSETS_DIR}/${plat}"
case "$plat" in
darwin-arm64) download_darwin_arm64 ;;
linux-amd64) download_linux_amd64 ;;
linux-arm64) download_linux_arm64 ;;
windows-amd64) download_windows_amd64 ;;
esac
else
echo "Library for ${plat} already exists (v${ONNX_VERSION}), skipping download"
fi
case "$plat" in
darwin-arm64) download_darwin_arm64 ;;
linux-amd64) download_linux_amd64 ;;
linux-arm64) download_linux_arm64 ;;
windows-amd64) download_windows_amd64 ;;
esac
}
echo "ONNX Runtime v${ONNX_VERSION} - Platform: ${PLATFORM}"
+2 -2
View File
@@ -1,12 +1,12 @@
{
"name": "claude-mnemonic-dashboard",
"version": "v0.6.5-11-gdb528b2-dirty",
"version": "v0.6.11-2-g8ff0873-dirty",
"lockfileVersion": 3,
"requires": true,
"packages": {
"": {
"name": "claude-mnemonic-dashboard",
"version": "v0.6.5-11-gdb528b2-dirty",
"version": "v0.6.11-2-g8ff0873-dirty",
"dependencies": {
"vue": "^3.5.13"
},
+1 -1
View File
@@ -1,6 +1,6 @@
{
"name": "claude-mnemonic-dashboard",
"version": "v0.6.5-11-gdb528b2-dirty",
"version": "v0.6.11-2-g8ff0873-dirty",
"private": true,
"type": "module",
"scripts": {