docs/public/development.mdx
# Clone repository
git clone https://github.com/thedotmack/claude-mem.git
cd claude-mem
# Install dependencies
npm install
# Build all components
npm run build
The build process uses esbuild to compile TypeScript:
plugin/scripts/plugin/scripts/mcp-server.cjsplugin/scripts/worker-service.cjsplugin/ui/viewer.htmlBuild Output:
*-hook.js (ESM format)version-check.js (ESM format, sub-100ms)worker-service.cjs (CJS format)mcp-server.cjs (CJS format)viewer.html (self-contained HTML bundle)# Build everything
npm run build
# Build only hooks
npm run build:hooks
# The build script is defined in scripts/build-hooks.js
Edit TypeScript source files in src/:
src/
├── hooks/ # Hook implementations (entry points + logic)
├── services/ # Worker service and database
├── servers/ # MCP search server
├── sdk/ # Claude Agent SDK integration
├── shared/ # Shared utilities
├── ui/
│ └── viewer/ # React web viewer UI components
└── utils/ # General utilities
npm run build
# Run all tests
npm test
# Test specific file
node --test tests/session-lifecycle.test.ts
# Test context injection
npm run test:context
# Verbose context test
npm run test:context:verbose
# Start worker manually
npm run worker:start
# Check worker status
npm run worker:status
# View logs
npm run worker:logs
# Test hooks manually
echo '{"session_id":"test-123","cwd":"'$(pwd)'","source":"startup"}' | node plugin/scripts/context-hook.js
CLAUDE_MEM_WORKER_SCRIPT_PATH pins the worker bundle the daemon spawns/respawns,
so you can test a local build in isolation without overwriting your installed
plugin:
npm run build # bundles src → plugin/scripts/worker-service.cjs
CLAUDE_MEM_WORKER_SCRIPT_PATH="$PWD/plugin/scripts/worker-service.cjs" \
CLAUDE_MEM_DATA_DIR=/tmp/cm-dev \
CLAUDE_MEM_WORKER_PORT=37999 \
bun plugin/scripts/worker-service.cjs start
This supersedes the implicit cwd fallback (which only applies when the worker
happens to be launched from the repo root and the marketplace path is absent).
Unset the variable to return to the installed-plugin resolution.
Repeat steps 1-4 until your changes work as expected.
The web viewer UI is a React application built into a self-contained HTML bundle.
Location: src/ui/viewer/
Structure:
src/ui/viewer/
├── index.tsx # Entry point
├── App.tsx # Main application component
├── components/ # React components
│ ├── Header.tsx # Header with logo and actions
│ ├── Sidebar.tsx # Project filter sidebar
│ ├── Feed.tsx # Main feed with infinite scroll
│ ├── cards/ # Card components
│ │ ├── ObservationCard.tsx
│ │ ├── PromptCard.tsx
│ │ ├── SummaryCard.tsx
│ │ └── SkeletonCard.tsx
├── hooks/ # Custom React hooks
│ ├── useSSE.ts # Server-Sent Events connection
│ ├── usePagination.ts # Infinite scroll pagination
│ ├── useSettings.ts # Settings persistence
│ └── useStats.ts # Database statistics
├── utils/ # Utilities
│ ├── constants.ts # Constants (API URLs, etc.)
│ ├── formatters.ts # Date/time formatting
│ └── merge.ts # Data merging and deduplication
└── assets/ # Static assets (fonts, logos)
# Build everything including viewer
npm run build
# The viewer is built to plugin/ui/viewer.html
# It's a self-contained HTML file with inlined JS and CSS
src/ui/viewer/npm run buildnpm run sync-marketplacenpm run worker:restartHot Reload: Not currently supported. Full rebuild + restart required for changes.
Example: Adding a new card type
src/ui/viewer/components/cards/YourCard.tsx:import React from 'react';
export interface YourCardProps {
// Your data structure
}
export const YourCard: React.FC<YourCardProps> = ({ ... }) => {
return (
<div className="card">
</div>
);
};
Feed.tsx:import { YourCard } from './cards/YourCard';
// In render logic:
{item.type === 'your_type' && <YourCard {...item} />}
Update types if needed in src/ui/viewer/types.ts
Rebuild and test
Data Flow:
Key Patterns:
usePagination hook with Intersection ObserveruseSSE hook with auto-reconnectionmerge.ts utilities prevent duplicate itemsuseSettings hook with localStoragesrc/hooks/your-hook.ts:#!/usr/bin/env node
import { readStdin } from '../shared/stdin';
async function main() {
const input = await readStdin();
// Hook implementation
const result = {
hookSpecificOutput: 'Optional output'
};
console.log(JSON.stringify(result));
}
main().catch(console.error);
Note: As of v4.3.1, hooks are self-contained files. The shebang will be added automatically by esbuild during the build process.
plugin/hooks/hooks.json:{
"YourHook": [{
"hooks": [{
"type": "command",
"command": "node ${CLAUDE_PLUGIN_ROOT}/scripts/your-hook.js",
"timeout": 120
}]
}]
}
npm run build
src/services/sqlite/migrations.ts:export const migration011: Migration = {
version: 11,
up: (db: Database) => {
db.run(`
ALTER TABLE observations ADD COLUMN new_field TEXT;
`);
},
down: (db: Database) => {
// Optional: define rollback
}
};
src/services/sqlite/types.ts:export interface Observation {
// ... existing fields
new_field?: string;
}
src/services/sqlite/SessionStore.ts:createObservation(obs: Observation) {
// Include new_field in INSERT
}
# Backup database first!
cp ~/.claude-mem/claude-mem.db ~/.claude-mem/claude-mem.db.backup
# Run tests
npm test
src/sdk/prompts.ts:export function buildObservationPrompt(observation: Observation): string {
return `
<observation>
</observation>
`;
}
src/sdk/parser.ts:export function parseObservation(xml: string): ParsedObservation {
// Parse new XML fields
}
npm test
src/servers/mcp-server.ts:server.setRequestHandler(CallToolRequestSchema, async (request) => {
if (request.params.name === 'your_new_tool') {
// Implement tool logic
const results = await search.yourNewSearch(params);
return formatResults(results);
}
});
src/services/sqlite/SessionSearch.ts:yourNewSearch(params: YourParams): SearchResult[] {
// Implement FTS5 search
}
npm run build
npm test
Claude-mem relies on real-world usage and manual testing rather than traditional unit tests. The project philosophy prioritizes:
This approach was chosen because:
When developing new features:
Build and sync:
npm run build
npm run sync-marketplace
npm run worker:restart
Test in real session:
Check database state:
sqlite3 ~/.claude-mem/claude-mem.db "SELECT * FROM your_table;"
Monitor worker logs:
npm run worker:logs
Verify queue health (for recovery features):
bun scripts/check-pending-queue.ts
Health Checks:
# Worker status
npm run worker:status
# Queue inspection
curl http://localhost:37777/api/pending-queue
# Database integrity
sqlite3 ~/.claude-mem/claude-mem.db "PRAGMA integrity_check;"
Hook Testing:
# Test context hook manually
echo '{"session_id":"test-123","cwd":"'$(pwd)'","source":"startup"}' | node plugin/scripts/context-hook.js
# Test new hook
echo '{"session_id":"test-123","cwd":"'$(pwd)'","prompt":"test"}' | node plugin/scripts/new-hook.js
Data Verification:
# Check recent observations
sqlite3 ~/.claude-mem/claude-mem.db "
SELECT id, tool_name, created_at
FROM observations
ORDER BY created_at_epoch DESC
LIMIT 10;
"
# Check summaries
sqlite3 ~/.claude-mem/claude-mem.db "
SELECT id, request, completed
FROM session_summaries
ORDER BY created_at_epoch DESC
LIMIT 5;
"
For manual recovery features specifically:
Simulate stuck messages:
# Manually create stuck message (for testing only)
sqlite3 ~/.claude-mem/claude-mem.db "
UPDATE pending_messages
SET status = 'processing',
started_processing_at_epoch = strftime('%s', 'now', '-10 minutes') * 1000
WHERE id = 123;
"
Test recovery:
bun scripts/check-pending-queue.ts
Verify results:
curl http://localhost:37777/api/pending-queue | jq '.queue'
Before releasing:
Test all hook triggers:
Test core features:
Test edge cases:
Cross-platform (if applicable):
/**
* Create a new observation in the database
*/
export async function createObservation(
obs: Observation
): Promise<number> {
try {
const result = await db.insert('observations', {
session_id: obs.session_id,
tool_name: obs.tool_name,
// ...
});
return result.id;
} catch (error) {
logger.error('Failed to create observation', error);
throw error;
}
}
export DEBUG=claude-mem:*
npm run worker:restart
npm run worker:logs
sqlite3 ~/.claude-mem/claude-mem.db
# View schema
.schema observations
# Query data
SELECT * FROM observations LIMIT 10;
Use correlation IDs to trace observations through the pipeline:
sqlite3 ~/.claude-mem/claude-mem.db
SELECT correlation_id, tool_name, created_at
FROM observations
WHERE session_id = 'YOUR_SESSION_ID'
ORDER BY created_at;
Run hooks manually with test input:
# Test context hook
echo '{"session_id":"test-123","cwd":"'$(pwd)'","source":"startup"}' | node plugin/scripts/context-hook.js
# Test new hook
echo '{"session_id":"test-123","cwd":"'$(pwd)'","prompt":"test"}' | node plugin/scripts/new-hook.js
# Update version in package.json
npm version patch # or minor, or major
# Build
npm run build
# Publish to NPM
npm run release
The release script:
package.jsonCHANGELOG.md# Manual version bump:
# 1. Update version in package.json
# 2. Update version in plugin/.claude-plugin/plugin.json
# 3. Update version at top of CLAUDE.md
# 4. Update version badge in README.md
# 5. Run: npm run build && npm run sync-marketplace
# Or use npm version command:
npm version 4.3.2
# Update changelog
# Edit CHANGELOG.md manually
# Commit
git add .
git commit -m "chore: Release v4.3.2"
# Tag
git tag v4.3.2
# Push
git push origin main --tags
# Publish to NPM
npm run release
git checkout -b feature/amazing-feature)git commit -m 'Add amazing feature')git push origin feature/amazing-feature)# Check TypeScript types
npx tsc --noEmit
# Lint code (if configured)
npm run lint
# Format code (if configured)
npm run format
# Clean build artifacts
rm -rf plugin/scripts/*.js plugin/scripts/*.cjs
Clean node_modules:
rm -rf node_modules
npm install
Check Node.js version:
node --version # Should be >= 20.0.0
Check for syntax errors:
npx tsc --noEmit
Check database:
rm ~/.claude-mem/claude-mem.db
npm test
Check worker status:
npm run worker:status
View logs:
npm run worker:logs
Kill existing process:
npm run worker:stop
Check port:
lsof -i :37777
Try custom port:
export CLAUDE_MEM_WORKER_PORT=38000
npm run worker:start