diff --git a/README.md b/README.md index a52a9bb7..2d45d02e 100644 --- a/README.md +++ b/README.md @@ -184,7 +184,7 @@ See [Architecture Overview](https://docs.claude-mem.ai/architecture/overview) fo ## MCP Search Tools -Claude-Mem provides intelligent memory search through **4 MCP tools** following a token-efficient **3-layer workflow pattern**: +Claude-Mem provides intelligent memory search through **5 MCP tools** following a token-efficient **3-layer workflow pattern**: **The 3-Layer Workflow:** @@ -197,6 +197,7 @@ Claude-Mem provides intelligent memory search through **4 MCP tools** following - Start with `search` to get an index of results - Use `timeline` to see what was happening around specific observations - Use `get_observations` to fetch full details for relevant IDs +- Use `save_memory` to manually store important information - **~10x token savings** by filtering before fetching details **Available MCP Tools:** @@ -204,7 +205,8 @@ Claude-Mem provides intelligent memory search through **4 MCP tools** following 1. **`search`** - Search memory index with full-text queries, filters by type/date/project 2. **`timeline`** - Get chronological context around a specific observation or query 3. **`get_observations`** - Fetch full observation details by IDs (always batch multiple IDs) -4. **`__IMPORTANT`** - Workflow documentation (always visible to Claude) +4. **`save_memory`** - Manually save a memory/observation for semantic search +5. **`__IMPORTANT`** - Workflow documentation (always visible to Claude) **Example Usage:** @@ -216,6 +218,9 @@ search(query="authentication bug", type="bugfix", limit=10) // Step 3: Fetch full details get_observations(ids=[123, 456]) + +// Save important information manually +save_memory(text="API requires auth header X-API-Key", title="API Auth") ``` See [Search Tools Guide](https://docs.claude-mem.ai/usage/search-tools) for detailed examples. diff --git a/src/servers/mcp-server.ts b/src/servers/mcp-server.ts index 30d5010a..bb42fd85 100644 --- a/src/servers/mcp-server.ts +++ b/src/servers/mcp-server.ts @@ -233,6 +233,31 @@ NEVER fetch full details without filtering first. 10x token savings.`, handler: async (args: any) => { return await callWorkerAPIPost('/api/observations/batch', args); } + }, + { + name: 'save_memory', + description: 'Save a manual memory/observation for semantic search. Use this to remember important information.', + inputSchema: { + type: 'object', + properties: { + text: { + type: 'string', + description: 'Content to remember (required)' + }, + title: { + type: 'string', + description: 'Short title (auto-generated from text if omitted)' + }, + project: { + type: 'string', + description: 'Project name (uses "claude-mem" if omitted)' + } + }, + required: ['text'] + }, + handler: async (args: any) => { + return await callWorkerAPIPost('/api/memory/save', args); + } } ]; diff --git a/src/services/sqlite/SessionStore.ts b/src/services/sqlite/SessionStore.ts index 321bb252..9cd41f19 100644 --- a/src/services/sqlite/SessionStore.ts +++ b/src/services/sqlite/SessionStore.ts @@ -2124,6 +2124,34 @@ export class SessionStore { return stmt.get(id) || null; } + /** + * Get or create a manual session for storing user-created observations + * Manual sessions use a predictable ID format: "manual-{project}" + */ + getOrCreateManualSession(project: string): string { + const memorySessionId = `manual-${project}`; + const contentSessionId = `manual-content-${project}`; + + const existing = this.db.prepare( + 'SELECT memory_session_id FROM sdk_sessions WHERE memory_session_id = ?' + ).get(memorySessionId) as { memory_session_id: string } | undefined; + + if (existing) { + return memorySessionId; + } + + // Create new manual session + const now = new Date(); + this.db.prepare(` + INSERT INTO sdk_sessions (memory_session_id, content_session_id, project, started_at, started_at_epoch, status) + VALUES (?, ?, ?, ?, ?, 'active') + `).run(memorySessionId, contentSessionId, project, now.toISOString(), now.getTime()); + + logger.info('SESSION', 'Created manual session', { memorySessionId, project }); + + return memorySessionId; + } + /** * Close the database connection */ diff --git a/src/services/worker-service.ts b/src/services/worker-service.ts index 065b82c1..1e12eb34 100644 --- a/src/services/worker-service.ts +++ b/src/services/worker-service.ts @@ -108,6 +108,7 @@ import { DataRoutes } from './worker/http/routes/DataRoutes.js'; import { SearchRoutes } from './worker/http/routes/SearchRoutes.js'; import { SettingsRoutes } from './worker/http/routes/SettingsRoutes.js'; import { LogsRoutes } from './worker/http/routes/LogsRoutes.js'; +import { MemoryRoutes } from './worker/http/routes/MemoryRoutes.js'; // Process management for zombie cleanup (Issue #737) import { startOrphanReaper, reapOrphanedProcesses } from './worker/ProcessRegistry.js'; @@ -240,6 +241,7 @@ export class WorkerService { this.server.registerRoutes(new DataRoutes(this.paginationHelper, this.dbManager, this.sessionManager, this.sseBroadcaster, this, this.startTime)); this.server.registerRoutes(new SettingsRoutes(this.settingsManager)); this.server.registerRoutes(new LogsRoutes()); + this.server.registerRoutes(new MemoryRoutes(this.dbManager, 'claude-mem')); // Early handler for /api/context/inject — fail open if not yet initialized this.server.app.get('/api/context/inject', async (req, res, next) => { diff --git a/src/services/worker/http/routes/MemoryRoutes.ts b/src/services/worker/http/routes/MemoryRoutes.ts new file mode 100644 index 00000000..65111cfc --- /dev/null +++ b/src/services/worker/http/routes/MemoryRoutes.ts @@ -0,0 +1,93 @@ +/** + * Memory Routes + * + * Handles manual memory/observation saving. + * POST /api/memory/save - Save a manual memory observation + */ + +import express, { Request, Response } from 'express'; +import { BaseRouteHandler } from '../BaseRouteHandler.js'; +import { logger } from '../../../../utils/logger.js'; +import type { DatabaseManager } from '../../DatabaseManager.js'; + +export class MemoryRoutes extends BaseRouteHandler { + constructor( + private dbManager: DatabaseManager, + private defaultProject: string + ) { + super(); + } + + setupRoutes(app: express.Application): void { + app.post('/api/memory/save', this.handleSaveMemory.bind(this)); + } + + /** + * POST /api/memory/save - Save a manual memory/observation + * Body: { text: string, title?: string, project?: string } + */ + private handleSaveMemory = this.wrapHandler(async (req: Request, res: Response): Promise => { + const { text, title, project } = req.body; + const targetProject = project || this.defaultProject; + + if (!text || typeof text !== 'string' || text.trim().length === 0) { + this.badRequest(res, 'text is required and must be non-empty'); + return; + } + + const sessionStore = this.dbManager.getSessionStore(); + const chromaSync = this.dbManager.getChromaSync(); + + // 1. Get or create manual session for project + const memorySessionId = sessionStore.getOrCreateManualSession(targetProject); + + // 2. Build observation + const observation = { + type: 'discovery', // Use existing valid type + title: title || text.substring(0, 60).trim() + (text.length > 60 ? '...' : ''), + subtitle: 'Manual memory', + facts: [] as string[], + narrative: text, + concepts: [] as string[], + files_read: [] as string[], + files_modified: [] as string[] + }; + + // 3. Store to SQLite + const result = sessionStore.storeObservation( + memorySessionId, + targetProject, + observation, + 0, // promptNumber + 0 // discoveryTokens + ); + + logger.info('HTTP', 'Manual observation saved', { + id: result.id, + project: targetProject, + title: observation.title + }); + + // 4. Sync to ChromaDB (async, fire-and-forget) + chromaSync.syncObservation( + result.id, + memorySessionId, + targetProject, + observation, + 0, + result.createdAtEpoch, + 0 + ).catch(err => { + logger.error('CHROMA', 'ChromaDB sync failed', { id: result.id }, err as Error); + }); + + // 5. Return success + res.json({ + success: true, + id: result.id, + title: observation.title, + project: targetProject, + message: `Memory saved as observation #${result.id}` + }); + }); +}