mirror of
https://github.com/lukaszraczylo/claude-mnemonic.git
synced 2026-06-05 23:03:55 +00:00
Add restart command, fix post-update restarts as well.
This commit is contained in:
@@ -266,6 +266,9 @@ archives:
|
||||
- src: plugin/hooks/hooks.json
|
||||
dst: hooks
|
||||
strip_parent: true
|
||||
- src: plugin/commands/*
|
||||
dst: commands
|
||||
strip_parent: true
|
||||
format_overrides:
|
||||
- goos: windows
|
||||
formats:
|
||||
|
||||
@@ -138,10 +138,13 @@ install: build stop-worker
|
||||
@# Install to marketplaces directory (for direct installs)
|
||||
@mkdir -p $(HOME)/.claude/plugins/marketplaces/claude-mnemonic/hooks
|
||||
@mkdir -p $(HOME)/.claude/plugins/marketplaces/claude-mnemonic/.claude-plugin
|
||||
@mkdir -p $(HOME)/.claude/plugins/marketplaces/claude-mnemonic/commands
|
||||
cp $(BUILD_DIR)/worker $(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 $(PLUGIN_DIR)/hooks/hooks.json $(HOME)/.claude/plugins/marketplaces/claude-mnemonic/hooks/
|
||||
@# Copy slash commands if they exist
|
||||
@if [ -d "$(PLUGIN_DIR)/commands" ]; then cp -r $(PLUGIN_DIR)/commands/* $(HOME)/.claude/plugins/marketplaces/claude-mnemonic/commands/ 2>/dev/null || true; fi
|
||||
@# Update plugin.json and marketplace.json with current version to prevent stale version directories
|
||||
@sed 's/"version": "[^"]*"/"version": "$(VERSION)"/g' $(PLUGIN_DIR)/.claude-plugin/plugin.json > $(HOME)/.claude/plugins/marketplaces/claude-mnemonic/.claude-plugin/plugin.json
|
||||
@sed 's/"version": "[^"]*"/"version": "$(VERSION)"/g' $(PLUGIN_DIR)/.claude-plugin/marketplace.json > $(HOME)/.claude/plugins/marketplaces/claude-mnemonic/.claude-plugin/marketplace.json
|
||||
|
||||
+80
-37
@@ -469,57 +469,100 @@ func (u *Updater) extractTarGz(archivePath, destDir string) error {
|
||||
}
|
||||
|
||||
func (u *Updater) replaceBinaries(extractDir string) error {
|
||||
// Files to replace
|
||||
binaries := []struct {
|
||||
src string
|
||||
dest string
|
||||
}{
|
||||
{"worker", filepath.Join(u.installDir, "worker")},
|
||||
{"mcp-server", filepath.Join(u.installDir, "mcp-server")},
|
||||
{"hooks/session-start", filepath.Join(u.installDir, "hooks", "session-start")},
|
||||
{"hooks/user-prompt", filepath.Join(u.installDir, "hooks", "user-prompt")},
|
||||
{"hooks/post-tool-use", filepath.Join(u.installDir, "hooks", "post-tool-use")},
|
||||
{"hooks/stop", filepath.Join(u.installDir, "hooks", "stop")},
|
||||
{"hooks/subagent-stop", filepath.Join(u.installDir, "hooks", "subagent-stop")},
|
||||
// Binary files to replace
|
||||
binaryFiles := []string{
|
||||
"worker",
|
||||
"mcp-server",
|
||||
"hooks/session-start",
|
||||
"hooks/user-prompt",
|
||||
"hooks/post-tool-use",
|
||||
"hooks/stop",
|
||||
"hooks/subagent-stop",
|
||||
"hooks/statusline",
|
||||
}
|
||||
|
||||
for _, b := range binaries {
|
||||
src := filepath.Join(extractDir, b.src)
|
||||
if _, err := os.Stat(src); os.IsNotExist(err) {
|
||||
continue // Skip if not in archive
|
||||
}
|
||||
// Get all install directories (marketplaces and cache directories)
|
||||
installDirs := u.getInstallDirectories()
|
||||
|
||||
// Backup existing binary
|
||||
if _, err := os.Stat(b.dest); err == nil {
|
||||
backup := b.dest + ".bak"
|
||||
if err := os.Rename(b.dest, backup); err != nil {
|
||||
return fmt.Errorf("failed to backup %s: %w", b.dest, err)
|
||||
for _, installDir := range installDirs {
|
||||
log.Debug().Str("dir", installDir).Msg("Installing binaries to directory")
|
||||
|
||||
for _, binaryFile := range binaryFiles {
|
||||
src := filepath.Join(extractDir, binaryFile)
|
||||
if _, err := os.Stat(src); os.IsNotExist(err) {
|
||||
continue // Skip if not in archive
|
||||
}
|
||||
defer func(backup, dest string) {
|
||||
// Clean up backup on success, restore on failure
|
||||
if _, err := os.Stat(dest); err == nil {
|
||||
_ = os.Remove(backup)
|
||||
|
||||
dest := filepath.Join(installDir, binaryFile)
|
||||
|
||||
// Backup existing binary
|
||||
if _, err := os.Stat(dest); err == nil {
|
||||
backup := dest + ".bak"
|
||||
if err := os.Rename(dest, backup); err != nil {
|
||||
log.Warn().Err(err).Str("file", dest).Msg("Failed to backup, continuing anyway")
|
||||
} else {
|
||||
_ = os.Rename(backup, dest)
|
||||
defer func(backup, dest string) {
|
||||
// Clean up backup on success, restore on failure
|
||||
if _, err := os.Stat(dest); err == nil {
|
||||
_ = os.Remove(backup)
|
||||
} else {
|
||||
_ = os.Rename(backup, dest)
|
||||
}
|
||||
}(backup, dest)
|
||||
}
|
||||
}(backup, b.dest)
|
||||
}
|
||||
}
|
||||
|
||||
// Copy new binary
|
||||
if err := copyFile(src, b.dest); err != nil {
|
||||
return fmt.Errorf("failed to install %s: %w", b.dest, err)
|
||||
}
|
||||
// Copy new binary
|
||||
if err := copyFile(src, dest); err != nil {
|
||||
return fmt.Errorf("failed to install %s: %w", dest, err)
|
||||
}
|
||||
|
||||
// Make executable
|
||||
// #nosec G302 -- executables require 0755 permissions
|
||||
if err := os.Chmod(b.dest, 0755); err != nil {
|
||||
return fmt.Errorf("failed to chmod %s: %w", b.dest, err)
|
||||
// Make executable
|
||||
// #nosec G302 -- executables require 0755 permissions
|
||||
if err := os.Chmod(dest, 0755); err != nil {
|
||||
return fmt.Errorf("failed to chmod %s: %w", dest, err)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// getInstallDirectories returns all directories where binaries should be installed.
|
||||
// This includes the marketplaces directory and any cache directories.
|
||||
func (u *Updater) getInstallDirectories() []string {
|
||||
dirs := []string{u.installDir}
|
||||
|
||||
// Also check cache directories where Claude Code looks for plugins
|
||||
home, err := os.UserHomeDir()
|
||||
if err != nil {
|
||||
return dirs
|
||||
}
|
||||
|
||||
// Look for cache directories under ~/.claude/plugins/cache/claude-mnemonic/claude-mnemonic/
|
||||
cacheBase := filepath.Join(home, ".claude/plugins/cache/claude-mnemonic/claude-mnemonic")
|
||||
entries, err := os.ReadDir(cacheBase)
|
||||
if err != nil {
|
||||
return dirs
|
||||
}
|
||||
|
||||
for _, entry := range entries {
|
||||
if entry.IsDir() {
|
||||
cacheDir := filepath.Join(cacheBase, entry.Name())
|
||||
// Only add if it's different from installDir and contains a worker binary
|
||||
if cacheDir != u.installDir {
|
||||
workerPath := filepath.Join(cacheDir, "worker")
|
||||
if _, err := os.Stat(workerPath); err == nil {
|
||||
dirs = append(dirs, cacheDir)
|
||||
log.Debug().Str("dir", cacheDir).Msg("Found cache directory to update")
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return dirs
|
||||
}
|
||||
|
||||
func copyFile(src, dst string) error {
|
||||
// Ensure parent directory exists
|
||||
if err := os.MkdirAll(filepath.Dir(dst), 0750); err != nil {
|
||||
|
||||
@@ -1024,7 +1024,7 @@ func (s *Service) handleSelfCheck(w http.ResponseWriter, r *http.Request) {
|
||||
})
|
||||
}
|
||||
|
||||
// handleUpdateRestart restarts the worker with the new binary.
|
||||
// handleUpdateRestart restarts the worker with the new binary (after update).
|
||||
func (s *Service) handleUpdateRestart(w http.ResponseWriter, r *http.Request) {
|
||||
status := s.updater.GetStatus()
|
||||
if status.State != "done" {
|
||||
@@ -1050,3 +1050,29 @@ func (s *Service) handleUpdateRestart(w http.ResponseWriter, r *http.Request) {
|
||||
}
|
||||
}()
|
||||
}
|
||||
|
||||
// handleRestart restarts the worker process (general restart, not tied to update).
|
||||
func (s *Service) handleRestart(w http.ResponseWriter, r *http.Request) {
|
||||
log.Info().Msg("Manual restart requested via API")
|
||||
|
||||
// Send response before restarting
|
||||
writeJSON(w, map[string]interface{}{
|
||||
"success": true,
|
||||
"message": "Restarting worker...",
|
||||
"version": s.version,
|
||||
})
|
||||
|
||||
// Flush the response
|
||||
if f, ok := w.(http.Flusher); ok {
|
||||
f.Flush()
|
||||
}
|
||||
|
||||
// Restart in background after response is sent
|
||||
go func() {
|
||||
// Small delay to ensure response is sent
|
||||
time.Sleep(100 * time.Millisecond)
|
||||
if err := s.updater.Restart(); err != nil {
|
||||
log.Error().Err(err).Msg("Failed to restart worker")
|
||||
}
|
||||
}()
|
||||
}
|
||||
|
||||
@@ -635,6 +635,9 @@ func (s *Service) setupRoutes() {
|
||||
s.router.Get("/api/update/status", s.handleUpdateStatus)
|
||||
s.router.Post("/api/update/restart", s.handleUpdateRestart)
|
||||
|
||||
// General restart endpoint (works before DB is ready)
|
||||
s.router.Post("/api/restart", s.handleRestart)
|
||||
|
||||
// Selfcheck endpoint (works before DB is ready - checks all components)
|
||||
s.router.Get("/api/selfcheck", s.handleSelfCheck)
|
||||
|
||||
|
||||
@@ -0,0 +1,21 @@
|
||||
# Restart Claude Mnemonic Worker
|
||||
|
||||
Restart the claude-mnemonic worker process. Use this command when experiencing issues with the memory system.
|
||||
|
||||
## Instructions
|
||||
|
||||
1. Call the restart API endpoint using curl:
|
||||
```bash
|
||||
curl -X POST http://127.0.0.1:37777/api/restart
|
||||
```
|
||||
|
||||
2. Wait a moment for the worker to restart (typically 1-2 seconds)
|
||||
|
||||
3. Verify the worker is running by checking the version:
|
||||
```bash
|
||||
curl -s http://127.0.0.1:37777/api/version
|
||||
```
|
||||
|
||||
4. Report the result to the user, including the version number from the response.
|
||||
|
||||
If the restart fails, suggest the user check `/tmp/claude-mnemonic-worker.log` for errors.
|
||||
@@ -176,6 +176,7 @@ download_release() {
|
||||
info "Installing to ${INSTALL_DIR}..."
|
||||
mkdir -p "$INSTALL_DIR/hooks"
|
||||
mkdir -p "$INSTALL_DIR/.claude-plugin"
|
||||
mkdir -p "$INSTALL_DIR/commands"
|
||||
|
||||
# Copy binaries
|
||||
cp "$tmp_dir/worker" "$INSTALL_DIR/"
|
||||
@@ -185,6 +186,11 @@ download_release() {
|
||||
# Copy plugin configuration
|
||||
cp "$tmp_dir/.claude-plugin/"* "$INSTALL_DIR/.claude-plugin/"
|
||||
|
||||
# Copy slash commands if they exist in the release
|
||||
if [[ -d "$tmp_dir/commands" ]]; then
|
||||
cp -r "$tmp_dir/commands/"* "$INSTALL_DIR/commands/" 2>/dev/null || true
|
||||
fi
|
||||
|
||||
# Make binaries executable
|
||||
chmod +x "$INSTALL_DIR/worker"
|
||||
chmod +x "$INSTALL_DIR/mcp-server"
|
||||
@@ -230,6 +236,7 @@ register_plugin() {
|
||||
# Copy files to cache directory
|
||||
mkdir -p "$cache_path/.claude-plugin"
|
||||
mkdir -p "$cache_path/hooks"
|
||||
mkdir -p "$cache_path/commands"
|
||||
cp -r "$INSTALL_DIR/"* "$cache_path/" 2>/dev/null || true
|
||||
|
||||
# Register in installed_plugins.json
|
||||
|
||||
@@ -45,6 +45,7 @@ if command -v jq &> /dev/null; then
|
||||
# Ensure cache directory exists and copy plugin files
|
||||
mkdir -p "$CACHE_PATH/.claude-plugin"
|
||||
mkdir -p "$CACHE_PATH/hooks"
|
||||
mkdir -p "$CACHE_PATH/commands"
|
||||
|
||||
# Copy files from marketplace to cache
|
||||
cp -r "$MARKETPLACE_PATH/"* "$CACHE_PATH/" 2>/dev/null || true
|
||||
|
||||
Generated
+2
-2
@@ -1,12 +1,12 @@
|
||||
{
|
||||
"name": "claude-mnemonic-dashboard",
|
||||
"version": "v0.6.11-2-g8ff0873-dirty",
|
||||
"version": "v0.6.15-2-gd8a4939-dirty",
|
||||
"lockfileVersion": 3,
|
||||
"requires": true,
|
||||
"packages": {
|
||||
"": {
|
||||
"name": "claude-mnemonic-dashboard",
|
||||
"version": "v0.6.11-2-g8ff0873-dirty",
|
||||
"version": "v0.6.15-2-gd8a4939-dirty",
|
||||
"dependencies": {
|
||||
"vue": "^3.5.13"
|
||||
},
|
||||
|
||||
+1
-1
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "claude-mnemonic-dashboard",
|
||||
"version": "v0.6.11-2-g8ff0873-dirty",
|
||||
"version": "v0.6.15-2-gd8a4939-dirty",
|
||||
"private": true,
|
||||
"type": "module",
|
||||
"scripts": {
|
||||
|
||||
+25
-1
@@ -7,7 +7,7 @@ import Timeline from '@/components/Timeline.vue'
|
||||
import Sidebar from '@/components/Sidebar.vue'
|
||||
|
||||
// Composables
|
||||
const { isConnected, isProcessing, queueDepth } = useSSE()
|
||||
const { isConnected, isReconnecting, reconnectCountdown, isProcessing, queueDepth } = useSSE()
|
||||
const { stats } = useStats()
|
||||
const { updateInfo, updateStatus, isUpdating, applyUpdate } = useUpdate()
|
||||
const { health } = useHealth()
|
||||
@@ -33,6 +33,17 @@ const {
|
||||
|
||||
<template>
|
||||
<div class="min-h-screen">
|
||||
<!-- Reconnection Banner -->
|
||||
<Transition name="slide">
|
||||
<div
|
||||
v-if="isReconnecting"
|
||||
class="fixed top-0 left-0 right-0 z-50 bg-amber-500/90 backdrop-blur-sm text-black px-4 py-2 text-center text-sm font-medium shadow-lg"
|
||||
>
|
||||
<i class="fas fa-sync-alt fa-spin mr-2" />
|
||||
Connection lost. Reconnecting<span v-if="reconnectCountdown > 0"> in {{ reconnectCountdown }}s</span>...
|
||||
</div>
|
||||
</Transition>
|
||||
|
||||
<!-- Header -->
|
||||
<Header
|
||||
:is-connected="isConnected"
|
||||
@@ -94,3 +105,16 @@ const {
|
||||
</main>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<style scoped>
|
||||
.slide-enter-active,
|
||||
.slide-leave-active {
|
||||
transition: transform 0.3s ease, opacity 0.3s ease;
|
||||
}
|
||||
|
||||
.slide-enter-from,
|
||||
.slide-leave-to {
|
||||
transform: translateY(-100%);
|
||||
opacity: 0;
|
||||
}
|
||||
</style>
|
||||
|
||||
@@ -3,12 +3,15 @@ import type { SSEEvent } from '@/types'
|
||||
|
||||
// Singleton state - shared across all useSSE() calls
|
||||
const isConnected = ref(false)
|
||||
const isReconnecting = ref(false)
|
||||
const reconnectCountdown = ref(0)
|
||||
const isProcessing = ref(false)
|
||||
const queueDepth = ref(0)
|
||||
const lastEvent = ref<SSEEvent | null>(null)
|
||||
|
||||
let eventSource: EventSource | null = null
|
||||
let reconnectTimeout: number | null = null
|
||||
let countdownInterval: number | null = null
|
||||
let connectionCount = 0
|
||||
let reconnectAttempt = 0
|
||||
|
||||
@@ -24,6 +27,28 @@ function getBackoffDelay(): number {
|
||||
return Math.floor(baseDelay + jitter)
|
||||
}
|
||||
|
||||
function startCountdown(delayMs: number) {
|
||||
reconnectCountdown.value = Math.ceil(delayMs / 1000)
|
||||
if (countdownInterval) {
|
||||
clearInterval(countdownInterval)
|
||||
}
|
||||
countdownInterval = window.setInterval(() => {
|
||||
reconnectCountdown.value = Math.max(0, reconnectCountdown.value - 1)
|
||||
if (reconnectCountdown.value <= 0 && countdownInterval) {
|
||||
clearInterval(countdownInterval)
|
||||
countdownInterval = null
|
||||
}
|
||||
}, 1000)
|
||||
}
|
||||
|
||||
function stopCountdown() {
|
||||
if (countdownInterval) {
|
||||
clearInterval(countdownInterval)
|
||||
countdownInterval = null
|
||||
}
|
||||
reconnectCountdown.value = 0
|
||||
}
|
||||
|
||||
export function useSSE() {
|
||||
|
||||
const connect = () => {
|
||||
@@ -36,6 +61,8 @@ export function useSSE() {
|
||||
|
||||
eventSource.onopen = () => {
|
||||
isConnected.value = true
|
||||
isReconnecting.value = false
|
||||
stopCountdown()
|
||||
reconnectAttempt = 0 // Reset backoff on successful connection
|
||||
console.log('[SSE] Connected')
|
||||
}
|
||||
@@ -62,6 +89,7 @@ export function useSSE() {
|
||||
|
||||
eventSource.onerror = () => {
|
||||
isConnected.value = false
|
||||
isReconnecting.value = true
|
||||
eventSource?.close()
|
||||
eventSource = null
|
||||
|
||||
@@ -70,6 +98,7 @@ export function useSSE() {
|
||||
reconnectAttempt++
|
||||
console.log(`[SSE] Reconnecting in ${Math.round(delay/1000)}s (attempt ${reconnectAttempt})`)
|
||||
|
||||
startCountdown(delay)
|
||||
reconnectTimeout = window.setTimeout(() => {
|
||||
connect()
|
||||
}, delay)
|
||||
@@ -81,11 +110,13 @@ export function useSSE() {
|
||||
clearTimeout(reconnectTimeout)
|
||||
reconnectTimeout = null
|
||||
}
|
||||
stopCountdown()
|
||||
if (eventSource) {
|
||||
eventSource.close()
|
||||
eventSource = null
|
||||
}
|
||||
isConnected.value = false
|
||||
isReconnecting.value = false
|
||||
}
|
||||
|
||||
// Handle page unload/refresh to ensure SSE connection is closed immediately
|
||||
@@ -132,6 +163,8 @@ export function useSSE() {
|
||||
|
||||
return {
|
||||
isConnected,
|
||||
isReconnecting,
|
||||
reconnectCountdown,
|
||||
isProcessing,
|
||||
queueDepth,
|
||||
lastEvent,
|
||||
|
||||
Reference in New Issue
Block a user