Compare commits

...

6 Commits

Author SHA1 Message Date
Alex Newman a22098d661 chore: Bump version to 6.3.6 2025-11-30 17:31:23 -05:00
Dmytro Gaivoronsky 69b17e15a2 feat: Auto-detect and rebuild native modules on Node.js version changes (#149)
Implements three-layer defense against native module version mismatches:

Layer 1: Node.js Version Tracking
- Track Node.js version alongside package version in .install-version marker
- Auto-trigger npm install when Node.js version changes
- Backward compatible with old plain-text version marker format

Layer 2: Native Module Verification
- Add verifyNativeModules() function to test better-sqlite3 loads correctly
- Verify after install completes to catch corrupted builds
- Retry with force flag if initial install verification fails

Layer 3: Graceful Failure
- Catch ERR_DLOPEN_FAILED in context-hook and delete version marker
- Exit cleanly to avoid error spam in Claude Code UI
- Auto-fix on next session start

Changes:
- scripts/smart-install.js: Add Node.js version tracking and verification
- src/hooks/context-hook.ts: Add graceful failure handling for native module errors
- tests/smart-install.test.js: Add tests for version marker format compatibility
- plugin/scripts/context-hook.js: Built output from TypeScript source

Fixes the issue where users see ERR_DLOPEN_FAILED errors after Node.js upgrades,
requiring manual npm install. Now automatically detects and fixes the issue.

Related design doc: docs/context/native-module-auto-fix-design.md
Implementation plan: docs/context/native-module-auto-fix-implementation.md

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-authored-by: Claude <noreply@anthropic.com>
Co-authored-by: Alex Newman <thedotmack@gmail.com>
2025-11-30 17:28:07 -05:00
Alex Newman de279ef6bf docs: Update CHANGELOG.md for v6.3.5
🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude <noreply@anthropic.com>
2025-11-30 17:10:28 -05:00
Alex Newman c6fed386ef chore: Bump version to 6.3.5
Update version across all tracking files for patch release.

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude <noreply@anthropic.com>
2025-11-30 17:08:33 -05:00
Alex Newman ffcd7d21b3 Restore Community Button and Responsive Mobile Navigation (#152)
* feat: Restore community button and responsive mobile navigation

Restores the Discord community button and responsive layout features from commit f117051:

- Community button in header with Discord icon and link
- Responsive breakpoints: community button moves to sidebar at 600px
- Projects dropdown moves to sidebar at 480px
- Sidebar proper width constraints (100% width, 400px max-width)
- Icon links use CSS classes instead of inline styles
- Full-height layout styling for proper flex behavior

This brings back the mobile-first navigation reorganization that creates
a Discord-like mobile experience where the sidebar becomes the primary
navigation container on smaller screens.

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude <noreply@anthropic.com>

* fix: Pass projects props to Sidebar component

The Sidebar component was trying to map over projects array but App.tsx
wasn't passing the projects, currentFilter, and onFilterChange props to
the Sidebar component. This caused a TypeError when the sidebar tried to
render the project filter dropdown.

Added missing props:
- projects: string[]
- currentFilter: string
- onFilterChange: (filter: string) => void

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude <noreply@anthropic.com>

* chore: Update UI build artifacts

Update compiled viewer bundle and templates after fixing Sidebar props.

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude <noreply@anthropic.com>

---------

Co-authored-by: Claude <noreply@anthropic.com>
2025-11-30 17:06:01 -05:00
Alex Newman efb7507a8f docs: Update CHANGELOG.md for v6.3.4
🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude <noreply@anthropic.com>
2025-11-30 16:09:42 -05:00
16 changed files with 1217 additions and 311 deletions
+1 -1
View File
@@ -10,7 +10,7 @@
"plugins": [
{
"name": "claude-mem",
"version": "6.3.4",
"version": "6.3.6",
"source": "./plugin",
"description": "Persistent memory system for Claude Code - context compression across sessions"
}
+36
View File
@@ -4,6 +4,42 @@ All notable changes to this project will be documented in this file.
The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/).
## [6.3.5] - 2025-11-30
## Changes
- ✨ Restored Discord community button in viewer header
- 📱 Added responsive mobile navigation menu
- 🔄 Reorganized Sidebar component for better mobile UX
- 🐛 Fixed missing props being passed to Sidebar component
## Technical Details
- Community button visible in header on desktop (> 600px width)
- Mobile menu icon appears on small screens (≤ 600px width)
- Sidebar toggles via hamburger menu on mobile
- Both buttons positioned in header for consistent UX
Full changelog: https://github.com/thedotmack/claude-mem/compare/v6.3.4...v6.3.5
## [6.3.4] - 2025-11-30
## Bug Fixes
### Worker Startup Improvements
Fixed critical issues with worker service startup on fresh installations:
- **Auto-start worker after installation** - The PM2 worker now starts automatically during plugin installation
- **Local PM2 resolution** - Plugin now uses local PM2 from node_modules/.bin instead of requiring global installation
- **Improved error messages** - Clear, actionable instructions with full paths when worker fails to start
- **Cross-platform support** - Proper handling of Windows platform differences (pm2.cmd)
- **Security enhancement** - Switched from execSync to spawnSync with array arguments to prevent command injection
These changes significantly improve the first-time installation experience, eliminating the need for manual PM2 setup.
**Special thanks to @dreamiurg for identifying and fixing this critical UX issue!** 🙏
## [6.3.3] - 2025-11-30
Bug fixes and improvements to timeline context feature:
+1 -1
View File
@@ -6,7 +6,7 @@
Claude-mem is a Claude Code plugin providing persistent memory across sessions. It captures tool usage, compresses observations using the Claude Agent SDK, and injects relevant context into future sessions.
**Current Version**: 6.3.4
**Current Version**: 6.3.6
## Architecture
+4
View File
@@ -1484,6 +1484,7 @@
"integrity": "sha512-RFA/bURkcKzx/X9oumPG9Vp3D3JUgus/d0b67KB0t5S/raciymilkOa66olh78MUI92QLbEJevO7rvqU/kjwKA==",
"dev": true,
"license": "MIT",
"peer": true,
"dependencies": {
"@types/prop-types": "*",
"csstype": "^3.0.2"
@@ -2337,6 +2338,7 @@
"resolved": "https://registry.npmjs.org/express/-/express-4.21.2.tgz",
"integrity": "sha512-28HqgMZAmih1Czt9ny7qr6ek2qddF4FclbMzwhCREB6OFfH+rXAnuNCwo1/wFvrtbgsQDb4kSbX9de9lFbrXnA==",
"license": "MIT",
"peer": true,
"dependencies": {
"accepts": "~1.3.8",
"array-flatten": "1.1.1",
@@ -3849,6 +3851,7 @@
"resolved": "https://registry.npmjs.org/react/-/react-18.3.1.tgz",
"integrity": "sha512-wS+hAgJShR0KhEvPJArfuPVN1+Hz1t0Y6n5jLrGQbkb4urgPE/0Rve+1kMB1v/oWgHgm4WIcV+i7F2pTVj+2iQ==",
"license": "MIT",
"peer": true,
"dependencies": {
"loose-envify": "^1.1.0"
},
@@ -4850,6 +4853,7 @@
"node_modules/zod": {
"version": "3.25.76",
"license": "MIT",
"peer": true,
"funding": {
"url": "https://github.com/sponsors/colinhacks"
}
+1 -1
View File
@@ -1,6 +1,6 @@
{
"name": "claude-mem",
"version": "6.3.4",
"version": "6.3.6",
"description": "Memory compression system for Claude Code - persist context across sessions",
"keywords": [
"claude",
+1 -1
View File
@@ -1,6 +1,6 @@
{
"name": "claude-mem",
"version": "6.3.4",
"version": "6.3.6",
"description": "Persistent memory system for Claude Code - seamlessly preserve context across sessions",
"author": {
"name": "Alex Newman"
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
+421 -21
View File
@@ -297,10 +297,9 @@
overflow: hidden;
}
.container {
.full-height-flex-layout {
display: flex;
height: 100vh;
width: 100vw;
height: 100%;
position: relative;
}
@@ -314,8 +313,9 @@
position: fixed;
right: 0;
top: 0;
width: 400px;
height: 100vh;
width: 100%;
max-width: 400px;
background: var(--color-bg-primary);
border-left: 1px solid var(--color-border-primary);
display: flex;
@@ -331,12 +331,16 @@
}
.header {
padding: 14px 18px;
padding: 16px 24px;
border-bottom: 1px solid var(--color-border-primary);
display: flex;
justify-content: space-between;
align-items: center;
background: var(--color-bg-header);
background: linear-gradient(to bottom,
var(--color-bg-header) 0%,
var(--color-bg-primary) 100%);
backdrop-filter: blur(8px);
box-shadow: 0 1px 3px rgba(0, 0, 0, 0.03);
}
.sidebar-header {
@@ -354,13 +358,124 @@
color: var(--color-text-header);
}
.sidebar-community-btn {
display: none;
background: var(--color-bg-card);
border: 1px solid var(--color-border-primary);
border-radius: 6px;
padding: 0 14px;
height: 36px;
cursor: pointer;
align-items: center;
justify-content: center;
color: var(--color-text-secondary);
font-size: 13px;
font-weight: 500;
text-decoration: none;
transition: all 0.2s cubic-bezier(0.4, 0, 0.2, 1);
white-space: nowrap;
box-shadow: 0 1px 2px rgba(0, 0, 0, 0.04);
margin: 16px 18px;
}
.sidebar-community-btn:hover {
background: var(--color-bg-card-hover);
border-color: var(--color-border-focus);
color: var(--color-text-primary);
transform: translateY(-1px);
box-shadow: 0 2px 4px rgba(0, 0, 0, 0.08);
}
.sidebar-community-btn:active {
transform: translateY(0);
box-shadow: 0 1px 2px rgba(0, 0, 0, 0.04);
}
@media (max-width: 600px) {
.sidebar-community-btn {
display: flex;
}
}
.sidebar-project-filter {
display: none;
padding: 16px 18px;
border-bottom: 1px solid var(--color-border-primary);
}
.sidebar-project-filter label {
display: block;
margin-bottom: 8px;
font-size: 12px;
color: var(--color-text-muted);
font-family: 'Monaco', 'Menlo', 'Consolas', monospace;
font-weight: 500;
}
.sidebar-project-filter select {
width: 100%;
background: var(--color-bg-card);
border: 1px solid var(--color-border-primary);
border-radius: 6px;
padding: 0 32px 0 12px;
height: 36px;
font-size: 13px;
font-weight: 500;
color: var(--color-text-secondary);
cursor: pointer;
transition: all 0.2s cubic-bezier(0.4, 0, 0.2, 1);
box-shadow: 0 1px 2px rgba(0, 0, 0, 0.04);
appearance: none;
background-image: url("data:image/svg+xml,%3Csvg width='12' height='12' viewBox='0 0 12 12' fill='none' xmlns='http://www.w3.org/2000/svg'%3E%3Cpath d='M3 4.5L6 7.5L9 4.5' stroke='%23666' stroke-width='1.5' stroke-linecap='round' stroke-linejoin='round'/%3E%3C/svg%3E");
background-repeat: no-repeat;
background-position: right 10px center;
}
.sidebar-project-filter select:hover {
background-color: var(--color-bg-card-hover);
border-color: var(--color-border-focus);
color: var(--color-text-primary);
}
.sidebar-project-filter select:focus {
outline: none;
border-color: var(--color-border-focus);
box-shadow: 0 0 0 3px rgba(9, 105, 218, 0.1);
}
@media (max-width: 480px) {
.sidebar-project-filter {
display: block;
}
}
.sidebar-social-links {
display: none;
padding: 16px 18px;
border-bottom: 1px solid var(--color-border-primary);
gap: 8px;
justify-content: center;
}
.sidebar-social-links .icon-link {
flex: 1;
max-width: 80px;
}
@media (max-width: 768px) {
.sidebar-social-links {
display: flex;
}
}
.header h1 {
font-size: 16px;
font-size: 17px;
font-weight: 500;
color: var(--color-text-header);
display: flex;
align-items: center;
gap: 10px;
gap: 12px;
line-height: 1;
}
@@ -385,7 +500,6 @@
font-size: 10px;
font-weight: 600;
font-family: 'Monaspace Radon', monospace;
min-width: 18px;
height: 18px;
border-radius: 9px;
display: flex;
@@ -409,43 +523,106 @@
.logo-text {
font-family: 'Monaspace Radon', monospace;
font-weight: 100;
font-size: 20px;
font-size: 21px;
letter-spacing: -0.03em;
color: var(--color-text-logo);
line-height: 1;
padding-top: 1px;
}
.status {
display: flex;
align-items: center;
gap: 12px;
gap: 8px;
font-size: 13px;
}
.settings-btn,
.theme-toggle-btn {
background: transparent;
background: var(--color-bg-card);
border: 1px solid var(--color-border-primary);
padding: 8px;
border-radius: 6px;
padding: 0;
width: 36px;
height: 36px;
cursor: pointer;
display: flex;
align-items: center;
justify-content: center;
color: var(--color-text-primary);
transition: all 0.15s ease;
color: var(--color-text-secondary);
transition: all 0.2s cubic-bezier(0.4, 0, 0.2, 1);
box-shadow: 0 1px 2px rgba(0, 0, 0, 0.04);
}
.settings-btn:hover,
.theme-toggle-btn:hover {
background: var(--color-bg-secondary);
background: var(--color-bg-card-hover);
border-color: var(--color-border-focus);
color: var(--color-text-primary);
transform: translateY(-1px);
box-shadow: 0 2px 4px rgba(0, 0, 0, 0.08);
}
.settings-btn.active {
background: var(--color-bg-button);
background: linear-gradient(135deg, var(--color-bg-button) 0%, var(--color-accent-primary) 100%);
border-color: var(--color-bg-button);
color: var(--color-text-button);
box-shadow: 0 2px 8px rgba(9, 105, 218, 0.25);
}
.community-btn {
background: var(--color-bg-card);
border: 1px solid var(--color-border-primary);
border-radius: 6px;
padding: 0 14px;
height: 36px;
cursor: pointer;
display: flex;
align-items: center;
justify-content: center;
color: var(--color-text-secondary);
font-size: 13px;
font-weight: 500;
text-decoration: none;
transition: all 0.2s cubic-bezier(0.4, 0, 0.2, 1);
white-space: nowrap;
box-shadow: 0 1px 2px rgba(0, 0, 0, 0.04);
}
.community-btn:hover {
background: var(--color-bg-card-hover);
border-color: var(--color-border-focus);
color: var(--color-text-primary);
transform: translateY(-1px);
box-shadow: 0 2px 4px rgba(0, 0, 0, 0.08);
}
.community-btn:active {
transform: translateY(0);
box-shadow: 0 1px 2px rgba(0, 0, 0, 0.04);
}
.icon-link {
display: flex;
align-items: center;
justify-content: center;
width: 36px;
height: 36px;
color: var(--color-text-secondary);
background: var(--color-bg-card);
border: 1px solid var(--color-border-primary);
border-radius: 6px;
text-decoration: none;
transition: all 0.2s cubic-bezier(0.4, 0, 0.2, 1);
box-shadow: 0 1px 2px rgba(0, 0, 0, 0.04);
}
.icon-link:hover {
background: var(--color-bg-card-hover);
border-color: var(--color-border-focus);
color: var(--color-text-primary);
transform: translateY(-1px);
box-shadow: 0 2px 4px rgba(0, 0, 0, 0.08);
}
.settings-icon,
@@ -492,6 +669,40 @@
transition: all 0.15s ease;
}
.status select {
background: var(--color-bg-card);
border: 1px solid var(--color-border-primary);
border-radius: 6px;
padding: 0 32px 0 12px;
height: 36px;
font-size: 13px;
font-weight: 500;
color: var(--color-text-secondary);
cursor: pointer;
transition: all 0.2s cubic-bezier(0.4, 0, 0.2, 1);
box-shadow: 0 1px 2px rgba(0, 0, 0, 0.04);
appearance: none;
background-image: url("data:image/svg+xml,%3Csvg width='12' height='12' viewBox='0 0 12 12' fill='none' xmlns='http://www.w3.org/2000/svg'%3E%3Cpath d='M3 4.5L6 7.5L9 4.5' stroke='%23666' stroke-width='1.5' stroke-linecap='round' stroke-linejoin='round'/%3E%3C/svg%3E");
background-repeat: no-repeat;
background-position: right 10px center;
max-width: 180px;
}
.status select:hover {
background-color: var(--color-bg-card-hover);
border-color: var(--color-border-focus);
color: var(--color-text-primary);
transform: translateY(-1px);
box-shadow: 0 2px 4px rgba(0, 0, 0, 0.08);
}
.status select:focus {
outline: none;
border-color: var(--color-border-focus);
box-shadow: 0 0 0 3px rgba(9, 105, 218, 0.1);
transform: translateY(-1px);
}
select:hover,
input:hover {
border-color: var(--color-border-focus);
@@ -527,15 +738,15 @@
.feed {
flex: 1;
overflow-y: auto;
overflow-y: scroll;
height: 100vh;
padding: 24px 18px;
display: flex;
justify-content: center;
}
.feed-content {
width: 100%;
max-width: 42rem;
max-width: 650px;
}
.card {
@@ -579,7 +790,6 @@
display: flex;
align-items: center;
gap: 10px;
min-width: 10%;
}
.card-subheading-left {
@@ -1190,6 +1400,196 @@
transform: translateY(0);
}
}
/* Utility: Container */
.container {
width: 100%;
max-width: 600px;
margin: 0 auto;
}
/* Tablet Responsive Styles - 481px to 768px */
@media (max-width: 768px) and (min-width: 481px) {
/* Header stays on one line, hide icon links to save space */
.header {
padding: 14px 20px;
}
.status {
gap: 6px;
}
.status select {
max-width: 160px;
}
/* Hide icon links (docs, github, twitter) on tablet */
.icon-link {
display: none;
}
/* Sidebar full width on tablet */
.sidebar {
}
/* Feed adjustments */
.feed {
padding: 20px 16px;
}
.feed-content {
}
/* Card adjustments */
.card {
padding: 20px;
}
}
/* Mobile & Small Tablet - 600px and below */
@media (max-width: 600px) {
/* Hide community button in header, will show in sidebar */
.community-btn {
display: none;
}
}
/* Mobile Responsive Styles - 480px and below */
@media (max-width: 480px) {
/* Hide project dropdown in header, will show in sidebar */
.status select {
display: none;
}
/* Header stays on one line */
.header {
padding: 12px 16px;
}
.header h1 {
font-size: 15px;
gap: 8px;
}
.logomark {
height: 28px;
}
.logo-text {
font-size: 18px;
}
.status {
display: flex;
gap: 6px;
overflow-x: auto;
overflow-y: hidden;
-webkit-overflow-scrolling: touch;
scrollbar-width: none;
padding-bottom: 4px;
}
.status::-webkit-scrollbar {
display: none;
}
.status select {
max-width: 140px;
flex-shrink: 0;
padding: 0 28px 0 10px;
height: 32px;
font-size: 12px;
}
/* Hide icon links on mobile */
.icon-link {
display: none;
}
.settings-btn,
.theme-toggle-btn,
.icon-link {
width: 32px;
height: 32px;
flex-shrink: 0;
}
.community-btn {
height: 32px;
padding: 0 12px;
font-size: 12px;
flex-shrink: 0;
}
.community-btn svg {
width: 12px;
height: 12px;
}
.settings-icon,
.theme-toggle-btn svg,
.icon-link svg {
width: 16px;
height: 16px;
}
/* Sidebar adjustments for mobile */
.sidebar {
}
.sidebar-header {
padding: 12px 16px;
}
.settings-section {
padding: 16px;
}
/* Feed adjustments */
.feed {
padding: 16px 12px;
}
/* Card adjustments */
.card {
padding: 16px;
margin-bottom: 16px;
}
.card-title {
font-size: 15px;
}
.card-header {
flex-wrap: wrap;
gap: 8px;
}
.card-header-left {
flex-wrap: wrap;
}
/* Stats grid to single column */
.stats-grid {
grid-template-columns: 1fr;
}
/* Form inputs full width */
.form-group input,
.form-group select {
width: 100%;
}
/* Scroll to top button position */
.scroll-to-top {
bottom: 16px;
right: 16px;
width: 44px;
height: 44px;
}
}
</style>
</head>
+119 -16
View File
@@ -51,10 +51,31 @@ function getPackageVersion() {
}
}
function getNodeVersion() {
return process.version; // e.g., "v22.21.1"
}
function getInstalledVersion() {
try {
if (existsSync(VERSION_MARKER_PATH)) {
return readFileSync(VERSION_MARKER_PATH, 'utf-8').trim();
const content = readFileSync(VERSION_MARKER_PATH, 'utf-8').trim();
// Try parsing as JSON (new format)
try {
const marker = JSON.parse(content);
return {
packageVersion: marker.packageVersion,
nodeVersion: marker.nodeVersion,
installedAt: marker.installedAt
};
} catch {
// Fallback: old format (plain text version string)
return {
packageVersion: content,
nodeVersion: null, // Unknown
installedAt: null
};
}
}
} catch (error) {
// Marker doesn't exist or can't be read
@@ -62,9 +83,14 @@ function getInstalledVersion() {
return null;
}
function setInstalledVersion(version) {
function setInstalledVersion(packageVersion, nodeVersion) {
try {
writeFileSync(VERSION_MARKER_PATH, version, 'utf-8');
const marker = {
packageVersion,
nodeVersion,
installedAt: new Date().toISOString()
};
writeFileSync(VERSION_MARKER_PATH, JSON.stringify(marker, null, 2), 'utf-8');
} catch (error) {
log(`⚠️ Failed to write version marker: ${error.message}`, colors.yellow);
}
@@ -84,24 +110,77 @@ function needsInstall() {
}
// Check version marker
const currentVersion = getPackageVersion();
const installedVersion = getInstalledVersion();
const currentPackageVersion = getPackageVersion();
const currentNodeVersion = getNodeVersion();
const installed = getInstalledVersion();
if (!installedVersion) {
if (!installed) {
log('📦 No version marker found - installing', colors.cyan);
return true;
}
if (currentVersion !== installedVersion) {
log(`📦 Version changed (${installedVersion}${currentVersion}) - updating`, colors.cyan);
// Check package version
if (currentPackageVersion !== installed.packageVersion) {
log(`📦 Version changed (${installed.packageVersion}${currentPackageVersion}) - updating`, colors.cyan);
return true;
}
// Check Node.js version
if (installed.nodeVersion && currentNodeVersion !== installed.nodeVersion) {
log(`📦 Node.js version changed (${installed.nodeVersion}${currentNodeVersion}) - rebuilding native modules`, colors.cyan);
return true;
}
// If old format (no nodeVersion), assume needs install
if (!installed.nodeVersion) {
log('📦 Old version marker format - updating', colors.cyan);
return true;
}
// All good - no install needed
log(`✓ Dependencies already installed (v${currentVersion})`, colors.dim);
log(`✓ Dependencies already installed (v${currentPackageVersion})`, colors.dim);
return false;
}
/**
* Verify that better-sqlite3 native module loads correctly
* This catches ABI mismatches and corrupted builds
*/
async function verifyNativeModules() {
try {
log('🔍 Verifying native modules...', colors.dim);
// Try to actually load better-sqlite3
const { default: Database } = await import('better-sqlite3');
// Try to create a test in-memory database
const db = new Database(':memory:');
// Run a simple query to ensure it works
const result = db.prepare('SELECT 1 + 1 as result').get();
// Clean up
db.close();
if (result.result !== 2) {
throw new Error('SQLite math check failed');
}
log('✓ Native modules verified', colors.dim);
return true;
} catch (error) {
if (error.code === 'ERR_DLOPEN_FAILED') {
log('⚠️ Native module ABI mismatch detected', colors.yellow);
return false;
}
// Other errors are unexpected - log and fail
log(`❌ Native module verification failed: ${error.message}`, colors.red);
return false;
}
}
function getWindowsErrorHelp(errorOutput) {
// Detect Python version at runtime
let pythonStatus = ' Python not detected or version unknown';
@@ -157,7 +236,7 @@ function getWindowsErrorHelp(errorOutput) {
return help.join('\n');
}
function runNpmInstall() {
async function runNpmInstall() {
const isWindows = process.platform === 'win32';
log('', colors.cyan);
@@ -175,7 +254,7 @@ function runNpmInstall() {
for (const { command, label } of strategies) {
try {
log(`Attempting install ${label}...`, colors.dim);
// Run npm install silently
execSync(command, {
cwd: PLUGIN_ROOT,
@@ -188,12 +267,20 @@ function runNpmInstall() {
throw new Error('better-sqlite3 installation verification failed');
}
const version = getPackageVersion();
setInstalledVersion(version);
// NEW: Verify native modules actually work
const nativeModulesWork = await verifyNativeModules();
if (!nativeModulesWork) {
throw new Error('Native modules failed to load after install');
}
const packageVersion = getPackageVersion();
const nodeVersion = getNodeVersion();
setInstalledVersion(packageVersion, nodeVersion);
log('', colors.green);
log('✅ Dependencies installed successfully!', colors.bright);
log(` Version: ${version}`, colors.dim);
log(` Package version: ${packageVersion}`, colors.dim);
log(` Node.js version: ${nodeVersion}`, colors.dim);
log('', colors.reset);
return true;
@@ -251,8 +338,8 @@ async function main() {
const installNeeded = needsInstall();
if (installNeeded) {
// Run installation
const installSuccess = runNpmInstall();
// Run installation (now async)
const installSuccess = await runNpmInstall();
if (!installSuccess) {
log('', colors.red);
@@ -260,6 +347,22 @@ async function main() {
log('', colors.reset);
process.exit(1);
}
} else {
// NEW: Even if install not needed, verify native modules work
const nativeModulesWork = await verifyNativeModules();
if (!nativeModulesWork) {
log('📦 Native modules need rebuild - reinstalling', colors.cyan);
const installSuccess = await runNpmInstall();
if (!installSuccess) {
log('', colors.red);
log('⚠️ Native module rebuild failed', colors.yellow);
log('', colors.reset);
process.exit(1);
}
}
}
// Try to start the PM2 worker after fresh install
try {
+32 -2
View File
@@ -5,10 +5,20 @@
import path from 'path';
import { homedir } from 'os';
import { existsSync, readFileSync } from 'fs';
import { existsSync, readFileSync, unlinkSync } from 'fs';
import { stdin } from 'process';
import { fileURLToPath } from 'url';
import { dirname } from 'path';
import { SessionStore } from '../services/sqlite/SessionStore.js';
// Get __dirname equivalent in ESM
const __filename = fileURLToPath(import.meta.url);
const __dirname = dirname(__filename);
// Version marker path (same as smart-install.js)
// From src/hooks/ we need to go up to plugin root: ../../
const VERSION_MARKER_PATH = path.join(__dirname, '../../.install-version');
/**
* Get context depth from settings
* Priority: ~/.claude/settings.json > env var > default
@@ -156,7 +166,27 @@ async function contextHook(input?: SessionStartInput, useColors: boolean = false
const cwd = input?.cwd ?? process.cwd();
const project = cwd ? path.basename(cwd) : 'unknown-project';
const db = new SessionStore();
let db: SessionStore;
try {
db = new SessionStore();
} catch (error: any) {
if (error.code === 'ERR_DLOPEN_FAILED') {
// Native module ABI mismatch - delete version marker to trigger reinstall
try {
unlinkSync(VERSION_MARKER_PATH);
} catch (unlinkError) {
// Marker might not exist, that's okay
}
// Log once (not error spam) and exit cleanly
console.error('⚠️ Native module rebuild needed - restart Claude Code to auto-fix');
console.error(' (This happens after Node.js version upgrades)');
process.exit(0); // Exit cleanly to avoid error spam
}
// Other errors should still throw
throw error;
}
// Get ALL recent observations for this project (not filtered by summaries)
// This ensures we show observations even when summaries haven't been generated
+421 -21
View File
@@ -297,10 +297,9 @@
overflow: hidden;
}
.container {
.full-height-flex-layout {
display: flex;
height: 100vh;
width: 100vw;
height: 100%;
position: relative;
}
@@ -314,8 +313,9 @@
position: fixed;
right: 0;
top: 0;
width: 400px;
height: 100vh;
width: 100%;
max-width: 400px;
background: var(--color-bg-primary);
border-left: 1px solid var(--color-border-primary);
display: flex;
@@ -331,12 +331,16 @@
}
.header {
padding: 14px 18px;
padding: 16px 24px;
border-bottom: 1px solid var(--color-border-primary);
display: flex;
justify-content: space-between;
align-items: center;
background: var(--color-bg-header);
background: linear-gradient(to bottom,
var(--color-bg-header) 0%,
var(--color-bg-primary) 100%);
backdrop-filter: blur(8px);
box-shadow: 0 1px 3px rgba(0, 0, 0, 0.03);
}
.sidebar-header {
@@ -354,13 +358,124 @@
color: var(--color-text-header);
}
.sidebar-community-btn {
display: none;
background: var(--color-bg-card);
border: 1px solid var(--color-border-primary);
border-radius: 6px;
padding: 0 14px;
height: 36px;
cursor: pointer;
align-items: center;
justify-content: center;
color: var(--color-text-secondary);
font-size: 13px;
font-weight: 500;
text-decoration: none;
transition: all 0.2s cubic-bezier(0.4, 0, 0.2, 1);
white-space: nowrap;
box-shadow: 0 1px 2px rgba(0, 0, 0, 0.04);
margin: 16px 18px;
}
.sidebar-community-btn:hover {
background: var(--color-bg-card-hover);
border-color: var(--color-border-focus);
color: var(--color-text-primary);
transform: translateY(-1px);
box-shadow: 0 2px 4px rgba(0, 0, 0, 0.08);
}
.sidebar-community-btn:active {
transform: translateY(0);
box-shadow: 0 1px 2px rgba(0, 0, 0, 0.04);
}
@media (max-width: 600px) {
.sidebar-community-btn {
display: flex;
}
}
.sidebar-project-filter {
display: none;
padding: 16px 18px;
border-bottom: 1px solid var(--color-border-primary);
}
.sidebar-project-filter label {
display: block;
margin-bottom: 8px;
font-size: 12px;
color: var(--color-text-muted);
font-family: 'Monaco', 'Menlo', 'Consolas', monospace;
font-weight: 500;
}
.sidebar-project-filter select {
width: 100%;
background: var(--color-bg-card);
border: 1px solid var(--color-border-primary);
border-radius: 6px;
padding: 0 32px 0 12px;
height: 36px;
font-size: 13px;
font-weight: 500;
color: var(--color-text-secondary);
cursor: pointer;
transition: all 0.2s cubic-bezier(0.4, 0, 0.2, 1);
box-shadow: 0 1px 2px rgba(0, 0, 0, 0.04);
appearance: none;
background-image: url("data:image/svg+xml,%3Csvg width='12' height='12' viewBox='0 0 12 12' fill='none' xmlns='http://www.w3.org/2000/svg'%3E%3Cpath d='M3 4.5L6 7.5L9 4.5' stroke='%23666' stroke-width='1.5' stroke-linecap='round' stroke-linejoin='round'/%3E%3C/svg%3E");
background-repeat: no-repeat;
background-position: right 10px center;
}
.sidebar-project-filter select:hover {
background-color: var(--color-bg-card-hover);
border-color: var(--color-border-focus);
color: var(--color-text-primary);
}
.sidebar-project-filter select:focus {
outline: none;
border-color: var(--color-border-focus);
box-shadow: 0 0 0 3px rgba(9, 105, 218, 0.1);
}
@media (max-width: 480px) {
.sidebar-project-filter {
display: block;
}
}
.sidebar-social-links {
display: none;
padding: 16px 18px;
border-bottom: 1px solid var(--color-border-primary);
gap: 8px;
justify-content: center;
}
.sidebar-social-links .icon-link {
flex: 1;
max-width: 80px;
}
@media (max-width: 768px) {
.sidebar-social-links {
display: flex;
}
}
.header h1 {
font-size: 16px;
font-size: 17px;
font-weight: 500;
color: var(--color-text-header);
display: flex;
align-items: center;
gap: 10px;
gap: 12px;
line-height: 1;
}
@@ -385,7 +500,6 @@
font-size: 10px;
font-weight: 600;
font-family: 'Monaspace Radon', monospace;
min-width: 18px;
height: 18px;
border-radius: 9px;
display: flex;
@@ -409,43 +523,106 @@
.logo-text {
font-family: 'Monaspace Radon', monospace;
font-weight: 100;
font-size: 20px;
font-size: 21px;
letter-spacing: -0.03em;
color: var(--color-text-logo);
line-height: 1;
padding-top: 1px;
}
.status {
display: flex;
align-items: center;
gap: 12px;
gap: 8px;
font-size: 13px;
}
.settings-btn,
.theme-toggle-btn {
background: transparent;
background: var(--color-bg-card);
border: 1px solid var(--color-border-primary);
padding: 8px;
border-radius: 6px;
padding: 0;
width: 36px;
height: 36px;
cursor: pointer;
display: flex;
align-items: center;
justify-content: center;
color: var(--color-text-primary);
transition: all 0.15s ease;
color: var(--color-text-secondary);
transition: all 0.2s cubic-bezier(0.4, 0, 0.2, 1);
box-shadow: 0 1px 2px rgba(0, 0, 0, 0.04);
}
.settings-btn:hover,
.theme-toggle-btn:hover {
background: var(--color-bg-secondary);
background: var(--color-bg-card-hover);
border-color: var(--color-border-focus);
color: var(--color-text-primary);
transform: translateY(-1px);
box-shadow: 0 2px 4px rgba(0, 0, 0, 0.08);
}
.settings-btn.active {
background: var(--color-bg-button);
background: linear-gradient(135deg, var(--color-bg-button) 0%, var(--color-accent-primary) 100%);
border-color: var(--color-bg-button);
color: var(--color-text-button);
box-shadow: 0 2px 8px rgba(9, 105, 218, 0.25);
}
.community-btn {
background: var(--color-bg-card);
border: 1px solid var(--color-border-primary);
border-radius: 6px;
padding: 0 14px;
height: 36px;
cursor: pointer;
display: flex;
align-items: center;
justify-content: center;
color: var(--color-text-secondary);
font-size: 13px;
font-weight: 500;
text-decoration: none;
transition: all 0.2s cubic-bezier(0.4, 0, 0.2, 1);
white-space: nowrap;
box-shadow: 0 1px 2px rgba(0, 0, 0, 0.04);
}
.community-btn:hover {
background: var(--color-bg-card-hover);
border-color: var(--color-border-focus);
color: var(--color-text-primary);
transform: translateY(-1px);
box-shadow: 0 2px 4px rgba(0, 0, 0, 0.08);
}
.community-btn:active {
transform: translateY(0);
box-shadow: 0 1px 2px rgba(0, 0, 0, 0.04);
}
.icon-link {
display: flex;
align-items: center;
justify-content: center;
width: 36px;
height: 36px;
color: var(--color-text-secondary);
background: var(--color-bg-card);
border: 1px solid var(--color-border-primary);
border-radius: 6px;
text-decoration: none;
transition: all 0.2s cubic-bezier(0.4, 0, 0.2, 1);
box-shadow: 0 1px 2px rgba(0, 0, 0, 0.04);
}
.icon-link:hover {
background: var(--color-bg-card-hover);
border-color: var(--color-border-focus);
color: var(--color-text-primary);
transform: translateY(-1px);
box-shadow: 0 2px 4px rgba(0, 0, 0, 0.08);
}
.settings-icon,
@@ -492,6 +669,40 @@
transition: all 0.15s ease;
}
.status select {
background: var(--color-bg-card);
border: 1px solid var(--color-border-primary);
border-radius: 6px;
padding: 0 32px 0 12px;
height: 36px;
font-size: 13px;
font-weight: 500;
color: var(--color-text-secondary);
cursor: pointer;
transition: all 0.2s cubic-bezier(0.4, 0, 0.2, 1);
box-shadow: 0 1px 2px rgba(0, 0, 0, 0.04);
appearance: none;
background-image: url("data:image/svg+xml,%3Csvg width='12' height='12' viewBox='0 0 12 12' fill='none' xmlns='http://www.w3.org/2000/svg'%3E%3Cpath d='M3 4.5L6 7.5L9 4.5' stroke='%23666' stroke-width='1.5' stroke-linecap='round' stroke-linejoin='round'/%3E%3C/svg%3E");
background-repeat: no-repeat;
background-position: right 10px center;
max-width: 180px;
}
.status select:hover {
background-color: var(--color-bg-card-hover);
border-color: var(--color-border-focus);
color: var(--color-text-primary);
transform: translateY(-1px);
box-shadow: 0 2px 4px rgba(0, 0, 0, 0.08);
}
.status select:focus {
outline: none;
border-color: var(--color-border-focus);
box-shadow: 0 0 0 3px rgba(9, 105, 218, 0.1);
transform: translateY(-1px);
}
select:hover,
input:hover {
border-color: var(--color-border-focus);
@@ -527,15 +738,15 @@
.feed {
flex: 1;
overflow-y: auto;
overflow-y: scroll;
height: 100vh;
padding: 24px 18px;
display: flex;
justify-content: center;
}
.feed-content {
width: 100%;
max-width: 42rem;
max-width: 650px;
}
.card {
@@ -579,7 +790,6 @@
display: flex;
align-items: center;
gap: 10px;
min-width: 10%;
}
.card-subheading-left {
@@ -1190,6 +1400,196 @@
transform: translateY(0);
}
}
/* Utility: Container */
.container {
width: 100%;
max-width: 600px;
margin: 0 auto;
}
/* Tablet Responsive Styles - 481px to 768px */
@media (max-width: 768px) and (min-width: 481px) {
/* Header stays on one line, hide icon links to save space */
.header {
padding: 14px 20px;
}
.status {
gap: 6px;
}
.status select {
max-width: 160px;
}
/* Hide icon links (docs, github, twitter) on tablet */
.icon-link {
display: none;
}
/* Sidebar full width on tablet */
.sidebar {
}
/* Feed adjustments */
.feed {
padding: 20px 16px;
}
.feed-content {
}
/* Card adjustments */
.card {
padding: 20px;
}
}
/* Mobile & Small Tablet - 600px and below */
@media (max-width: 600px) {
/* Hide community button in header, will show in sidebar */
.community-btn {
display: none;
}
}
/* Mobile Responsive Styles - 480px and below */
@media (max-width: 480px) {
/* Hide project dropdown in header, will show in sidebar */
.status select {
display: none;
}
/* Header stays on one line */
.header {
padding: 12px 16px;
}
.header h1 {
font-size: 15px;
gap: 8px;
}
.logomark {
height: 28px;
}
.logo-text {
font-size: 18px;
}
.status {
display: flex;
gap: 6px;
overflow-x: auto;
overflow-y: hidden;
-webkit-overflow-scrolling: touch;
scrollbar-width: none;
padding-bottom: 4px;
}
.status::-webkit-scrollbar {
display: none;
}
.status select {
max-width: 140px;
flex-shrink: 0;
padding: 0 28px 0 10px;
height: 32px;
font-size: 12px;
}
/* Hide icon links on mobile */
.icon-link {
display: none;
}
.settings-btn,
.theme-toggle-btn,
.icon-link {
width: 32px;
height: 32px;
flex-shrink: 0;
}
.community-btn {
height: 32px;
padding: 0 12px;
font-size: 12px;
flex-shrink: 0;
}
.community-btn svg {
width: 12px;
height: 12px;
}
.settings-icon,
.theme-toggle-btn svg,
.icon-link svg {
width: 16px;
height: 16px;
}
/* Sidebar adjustments for mobile */
.sidebar {
}
.sidebar-header {
padding: 12px 16px;
}
.settings-section {
padding: 16px;
}
/* Feed adjustments */
.feed {
padding: 16px 12px;
}
/* Card adjustments */
.card {
padding: 16px;
margin-bottom: 16px;
}
.card-title {
font-size: 15px;
}
.card-header {
flex-wrap: wrap;
gap: 8px;
}
.card-header-left {
flex-wrap: wrap;
}
/* Stats grid to single column */
.stats-grid {
grid-template-columns: 1fr;
}
/* Form inputs full width */
.form-group input,
.form-group select {
width: 100%;
}
/* Scroll to top button position */
.scroll-to-top {
bottom: 16px;
right: 16px;
width: 44px;
height: 44px;
}
}
</style>
</head>
+28 -24
View File
@@ -86,29 +86,30 @@ export function App() {
}, [currentFilter]);
return (
<div className="container">
<div className="main-col">
<Header
isConnected={isConnected}
projects={projects}
currentFilter={currentFilter}
onFilterChange={setCurrentFilter}
onSettingsToggle={toggleSidebar}
sidebarOpen={sidebarOpen}
isProcessing={isProcessing}
queueDepth={queueDepth}
themePreference={preference}
onThemeChange={setThemePreference}
/>
<Feed
observations={allObservations}
summaries={allSummaries}
prompts={allPrompts}
onLoadMore={handleLoadMore}
isLoading={pagination.observations.isLoading || pagination.summaries.isLoading || pagination.prompts.isLoading}
hasMore={pagination.observations.hasMore || pagination.summaries.hasMore || pagination.prompts.hasMore}
/>
</div>
<>
<Header
isConnected={isConnected}
projects={projects}
currentFilter={currentFilter}
onFilterChange={setCurrentFilter}
onSettingsToggle={toggleSidebar}
sidebarOpen={sidebarOpen}
isProcessing={isProcessing}
queueDepth={queueDepth}
themePreference={preference}
onThemeChange={setThemePreference}
/>
<Feed
observations={allObservations}
summaries={allSummaries}
prompts={allPrompts}
onLoadMore={handleLoadMore}
isLoading={pagination.observations.isLoading || pagination.summaries.isLoading || pagination.prompts.isLoading}
hasMore={pagination.observations.hasMore || pagination.summaries.hasMore || pagination.prompts.hasMore}
/>
<Sidebar
isOpen={sidebarOpen}
@@ -117,10 +118,13 @@ export function App() {
isSaving={isSaving}
saveStatus={saveStatus}
isConnected={isConnected}
projects={projects}
currentFilter={currentFilter}
onFilterChange={setCurrentFilter}
onSave={saveSettings}
onClose={toggleSidebar}
onRefreshStats={refreshStats}
/>
</div>
</>
);
}
+15 -27
View File
@@ -46,15 +46,7 @@ export function Header({
target="_blank"
rel="noopener noreferrer"
title="Documentation"
style={{
display: 'block',
padding: '8px 4px 8px 8px',
color: '#a0a0a0',
transition: 'color 0.2s',
lineHeight: 0
}}
onMouseEnter={(e) => e.currentTarget.style.color = '#606060'}
onMouseLeave={(e) => e.currentTarget.style.color = '#a0a0a0'}
className="icon-link"
>
<svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round">
<path d="M4 19.5A2.5 2.5 0 0 1 6.5 17H20"></path>
@@ -66,15 +58,7 @@ export function Header({
target="_blank"
rel="noopener noreferrer"
title="GitHub"
style={{
display: 'block',
padding: '8px 4px',
color: '#a0a0a0',
transition: 'color 0.2s',
lineHeight: 0
}}
onMouseEnter={(e) => e.currentTarget.style.color = '#606060'}
onMouseLeave={(e) => e.currentTarget.style.color = '#a0a0a0'}
className="icon-link"
>
<svg width="16" height="16" viewBox="0 0 24 24" fill="currentColor">
<path d="M12 0c-6.626 0-12 5.373-12 12 0 5.302 3.438 9.8 8.207 11.387.599.111.793-.261.793-.577v-2.234c-3.338.726-4.033-1.416-4.033-1.416-.546-1.387-1.333-1.756-1.333-1.756-1.089-.745.083-.729.083-.729 1.205.084 1.839 1.237 1.839 1.237 1.07 1.834 2.807 1.304 3.492.997.107-.775.418-1.305.762-1.604-2.665-.305-5.467-1.334-5.467-5.931 0-1.311.469-2.381 1.236-3.221-.124-.303-.535-1.524.117-3.176 0 0 1.008-.322 3.301 1.23.957-.266 1.983-.399 3.003-.404 1.02.005 2.047.138 3.006.404 2.291-1.552 3.297-1.23 3.297-1.23.653 1.653.242 2.874.118 3.176.77.84 1.235 1.911 1.235 3.221 0 4.609-2.807 5.624-5.479 5.921.43.372.823 1.102.823 2.222v3.293c0 .319.192.694.801.576 4.765-1.589 8.199-6.086 8.199-11.386 0-6.627-5.373-12-12-12z"/>
@@ -85,20 +69,24 @@ export function Header({
target="_blank"
rel="noopener noreferrer"
title="X (Twitter)"
style={{
display: 'block',
padding: '8px 8px 8px 4px',
color: '#a0a0a0',
transition: 'color 0.2s',
lineHeight: 0
}}
onMouseEnter={(e) => e.currentTarget.style.color = '#606060'}
onMouseLeave={(e) => e.currentTarget.style.color = '#a0a0a0'}
className="icon-link"
>
<svg width="16" height="16" viewBox="0 0 24 24" fill="currentColor">
<path d="M18.244 2.25h3.308l-7.227 8.26 8.502 11.24H16.17l-5.214-6.817L4.99 21.75H1.68l7.73-8.835L1.254 2.25H8.08l4.713 6.231zm-1.161 17.52h1.833L7.084 4.126H5.117z"/>
</svg>
</a>
<a
href="https://discord.gg/J4wttp9vDu"
target="_blank"
rel="noopener noreferrer"
className="community-btn"
title="Join our Discord community"
>
<svg width="14" height="14" viewBox="0 0 24 24" fill="currentColor" style={{ marginRight: '6px' }}>
<path d="M20.317 4.37a19.791 19.791 0 0 0-4.885-1.515a.074.074 0 0 0-.079.037c-.21.375-.444.864-.608 1.25a18.27 18.27 0 0 0-5.487 0a12.64 12.64 0 0 0-.617-1.25a.077.077 0 0 0-.079-.037A19.736 19.736 0 0 0 3.677 4.37a.07.07 0 0 0-.032.027C.533 9.046-.32 13.58.099 18.057a.082.082 0 0 0 .031.057a19.9 19.9 0 0 0 5.993 3.03a.078.078 0 0 0 .084-.028a14.09 14.09 0 0 0 1.226-1.994a.076.076 0 0 0-.041-.106a13.107 13.107 0 0 1-1.872-.892a.077.077 0 0 1-.008-.128a10.2 10.2 0 0 0 .372-.292a.074.074 0 0 1 .077-.01c3.928 1.793 8.18 1.793 12.062 0a.074.074 0 0 1 .078.01c.12.098.246.198.373.292a.077.077 0 0 1-.006.127a12.299 12.299 0 0 1-1.873.892a.077.077 0 0 0-.041.107c.36.698.772 1.362 1.225 1.993a.076.076 0 0 0 .084.028a19.839 19.839 0 0 0 6.002-3.03a.077.077 0 0 0 .032-.054c.5-5.177-.838-9.674-3.549-13.66a.061.061 0 0 0-.031-.03zM8.02 15.33c-1.183 0-2.157-1.085-2.157-2.419c0-1.333.956-2.419 2.157-2.419c1.21 0 2.176 1.096 2.157 2.42c0 1.333-.956 2.418-2.157 2.418zm7.975 0c-1.183 0-2.157-1.085-2.157-2.419c0-1.333.955-2.419 2.157-2.419c1.21 0 2.176 1.096 2.157 2.42c0 1.333-.946 2.418-2.157 2.418z"/>
</svg>
<span>Community</span>
</a>
<select
value={currentFilter}
onChange={e => onFilterChange(e.target.value)}
+65 -171
View File
@@ -10,12 +10,15 @@ interface SidebarProps {
isSaving: boolean;
saveStatus: string;
isConnected: boolean;
projects: string[];
currentFilter: string;
onFilterChange: (filter: string) => void;
onSave: (settings: Settings) => void;
onClose: () => void;
onRefreshStats: () => void;
}
export function Sidebar({ isOpen, settings, stats, isSaving, saveStatus, isConnected, onSave, onClose, onRefreshStats }: SidebarProps) {
export function Sidebar({ isOpen, settings, stats, isSaving, saveStatus, isConnected, projects, currentFilter, onFilterChange, onSave, onClose, onRefreshStats }: SidebarProps) {
// Settings form state
const [model, setModel] = useState(settings.CLAUDE_MEM_MODEL || DEFAULT_SETTINGS.CLAUDE_MEM_MODEL);
const [contextObs, setContextObs] = useState(settings.CLAUDE_MEM_CONTEXT_OBSERVATIONS || DEFAULT_SETTINGS.CLAUDE_MEM_CONTEXT_OBSERVATIONS);
@@ -26,19 +29,6 @@ export function Sidebar({ isOpen, settings, stats, isSaving, saveStatus, isConne
const [mcpToggling, setMcpToggling] = useState(false);
const [mcpStatus, setMcpStatus] = useState('');
// Branch switching state
interface BranchInfo {
branch: string | null;
isBeta: boolean;
isGitRepo: boolean;
isDirty: boolean;
canSwitch: boolean;
error?: string;
}
const [branchInfo, setBranchInfo] = useState<BranchInfo | null>(null);
const [branchSwitching, setBranchSwitching] = useState(false);
const [branchStatus, setBranchStatus] = useState('');
// Update settings form state when settings change
useEffect(() => {
setModel(settings.CLAUDE_MEM_MODEL || DEFAULT_SETTINGS.CLAUDE_MEM_MODEL);
@@ -54,14 +44,6 @@ export function Sidebar({ isOpen, settings, stats, isSaving, saveStatus, isConne
.catch(error => console.error('Failed to load MCP status:', error));
}, []);
// Fetch branch status on mount
useEffect(() => {
fetch('/api/branch/status')
.then(res => res.json())
.then(data => setBranchInfo(data))
.catch(error => console.error('Failed to load branch status:', error));
}, []);
// Refresh stats when sidebar opens
useEffect(() => {
if (isOpen) {
@@ -106,67 +88,6 @@ export function Sidebar({ isOpen, settings, stats, isSaving, saveStatus, isConne
}
};
const handleBranchSwitch = async (targetBranch: string) => {
setBranchSwitching(true);
setBranchStatus('Switching branches...');
try {
const response = await fetch('/api/branch/switch', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ branch: targetBranch })
});
const result = await response.json();
if (result.success) {
setBranchStatus(`${result.message}`);
// Worker will restart, page will refresh
setTimeout(() => {
setBranchStatus('Restarting worker...');
}, 1000);
} else {
setBranchStatus(`✗ Error: ${result.error}`);
setTimeout(() => setBranchStatus(''), 5000);
setBranchSwitching(false);
}
} catch (error) {
setBranchStatus(`✗ Error: ${error instanceof Error ? error.message : 'Unknown error'}`);
setTimeout(() => setBranchStatus(''), 5000);
setBranchSwitching(false);
}
};
const handleBranchUpdate = async () => {
setBranchSwitching(true);
setBranchStatus('Checking for updates...');
try {
const response = await fetch('/api/branch/update', {
method: 'POST',
headers: { 'Content-Type': 'application/json' }
});
const result = await response.json();
if (result.success) {
setBranchStatus(`${result.message}`);
// Worker will restart, page will refresh
setTimeout(() => {
setBranchStatus('Restarting worker...');
}, 1000);
} else {
setBranchStatus(`✗ Error: ${result.error}`);
setTimeout(() => setBranchStatus(''), 5000);
setBranchSwitching(false);
}
} catch (error) {
setBranchStatus(`✗ Error: ${error instanceof Error ? error.message : 'Unknown error'}`);
setTimeout(() => setBranchStatus(''), 5000);
setBranchSwitching(false);
}
};
return (
<div className={`sidebar ${isOpen ? 'open' : ''}`}>
<div className="sidebar-header">
@@ -200,6 +121,67 @@ export function Sidebar({ isOpen, settings, stats, isSaving, saveStatus, isConne
</button>
</div>
</div>
<a
href="https://discord.gg/J4wttp9vDu"
target="_blank"
rel="noopener noreferrer"
className="sidebar-community-btn"
title="Join our Discord community"
>
<svg width="14" height="14" viewBox="0 0 24 24" fill="currentColor" style={{ marginRight: '6px' }}>
<path d="M20.317 4.37a19.791 19.791 0 0 0-4.885-1.515a.074.074 0 0 0-.079.037c-.21.375-.444.864-.608 1.25a18.27 18.27 0 0 0-5.487 0a12.64 12.64 0 0 0-.617-1.25a.077.077 0 0 0-.079-.037A19.736 19.736 0 0 0 3.677 4.37a.07.07 0 0 0-.032.027C.533 9.046-.32 13.58.099 18.057a.082.082 0 0 0 .031.057a19.9 19.9 0 0 0 5.993 3.03a.078.078 0 0 0 .084-.028a14.09 14.09 0 0 0 1.226-1.994a.076.076 0 0 0-.041-.106a13.107 13.107 0 0 1-1.872-.892a.077.077 0 0 1-.008-.128a10.2 10.2 0 0 0 .372-.292a.074.074 0 0 1 .077-.01c3.928 1.793 8.18 1.793 12.062 0a.074.074 0 0 1 .078.01c.12.098.246.198.373.292a.077.077 0 0 1-.006.127a12.299 12.299 0 0 1-1.873.892a.077.077 0 0 0-.041.107c.36.698.772 1.362 1.225 1.993a.076.076 0 0 0 .084.028a19.839 19.839 0 0 0 6.002-3.03a.077.077 0 0 0 .032-.054c.5-5.177-.838-9.674-3.549-13.66a.061.061 0 0 0-.031-.03zM8.02 15.33c-1.183 0-2.157-1.085-2.157-2.419c0-1.333.956-2.419 2.157-2.419c1.21 0 2.176 1.096 2.157 2.42c0 1.333-.956 2.418-2.157 2.418zm7.975 0c-1.183 0-2.157-1.085-2.157-2.419c0-1.333.955-2.419 2.157-2.419c1.21 0 2.176 1.096 2.157 2.42c0 1.333-.946 2.418-2.157 2.418z"/>
</svg>
<span>Community</span>
</a>
<div className="sidebar-social-links">
<a
href="https://docs.claude-mem.ai"
target="_blank"
rel="noopener noreferrer"
title="Documentation"
className="icon-link"
>
<svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round">
<path d="M4 19.5A2.5 2.5 0 0 1 6.5 17H20"></path>
<path d="M6.5 2H20v20H6.5A2.5 2.5 0 0 1 4 19.5v-15A2.5 2.5 0 0 1 6.5 2z"></path>
</svg>
</a>
<a
href="https://github.com/thedotmack/claude-mem/"
target="_blank"
rel="noopener noreferrer"
title="GitHub"
className="icon-link"
>
<svg width="16" height="16" viewBox="0 0 24 24" fill="currentColor">
<path d="M12 0c-6.626 0-12 5.373-12 12 0 5.302 3.438 9.8 8.207 11.387.599.111.793-.261.793-.577v-2.234c-3.338.726-4.033-1.416-4.033-1.416-.546-1.387-1.333-1.756-1.333-1.756-1.089-.745.083-.729.083-.729 1.205.084 1.839 1.237 1.839 1.237 1.07 1.834 2.807 1.304 3.492.997.107-.775.418-1.305.762-1.604-2.665-.305-5.467-1.334-5.467-5.931 0-1.311.469-2.381 1.236-3.221-.124-.303-.535-1.524.117-3.176 0 0 1.008-.322 3.301 1.23.957-.266 1.983-.399 3.003-.404 1.02.005 2.047.138 3.006.404 2.291-1.552 3.297-1.23 3.297-1.23.653 1.653.242 2.874.118 3.176.77.84 1.235 1.911 1.235 3.221 0 4.609-2.807 5.624-5.479 5.921.43.372.823 1.102.823 2.222v3.293c0 .319.192.694.801.576 4.765-1.589 8.199-6.086 8.199-11.386 0-6.627-5.373-12-12-12z"/>
</svg>
</a>
<a
href="https://x.com/Claude_Memory"
target="_blank"
rel="noopener noreferrer"
title="X (Twitter)"
className="icon-link"
>
<svg width="16" height="16" viewBox="0 0 24 24" fill="currentColor">
<path d="M18.244 2.25h3.308l-7.227 8.26 8.502 11.24H16.17l-5.214-6.817L4.99 21.75H1.68l7.73-8.835L1.254 2.25H8.08l4.713 6.231zm-1.161 17.52h1.833L7.084 4.126H5.117z"/>
</svg>
</a>
</div>
<div className="sidebar-project-filter">
<label htmlFor="sidebar-project-select">Filter by Project</label>
<select
id="sidebar-project-select"
value={currentFilter}
onChange={e => onFilterChange(e.target.value)}
>
<option value="">All Projects</option>
{projects.map(project => (
<option key={project} value={project}>{project}</option>
))}
</select>
</div>
<div className="stats-scroll">
<div className="settings-section">
@@ -275,94 +257,6 @@ export function Sidebar({ isOpen, settings, stats, isSaving, saveStatus, isConne
</div>
</div>
<div className="settings-section">
<h3>Version Channel</h3>
<div className="form-group">
{branchInfo ? (
<>
<div style={{ display: 'flex', alignItems: 'center', gap: '8px', marginBottom: '8px' }}>
<span style={{
padding: '2px 8px',
borderRadius: '4px',
fontSize: '11px',
fontWeight: 600,
background: branchInfo.isBeta ? '#6b4500' : '#1a4d1a',
color: branchInfo.isBeta ? '#ffb84d' : '#4ade80',
border: `1px solid ${branchInfo.isBeta ? '#ffb84d' : '#4ade80'}`
}}>
{branchInfo.isBeta ? 'Beta' : 'Stable'}
</span>
<span style={{ fontSize: '12px', opacity: 0.7 }}>
{branchInfo.branch || 'main'}
</span>
</div>
{branchInfo.isBeta ? (
<>
<div className="setting-description" style={{ marginBottom: '12px' }}>
You're running the beta with Endless Mode. Your memory data is preserved when switching versions.
</div>
<div style={{ display: 'flex', gap: '8px', flexWrap: 'wrap' }}>
<button
onClick={() => handleBranchSwitch('main')}
disabled={branchSwitching}
style={{
background: '#2a2a2a',
border: '1px solid #404040',
padding: '8px 16px',
cursor: branchSwitching ? 'not-allowed' : 'pointer',
opacity: branchSwitching ? 0.5 : 1
}}
>
Switch to Stable
</button>
<button
onClick={handleBranchUpdate}
disabled={branchSwitching}
style={{
background: '#2a2a2a',
border: '1px solid #404040',
padding: '8px 16px',
cursor: branchSwitching ? 'not-allowed' : 'pointer',
opacity: branchSwitching ? 0.5 : 1
}}
>
Check for Updates
</button>
</div>
</>
) : (
<>
<div className="setting-description" style={{ marginBottom: '12px' }}>
Try the beta to access experimental features like Endless Mode. Your memory data is preserved when switching.
</div>
<button
onClick={() => handleBranchSwitch('beta/7.0')}
disabled={branchSwitching}
style={{
background: '#4a3500',
border: '1px solid #ffb84d',
color: '#ffb84d',
padding: '8px 16px',
cursor: branchSwitching ? 'not-allowed' : 'pointer',
opacity: branchSwitching ? 0.5 : 1
}}
>
Try Beta (Endless Mode)
</button>
</>
)}
{branchStatus && (
<div className="save-status" style={{ marginTop: '8px' }}>{branchStatus}</div>
)}
</>
) : (
<div style={{ fontSize: '12px', opacity: 0.5 }}>Loading branch info...</div>
)}
</div>
</div>
<div className="settings-section">
<h3>Worker Stats</h3>
<div className="stats-grid">
+47
View File
@@ -0,0 +1,47 @@
import { test } from 'node:test';
import assert from 'node:assert';
import { existsSync, readFileSync, writeFileSync, unlinkSync } from 'fs';
import { join } from 'path';
const VERSION_MARKER_PATH = join(process.cwd(), '.install-version');
test('version marker - new JSON format', () => {
const marker = {
packageVersion: '6.3.2',
nodeVersion: 'v22.21.1',
installedAt: new Date().toISOString()
};
writeFileSync(VERSION_MARKER_PATH, JSON.stringify(marker, null, 2));
const content = JSON.parse(readFileSync(VERSION_MARKER_PATH, 'utf-8'));
assert.strictEqual(content.packageVersion, '6.3.2');
assert.strictEqual(content.nodeVersion, 'v22.21.1');
assert.ok(content.installedAt);
unlinkSync(VERSION_MARKER_PATH);
});
test('version marker - backward compatibility with old format', () => {
// Old format: plain text version string
writeFileSync(VERSION_MARKER_PATH, '6.3.2');
const content = readFileSync(VERSION_MARKER_PATH, 'utf-8').trim();
// Should be able to parse old format
let marker;
try {
marker = JSON.parse(content);
} catch {
// Old format - create compatible object
marker = {
packageVersion: content,
nodeVersion: null,
installedAt: null
};
}
assert.strictEqual(marker.packageVersion, '6.3.2');
assert.strictEqual(marker.nodeVersion, null);
unlinkSync(VERSION_MARKER_PATH);
});