Files
claude-mem/scripts/build-hooks.js
T
Alex Newman 266c746d50 feat: Fix observation timestamps, refactor session management, and enhance worker reliability (#437)
* Refactor worker version checks and increase timeout settings

- Updated the default hook timeout from 5000ms to 120000ms for improved stability.
- Modified the worker version check to log a warning instead of restarting the worker on version mismatch.
- Removed legacy PM2 cleanup and worker start logic, simplifying the ensureWorkerRunning function.
- Enhanced polling mechanism for worker readiness with increased retries and reduced interval.

* feat: implement worker queue polling to ensure processing completion before proceeding

* refactor: change worker command from start to restart in hooks configuration

* refactor: remove session management complexity

- Simplify createSDKSession to pure INSERT OR IGNORE
- Remove auto-create logic from storeObservation/storeSummary
- Delete 11 unused session management methods
- Derive prompt_number from user_prompts count
- Keep sdk_sessions table schema unchanged for compatibility

* refactor: simplify session management by removing unused methods and auto-creation logic

* Refactor session prompt number retrieval in SessionRoutes

- Updated the method of obtaining the prompt number from the session.
- Replaced `store.getPromptCounter(sessionDbId)` with `store.getPromptNumberFromUserPrompts(claudeSessionId)` for better clarity and accuracy.
- Adjusted the logic for incrementing the prompt number to derive it from the user prompts count instead of directly incrementing a counter.

* refactor: replace getPromptCounter with getPromptNumberFromUserPrompts in SessionManager

Phase 7 of session management simplification. Updates SessionManager to derive
prompt numbers from user_prompts table count instead of using the deprecated
prompt_counter column.

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

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

* refactor: simplify SessionCompletionHandler to use direct SQL query

Phase 8: Remove call to findActiveSDKSession() and replace with direct
database query in SessionCompletionHandler.completeByClaudeId().

This removes dependency on the deleted findActiveSDKSession() method
and simplifies the code by using a straightforward SELECT query.

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

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

* refactor: remove markSessionCompleted call from SDKAgent

- Delete call to markSessionCompleted() in SDKAgent.ts
- Session status is no longer tracked or updated
- Part of phase 9: simplifying session management

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

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

* refactor: remove markSessionComplete method (Phase 10)

- Deleted markSessionComplete() method from DatabaseManager
- Removed markSessionComplete call from SessionCompletionHandler
- Session completion status no longer tracked in database
- Part of session management simplification effort

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

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

* refactor: replace deleted updateSDKSessionId calls in import script (Phase 11)

- Replace updateSDKSessionId() calls with direct SQL UPDATE statements
- Method was deleted in Phase 3 as part of session management simplification
- Import script now uses direct database access consistently

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

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

* test: add validation for SQL updates in sdk_sessions table

* refactor: enhance worker-cli to support manual and automated runs

* Remove cleanup hook and associated session completion logic

- Deleted the cleanup-hook implementation from the hooks directory.
- Removed the session completion endpoint that was used by the cleanup hook.
- Updated the SessionCompletionHandler to eliminate the completeByClaudeId method and its dependencies.
- Adjusted the SessionRoutes to reflect the removal of the session completion route.

* fix: update worker-cli command to use bun for consistency

* feat: Implement timestamp fix for observations and enhance processing logic

- Added `earliestPendingTimestamp` to `ActiveSession` to track the original timestamp of the earliest pending message.
- Updated `SDKAgent` to capture and utilize the earliest pending timestamp during response processing.
- Modified `SessionManager` to track the earliest timestamp when yielding messages.
- Created scripts for fixing corrupted timestamps, validating fixes, and investigating timestamp issues.
- Verified that all corrupted observations have been repaired and logic for future processing is sound.
- Ensured orphan processing can be safely re-enabled after validation.

* feat: Enhance SessionStore to support custom database paths and add timestamp fields for observations and summaries

* Refactor pending queue processing and add management endpoints

- Disabled automatic recovery of orphaned queues on startup; users must now use the new /api/pending-queue/process endpoint.
- Updated processOrphanedQueues method to processPendingQueues with improved session handling and return detailed results.
- Added new API endpoints for managing pending queues: GET /api/pending-queue and POST /api/pending-queue/process.
- Introduced a new script (check-pending-queue.ts) for checking and processing pending observation queues interactively or automatically.
- Enhanced logging and error handling for better monitoring of session processing.

* updated agent sdk

* feat: Add manual recovery guide and queue management endpoints to documentation

---------

Co-authored-by: Claude Sonnet 4.5 <noreply@anthropic.com>
2025-12-25 15:36:46 -05:00

289 lines
8.8 KiB
JavaScript

#!/usr/bin/env node
/**
* Build script for claude-mem hooks
* Bundles TypeScript hooks into individual standalone executables using esbuild
*/
import { build } from 'esbuild';
import fs from 'fs';
import path from 'path';
import { fileURLToPath } from 'url';
const __dirname = path.dirname(fileURLToPath(import.meta.url));
const HOOKS = [
{ name: 'context-hook', source: 'src/hooks/context-hook.ts' },
{ name: 'new-hook', source: 'src/hooks/new-hook.ts' },
{ name: 'save-hook', source: 'src/hooks/save-hook.ts' },
{ name: 'summary-hook', source: 'src/hooks/summary-hook.ts' },
{ name: 'user-message-hook', source: 'src/hooks/user-message-hook.ts' }
];
const WORKER_SERVICE = {
name: 'worker-service',
source: 'src/services/worker-service.ts'
};
const WORKER_WRAPPER = {
name: 'worker-wrapper',
source: 'src/services/worker-wrapper.ts'
};
const MCP_SERVER = {
name: 'mcp-server',
source: 'src/servers/mcp-server.ts'
};
const CONTEXT_GENERATOR = {
name: 'context-generator',
source: 'src/services/context-generator.ts'
};
const WORKER_CLI = {
name: 'worker-cli',
source: 'src/cli/worker-cli.ts'
};
async function buildHooks() {
console.log('🔨 Building claude-mem hooks and worker service...\n');
try {
// Read version from package.json
const packageJson = JSON.parse(fs.readFileSync('package.json', 'utf-8'));
const version = packageJson.version;
console.log(`📌 Version: ${version}`);
// Create output directories
console.log('\n📦 Preparing output directories...');
const hooksDir = 'plugin/scripts';
const uiDir = 'plugin/ui';
if (!fs.existsSync(hooksDir)) {
fs.mkdirSync(hooksDir, { recursive: true });
}
if (!fs.existsSync(uiDir)) {
fs.mkdirSync(uiDir, { recursive: true });
}
console.log('✓ Output directories ready');
// Generate plugin/package.json for cache directory dependency installation
// Note: bun:sqlite is a Bun built-in, no external dependencies needed for SQLite
console.log('\n📦 Generating plugin package.json...');
const pluginPackageJson = {
name: 'claude-mem-plugin',
version: version,
private: true,
description: 'Runtime dependencies for claude-mem bundled hooks',
type: 'module',
dependencies: {},
engines: {
node: '>=18.0.0',
bun: '>=1.0.0'
}
};
fs.writeFileSync('plugin/package.json', JSON.stringify(pluginPackageJson, null, 2) + '\n');
console.log('✓ plugin/package.json generated');
// Build React viewer
console.log('\n📋 Building React viewer...');
const { spawn } = await import('child_process');
const viewerBuild = spawn('node', ['scripts/build-viewer.js'], { stdio: 'inherit' });
await new Promise((resolve, reject) => {
viewerBuild.on('exit', (code) => {
if (code === 0) {
resolve();
} else {
reject(new Error(`Viewer build failed with exit code ${code}`));
}
});
});
// Build worker service
console.log(`\n🔧 Building worker service...`);
await build({
entryPoints: [WORKER_SERVICE.source],
bundle: true,
platform: 'node',
target: 'node18',
format: 'cjs',
outfile: `${hooksDir}/${WORKER_SERVICE.name}.cjs`,
minify: true,
logLevel: 'error', // Suppress warnings (import.meta warning is benign)
external: ['bun:sqlite'],
define: {
'__DEFAULT_PACKAGE_VERSION__': `"${version}"`
},
banner: {
js: '#!/usr/bin/env bun'
}
});
// Make worker service executable
fs.chmodSync(`${hooksDir}/${WORKER_SERVICE.name}.cjs`, 0o755);
const workerStats = fs.statSync(`${hooksDir}/${WORKER_SERVICE.name}.cjs`);
console.log(`✓ worker-service built (${(workerStats.size / 1024).toFixed(2)} KB)`);
// Build worker wrapper (Windows zombie port fix)
console.log(`\n🔧 Building worker wrapper...`);
await build({
entryPoints: [WORKER_WRAPPER.source],
bundle: true,
platform: 'node',
target: 'node18',
format: 'cjs',
outfile: `${hooksDir}/${WORKER_WRAPPER.name}.cjs`,
minify: true,
logLevel: 'error',
external: ['bun:sqlite'],
define: {
'__DEFAULT_PACKAGE_VERSION__': `"${version}"`
},
banner: {
js: '#!/usr/bin/env bun'
}
});
// Make worker wrapper executable
fs.chmodSync(`${hooksDir}/${WORKER_WRAPPER.name}.cjs`, 0o755);
const wrapperStats = fs.statSync(`${hooksDir}/${WORKER_WRAPPER.name}.cjs`);
console.log(`✓ worker-wrapper built (${(wrapperStats.size / 1024).toFixed(2)} KB)`);
// Build MCP server
console.log(`\n🔧 Building MCP server...`);
await build({
entryPoints: [MCP_SERVER.source],
bundle: true,
platform: 'node',
target: 'node18',
format: 'cjs',
outfile: `${hooksDir}/${MCP_SERVER.name}.cjs`,
minify: true,
logLevel: 'error',
external: ['bun:sqlite'],
define: {
'__DEFAULT_PACKAGE_VERSION__': `"${version}"`
},
banner: {
js: '#!/usr/bin/env node'
}
});
// Make MCP server executable
fs.chmodSync(`${hooksDir}/${MCP_SERVER.name}.cjs`, 0o755);
const mcpServerStats = fs.statSync(`${hooksDir}/${MCP_SERVER.name}.cjs`);
console.log(`✓ mcp-server built (${(mcpServerStats.size / 1024).toFixed(2)} KB)`);
// Build context generator
console.log(`\n🔧 Building context generator...`);
await build({
entryPoints: [CONTEXT_GENERATOR.source],
bundle: true,
platform: 'node',
target: 'node18',
format: 'cjs',
outfile: `${hooksDir}/${CONTEXT_GENERATOR.name}.cjs`,
minify: true,
logLevel: 'error',
external: ['bun:sqlite'],
define: {
'__DEFAULT_PACKAGE_VERSION__': `"${version}"`
}
});
const contextGenStats = fs.statSync(`${hooksDir}/${CONTEXT_GENERATOR.name}.cjs`);
console.log(`✓ context-generator built (${(contextGenStats.size / 1024).toFixed(2)} KB)`);
// Build worker CLI
console.log(`\n🔧 Building worker CLI...`);
await build({
entryPoints: [WORKER_CLI.source],
bundle: true,
platform: 'node',
target: 'node18',
format: 'esm',
outfile: `${hooksDir}/${WORKER_CLI.name}.js`,
minify: true,
logLevel: 'error',
external: ['bun:sqlite'],
define: {
'__DEFAULT_PACKAGE_VERSION__': `"${version}"`
},
banner: {
js: '#!/usr/bin/env bun'
}
});
// Make worker CLI executable
fs.chmodSync(`${hooksDir}/${WORKER_CLI.name}.js`, 0o755);
const workerCliStats = fs.statSync(`${hooksDir}/${WORKER_CLI.name}.js`);
console.log(`✓ worker-cli built (${(workerCliStats.size / 1024).toFixed(2)} KB)`);
// Build each hook
for (const hook of HOOKS) {
console.log(`\n🔧 Building ${hook.name}...`);
const outfile = `${hooksDir}/${hook.name}.js`;
await build({
entryPoints: [hook.source],
bundle: true,
platform: 'node',
target: 'node18',
format: 'esm',
outfile,
minify: true,
external: ['bun:sqlite'],
define: {
'__DEFAULT_PACKAGE_VERSION__': `"${version}"`
},
banner: {
js: '#!/usr/bin/env bun'
}
});
// Make executable
fs.chmodSync(outfile, 0o755);
// Check file size
const stats = fs.statSync(outfile);
const sizeInKB = (stats.size / 1024).toFixed(2);
console.log(`${hook.name} built (${sizeInKB} KB)`);
}
// Build mem-search skill zip for Claude Desktop
console.log('\n📦 Building mem-search skill zip for Claude Desktop...');
const { execSync } = await import('child_process');
const zipOutput = 'plugin/skills/mem-search.zip';
// Remove old zip if exists
if (fs.existsSync(zipOutput)) {
fs.unlinkSync(zipOutput);
}
// Create zip from mem-search skill directory
execSync(`cd plugin/skills && zip -r mem-search.zip mem-search/`, { stdio: 'pipe' });
const zipStats = fs.statSync(zipOutput);
console.log(`✓ mem-search.zip built (${(zipStats.size / 1024).toFixed(2)} KB)`);
console.log('\n✅ All hooks, worker service, and MCP server built successfully!');
console.log(` Output: ${hooksDir}/`);
console.log(` - Hooks: *-hook.js`);
console.log(` - Worker: worker-service.cjs`);
console.log(` - MCP Server: mcp-server.cjs`);
console.log(` - Skills: plugin/skills/`);
console.log(` - Desktop Skill: plugin/skills/mem-search.zip`);
console.log('\n💡 Note: Dependencies will be auto-installed on first hook execution');
} catch (error) {
console.error('\n❌ Build failed:', error.message);
if (error.errors) {
console.error('\nBuild errors:');
error.errors.forEach(err => console.error(` - ${err.text}`));
}
process.exit(1);
}
}
buildHooks();