Compare commits

...

3 Commits

Author SHA1 Message Date
Alex Newman d3fb58ca75 chore: bump version to 7.1.11 2025-12-13 21:00:04 -05:00
Copilot 6a63a8d69c refactor: simplify hook execution - use Node directly instead of Bun (#290)
Removes bun-wrapper indirection. Hooks are compiled JavaScript that work perfectly with Node. Worker still uses Bun where performance matters. Fixes #264
2025-12-13 20:58:38 -05:00
Alex Newman 1ac0db25e5 docs: regenerate CHANGELOG from GitHub releases 2025-12-13 20:23:01 -05:00
19 changed files with 284 additions and 3955 deletions
+1 -1
View File
@@ -10,7 +10,7 @@
"plugins": [
{
"name": "claude-mem",
"version": "7.1.10",
"version": "7.1.11",
"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/).
## [7.1.10] - 2025-12-14
## Enhancement
This release adds automatic orphan cleanup to complement the process leak fix from v7.1.9.
### Added
- **Auto-Cleanup on Startup**: Worker now automatically detects and kills orphaned chroma-mcp processes before starting
- Scans for existing chroma-mcp processes on worker startup
- Kills all found processes before creating new ones
- Logs cleanup activity (process count and PIDs)
- Non-fatal error handling (continues on cleanup failure)
### Benefits
- Automatically recovers from pre-7.1.9 process leaks without manual intervention
- Ensures clean slate on every worker restart
- Prevents accumulation even if v7.1.9's close() method fails
- No user action required - works transparently
### Example Logs
```
[INFO] [SYSTEM] Cleaning up orphaned chroma-mcp processes {count=2, pids=33753,33750}
[INFO] [SYSTEM] Orphaned processes cleaned up {count=2}
```
### Recommendation
Upgrade from v7.1.9 to get automatic orphan cleanup. Combined with v7.1.9's proper subprocess cleanup, this provides comprehensive protection against process leaks.
---
**Full Changelog**: https://github.com/thedotmack/claude-mem/compare/v7.1.9...v7.1.10
## [7.1.9] - 2025-12-14
## Critical Bugfix
+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**: 7.1.10
**Current Version**: 7.1.11
## Architecture
-3882
View File
File diff suppressed because it is too large Load Diff
+1 -1
View File
@@ -1,6 +1,6 @@
{
"name": "claude-mem",
"version": "7.1.10",
"version": "7.1.11",
"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": "7.1.10",
"version": "7.1.11",
"description": "Persistent memory system for Claude Code - seamlessly preserve context across sessions",
"author": {
"name": "Alex Newman"
+6 -6
View File
@@ -7,12 +7,12 @@
"hooks": [
{
"type": "command",
"command": "node \"${CLAUDE_PLUGIN_ROOT}/scripts/smart-install.js\" && bun \"${CLAUDE_PLUGIN_ROOT}/scripts/context-hook.js\"",
"command": "node \"${CLAUDE_PLUGIN_ROOT}/scripts/smart-install.js\" && node \"${CLAUDE_PLUGIN_ROOT}/scripts/context-hook.js\"",
"timeout": 300
},
{
"type": "command",
"command": "bun \"${CLAUDE_PLUGIN_ROOT}/scripts/user-message-hook.js\"",
"command": "node \"${CLAUDE_PLUGIN_ROOT}/scripts/user-message-hook.js\"",
"timeout": 10
}
]
@@ -23,7 +23,7 @@
"hooks": [
{
"type": "command",
"command": "bun \"${CLAUDE_PLUGIN_ROOT}/scripts/new-hook.js\"",
"command": "node \"${CLAUDE_PLUGIN_ROOT}/scripts/new-hook.js\"",
"timeout": 120
}
]
@@ -35,7 +35,7 @@
"hooks": [
{
"type": "command",
"command": "bun \"${CLAUDE_PLUGIN_ROOT}/scripts/save-hook.js\"",
"command": "node \"${CLAUDE_PLUGIN_ROOT}/scripts/save-hook.js\"",
"timeout": 120
}
]
@@ -46,7 +46,7 @@
"hooks": [
{
"type": "command",
"command": "bun \"${CLAUDE_PLUGIN_ROOT}/scripts/summary-hook.js\"",
"command": "node \"${CLAUDE_PLUGIN_ROOT}/scripts/summary-hook.js\"",
"timeout": 120
}
]
@@ -57,7 +57,7 @@
"hooks": [
{
"type": "command",
"command": "bun \"${CLAUDE_PLUGIN_ROOT}/scripts/cleanup-hook.js\"",
"command": "node \"${CLAUDE_PLUGIN_ROOT}/scripts/cleanup-hook.js\"",
"timeout": 120
}
]
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
+7 -10
View File
@@ -1,9 +1,10 @@
import { existsSync, readFileSync, writeFileSync, unlinkSync, mkdirSync } from 'fs';
import { createWriteStream } from 'fs';
import { join } from 'path';
import { spawn, spawnSync } from 'child_process';
import { spawn } from 'child_process';
import { homedir } from 'os';
import { DATA_DIR } from '../../shared/paths.js';
import { getBunPath, isBunAvailable } from '../../utils/bun-path.js';
const PID_FILE = join(DATA_DIR, 'worker.pid');
const LOG_DIR = join(DATA_DIR, 'logs');
@@ -56,26 +57,22 @@ export class ProcessManager {
}
private static isBunAvailable(): boolean {
try {
const result = spawnSync('bun', ['--version'], { stdio: 'pipe', timeout: 5000 });
return result.status === 0;
} catch {
return false;
}
return isBunAvailable();
}
private static async startWithBun(script: string, logFile: string, port: number): Promise<{ success: boolean; pid?: number; error?: string }> {
if (!this.isBunAvailable()) {
const bunPath = getBunPath();
if (!bunPath) {
return {
success: false,
error: 'Bun is required but not found in PATH. Install from https://bun.sh'
error: 'Bun is required but not found in PATH or common installation paths. Install from https://bun.sh'
};
}
try {
const isWindows = process.platform === 'win32';
const child = spawn('bun', [script], {
const child = spawn(bunPath, [script], {
detached: true,
stdio: ['ignore', 'pipe', 'pipe'],
env: { ...process.env, CLAUDE_MEM_WORKER_PORT: String(port) },
+77
View File
@@ -0,0 +1,77 @@
/**
* Bun Path Utility
*
* Resolves the Bun executable path for environments where Bun is not in PATH
* (e.g., fish shell users where ~/.config/fish/config.fish isn't read by /bin/sh)
*/
import { spawnSync } from 'child_process';
import { existsSync } from 'fs';
import { join } from 'path';
import { homedir } from 'os';
/**
* Get the Bun executable path
* Tries PATH first, then checks common installation locations
* Returns absolute path if found, null otherwise
*/
export function getBunPath(): string | null {
const isWindows = process.platform === 'win32';
// Try PATH first
try {
const result = spawnSync('bun', ['--version'], {
encoding: 'utf-8',
stdio: ['pipe', 'pipe', 'pipe'],
shell: isWindows
});
if (result.status === 0) {
return 'bun'; // Available in PATH
}
} catch {
// Not in PATH, continue to check common locations
}
// Check common installation paths
const bunPaths = isWindows
? [join(homedir(), '.bun', 'bin', 'bun.exe')]
: [
join(homedir(), '.bun', 'bin', 'bun'),
'/usr/local/bin/bun',
'/opt/homebrew/bin/bun', // Apple Silicon Homebrew
'/home/linuxbrew/.linuxbrew/bin/bun' // Linux Homebrew
];
for (const bunPath of bunPaths) {
if (existsSync(bunPath)) {
return bunPath;
}
}
return null;
}
/**
* Get the Bun executable path or throw an error
* Use this when Bun is required for operation
*/
export function getBunPathOrThrow(): string {
const bunPath = getBunPath();
if (!bunPath) {
const isWindows = process.platform === 'win32';
const installCmd = isWindows
? 'powershell -c "irm bun.sh/install.ps1 | iex"'
: 'curl -fsSL https://bun.sh/install | bash';
throw new Error(
`Bun is required but not found. Install it with:\n ${installCmd}\nThen restart your terminal.`
);
}
return bunPath;
}
/**
* Check if Bun is available (in PATH or common locations)
*/
export function isBunAvailable(): boolean {
return getBunPath() !== null;
}
+101
View File
@@ -0,0 +1,101 @@
import { describe, it, expect, vi } from 'vitest';
import { existsSync } from 'fs';
import { spawnSync } from 'child_process';
// Mock the dependencies
vi.mock('fs', () => ({
existsSync: vi.fn()
}));
vi.mock('child_process', () => ({
spawnSync: vi.fn()
}));
// Import after mocking
import { getBunPath, isBunAvailable, getBunPathOrThrow } from '../src/utils/bun-path';
describe('bun-path utility', () => {
it('should return "bun" when available in PATH', () => {
// Mock successful bun --version check
vi.mocked(spawnSync).mockReturnValue({
status: 0,
stdout: Buffer.from('1.0.0'),
stderr: Buffer.from(''),
pid: 1234,
output: [],
signal: null
} as any);
const result = getBunPath();
expect(result).toBe('bun');
expect(spawnSync).toHaveBeenCalledWith('bun', ['--version'], expect.any(Object));
});
it('should check common installation paths when not in PATH', () => {
// Mock failed PATH check
vi.mocked(spawnSync).mockReturnValue({
status: 1,
stdout: Buffer.from(''),
stderr: Buffer.from(''),
pid: 1234,
output: [],
signal: null
} as any);
// Mock existsSync to return true for ~/.bun/bin/bun
vi.mocked(existsSync).mockImplementation((path: any) => {
return path.includes('.bun/bin/bun');
});
const result = getBunPath();
expect(result).toContain('.bun/bin/bun');
});
it('should return null when bun is not found anywhere', () => {
// Mock failed PATH check
vi.mocked(spawnSync).mockReturnValue({
status: 1,
stdout: Buffer.from(''),
stderr: Buffer.from(''),
pid: 1234,
output: [],
signal: null
} as any);
// Mock existsSync to always return false
vi.mocked(existsSync).mockReturnValue(false);
const result = getBunPath();
expect(result).toBeNull();
});
it('should return true for isBunAvailable when bun is found', () => {
// Mock successful bun check
vi.mocked(spawnSync).mockReturnValue({
status: 0,
stdout: Buffer.from('1.0.0'),
stderr: Buffer.from(''),
pid: 1234,
output: [],
signal: null
} as any);
const result = isBunAvailable();
expect(result).toBe(true);
});
it('should throw error in getBunPathOrThrow when bun not found', () => {
// Mock failed bun check
vi.mocked(spawnSync).mockReturnValue({
status: 1,
stdout: Buffer.from(''),
stderr: Buffer.from(''),
pid: 1234,
output: [],
signal: null
} as any);
vi.mocked(existsSync).mockReturnValue(false);
expect(() => getBunPathOrThrow()).toThrow('Bun is required');
});
});