Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 2 additions & 2 deletions package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

2 changes: 1 addition & 1 deletion package.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{
"name": "simple-memory-mcp",
"version": "1.0.10",
"version": "1.0.34",
"description": "Simple memory MCP server for storing and retrieving memories with tags",
"main": "dist/index.js",
"type": "module",
Expand Down
68 changes: 68 additions & 0 deletions src/services/memory-service.ts
Original file line number Diff line number Diff line change
Expand Up @@ -485,6 +485,74 @@ export class MemoryService {
return result.changes;
}

/**
* Update a memory's content and/or tags by hash
*/
update(hash: string, content?: string, tags?: string[]): boolean {
if (!this.db) {
throw new Error('Database not initialized');
}

// Validate that at least one field is being updated
if (content === undefined && tags === undefined) {
throw new Error('Must provide either content or tags to update');
}

// Validate content size if provided
if (content !== undefined && content.length > this.maxContentSize) {
throw new Error(`Content exceeds maximum size of ${this.maxContentSize} characters`);
}

// Check if memory exists
const existing = this.stmts.getMemoryByHash.get(hash) as any;
if (!existing) {
return false;
}

try {
// Prepare statements outside transaction
const updateContentStmt = content !== undefined ?
this.db!.prepare(`UPDATE memories SET content = ? WHERE hash = ?`) : null;

const deleteTagsStmt = tags !== undefined ?
this.db!.prepare(`DELETE FROM tags WHERE memory_id = ?`) : null;

const updateMemory = this.db.transaction(() => {
// Update content if provided
if (content !== undefined && updateContentStmt) {
updateContentStmt.run(content, hash);
}

// Update tags if provided
if (tags !== undefined && deleteTagsStmt) {
// Delete existing tags for this memory
deleteTagsStmt.run(existing.id);

// Insert new tags
for (const tag of tags) {
const normalizedTag = tag.trim().toLowerCase();
if (normalizedTag) {
this.stmts.insertTag.run(existing.id, normalizedTag);
}
}
}

return true;
});

const updated = updateMemory();
debugLogHash('MemoryService: Updated memory with hash:', hash);

// Backup if needed (lazy, throttled)
this.backup?.backupIfNeeded();

return updated;
} catch (error: any) {
debugLog('MemoryService: Error updating memory:', error);
throw error;
}
}

/**
* Bulk link memories in a single transaction for performance
* Returns the number of relationships successfully created
Expand Down
183 changes: 183 additions & 0 deletions src/tests/memory-server-tests.ts
Original file line number Diff line number Diff line change
Expand Up @@ -375,6 +375,185 @@ async function testSearchWithRelationships(): Promise<void> {
}
}

/**
* Test updating memory content
*/
async function testUpdateMemoryContent(): Promise<void> {
// Store a memory first
const storeResult = await executeCommand(['store-memory', '--content', 'Original content', '--tags', 'update-test']);
const storeOutput = parseJsonOutput(storeResult.stdout);

if (!storeOutput?.success || !storeOutput.hash) {
throw new Error('Failed to store memory for update test');
}

const hash = storeOutput.hash;
console.log(` Stored memory with hash: ${formatHash(hash)}`);

// Update the content
const updateResult = await executeCommand(['update-memory', '--hash', hash, '--content', 'Updated content']);

if (updateResult.exitCode !== 0) {
throw new Error(`Update memory failed: ${updateResult.stderr}`);
}

const updateOutput = parseJsonOutput(updateResult.stdout);
if (!updateOutput?.success) {
throw new Error('Update memory did not return expected success response');
}

// Verify the update by searching
const searchResult = await executeCommand(['search-memory', '--query', 'Updated content']);
const searchOutput = parseJsonOutput(searchResult.stdout);

if (!searchOutput?.memories || searchOutput.memories.length === 0) {
throw new Error('Updated memory not found in search');
}

const updatedMemory = searchOutput.memories.find((m: any) => m.hash === hash);
if (!updatedMemory) {
throw new Error('Updated memory not found with correct hash');
}

if (updatedMemory.content !== 'Updated content') {
throw new Error(`Content not updated correctly: ${updatedMemory.content}`);
}

console.log('✓ Memory content updated successfully');
}

/**
* Test updating memory tags
*/
async function testUpdateMemoryTags(): Promise<void> {
// Store a memory first
const storeResult = await executeCommand(['store-memory', '--content', 'Content for tag update', '--tags', 'old,tag']);
const storeOutput = parseJsonOutput(storeResult.stdout);

if (!storeOutput?.success || !storeOutput.hash) {
throw new Error('Failed to store memory for tag update test');
}

const hash = storeOutput.hash;
console.log(` Stored memory with hash: ${formatHash(hash)}`);

// Update the tags
const updateResult = await executeCommand(['update-memory', '--hash', hash, '--tags', 'new,updated,tag']);

if (updateResult.exitCode !== 0) {
throw new Error(`Update memory tags failed: ${updateResult.stderr}`);
}

const updateOutput = parseJsonOutput(updateResult.stdout);
if (!updateOutput?.success) {
throw new Error('Update memory tags did not return expected success response');
}

// Verify the update by searching with new tags
const searchResult = await executeCommand(['search-memory', '--tags', 'new']);
const searchOutput = parseJsonOutput(searchResult.stdout);

if (!searchOutput?.memories || searchOutput.memories.length === 0) {
throw new Error('Updated memory not found in tag search');
}

const updatedMemory = searchOutput.memories.find((m: any) => m.hash === hash);
if (!updatedMemory) {
throw new Error('Updated memory not found with correct hash');
}

if (!updatedMemory.tags.includes('new') || !updatedMemory.tags.includes('updated')) {
throw new Error(`Tags not updated correctly: ${updatedMemory.tags.join(',')}`);
}

// Verify old tag is gone
if (updatedMemory.tags.includes('old')) {
throw new Error('Old tag still present after update');
}

console.log('✓ Memory tags updated successfully');
}

/**
* Test updating both content and tags
*/
async function testUpdateMemoryBoth(): Promise<void> {
// Store a memory first
const storeResult = await executeCommand(['store-memory', '--content', 'Original for both update', '--tags', 'original']);
const storeOutput = parseJsonOutput(storeResult.stdout);

if (!storeOutput?.success || !storeOutput.hash) {
throw new Error('Failed to store memory for combined update test');
}

const hash = storeOutput.hash;
console.log(` Stored memory with hash: ${formatHash(hash)}`);

// Update both content and tags
const updateResult = await executeCommand([
'update-memory',
'--hash', hash,
'--content', 'Updated both content and tags',
'--tags', 'combined,update'
]);

if (updateResult.exitCode !== 0) {
throw new Error(`Update memory (both) failed: ${updateResult.stderr}`);
}

const updateOutput = parseJsonOutput(updateResult.stdout);
if (!updateOutput?.success) {
throw new Error('Update memory (both) did not return expected success response');
}

// Verify the update
const searchResult = await executeCommand(['search-memory', '--tags', 'combined']);
const searchOutput = parseJsonOutput(searchResult.stdout);

if (!searchOutput?.memories || searchOutput.memories.length === 0) {
throw new Error('Updated memory not found in search');
}

const updatedMemory = searchOutput.memories.find((m: any) => m.hash === hash);
if (!updatedMemory) {
throw new Error('Updated memory not found with correct hash');
}

if (updatedMemory.content !== 'Updated both content and tags') {
throw new Error(`Content not updated correctly: ${updatedMemory.content}`);
}

if (!updatedMemory.tags.includes('combined') || !updatedMemory.tags.includes('update')) {
throw new Error(`Tags not updated correctly: ${updatedMemory.tags.join(',')}`);
}

console.log('✓ Memory content and tags updated successfully');
}

/**
* Test updating non-existent memory
*/
async function testUpdateNonExistentMemory(): Promise<void> {
const fakeHash = 'nonexistent123456789abcdef';

const updateResult = await executeCommand(['update-memory', '--hash', fakeHash, '--content', 'Should fail']);

if (updateResult.exitCode !== 0) {
throw new Error(`Update should return gracefully even for non-existent hash`);
}

const updateOutput = parseJsonOutput(updateResult.stdout);
if (updateOutput?.success === true) {
throw new Error('Update should fail for non-existent memory');
}

if (!updateOutput?.message || !updateOutput.message.includes('not found')) {
throw new Error('Update should return "not found" message');
}

console.log('✓ Non-existent memory update handled correctly');
}

/**
* Main test runner
*/
Expand All @@ -393,6 +572,10 @@ async function runAllTests(): Promise<void> {
{ name: 'Memory Statistics', fn: testMemoryStats },
{ name: 'Integrated Relationships', fn: testIntegratedRelationships },
{ name: 'Search with Relationships', fn: testSearchWithRelationships },
{ name: 'Update Memory Content', fn: testUpdateMemoryContent },
{ name: 'Update Memory Tags', fn: testUpdateMemoryTags },
{ name: 'Update Memory Both', fn: testUpdateMemoryBoth },
{ name: 'Update Non-Existent Memory', fn: testUpdateNonExistentMemory },
{ name: 'Delete Memory by Tag', fn: testDeleteMemoryByTag },
{ name: 'Search with Limit', fn: testSearchWithLimit },
{ name: 'Improved Search (OR + BM25)', fn: testImprovedSearch }
Expand Down
2 changes: 2 additions & 0 deletions src/tools/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ import type { Tool, ToolDefinition, ToolContext } from '../types/tools.js';
import { storeMemoryTool } from './store-memory/index.js';
import { searchMemoryTool } from './search-memory/index.js';
import { deleteMemoryTool } from './delete-memory/index.js';
import { updateMemoryTool } from './update-memory/index.js';
import { memoryStatsTool } from './memory-stats/index.js';
import { exportMemoryTool } from './export-memory/index.js';
import { importMemoryTool } from './import-memory/index.js';
Expand All @@ -18,6 +19,7 @@ export class ToolRegistry {
this.registerTool(storeMemoryTool);
this.registerTool(searchMemoryTool);
this.registerTool(deleteMemoryTool);
this.registerTool(updateMemoryTool);
this.registerTool(memoryStatsTool);
this.registerTool(exportMemoryTool);
this.registerTool(importMemoryTool);
Expand Down
32 changes: 32 additions & 0 deletions src/tools/update-memory/cli-parser.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,32 @@
import { parseCommandLineArgs } from '../../utils/cli-parser.js';

export function parseCliArgs(args: string[]) {
// PHASE 1: Convert string array to object (shared utility)
const rawArgs = parseCommandLineArgs(args);

// PHASE 2: Validate and transform (command-specific logic)
const result: any = {};

if (rawArgs.hash) {
result.hash = rawArgs.hash;
}

if (rawArgs.content) {
result.content = rawArgs.content;
}

if (rawArgs.tags) {
result.tags = (rawArgs.tags as string).split(',').map((tag: string) => tag.trim());
}

// Validation
if (!result.hash) {
throw new Error('--hash is required');
}

if (!result.content && !result.tags) {
throw new Error('Must provide either --content or --tags to update');
}

return result;
}
Loading