Skip to content
Draft
Show file tree
Hide file tree
Changes from 5 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
6 changes: 6 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,12 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
## [Unreleased]

### Added
- **ID-based Search and Export**: Quick batch operations using memory IDs
- `search-memory --ids "1,5,10"` - Search for specific memories by ID
- `export-memory --ids "1,2,3"` - Export specific memories by ID
- Useful for LLM batch operations when query/tag filters don't match all desired memories
- IDs can be obtained from previous search results

- **Automated Version Bumping**: GitHub Actions workflow automatically bumps patch version on every commit/merge to main branch
- Uses existing `npm run build:release` command
- Commits changes back to repository with `[skip-version]` tag
Expand Down
3 changes: 3 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -144,6 +144,9 @@ simple-memory search-memory --query "search term"
# Search by tags
simple-memory search-memory --tags "tag1,tag2"

# Search by specific IDs (useful for batch operations)
simple-memory search-memory --ids "1,5,10"

# Search with relevance filtering (0-1 scale)
simple-memory search-memory --query "architecture" --min-relevance 0.7

Expand Down
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.

32 changes: 29 additions & 3 deletions src/services/memory-service.ts
Original file line number Diff line number Diff line change
Expand Up @@ -347,7 +347,8 @@ export class MemoryService {
*/
search(
query?: string,
tags?: string[],
tags?: string[],
ids?: number[],
limit: number = 10,
daysAgo?: number,
startDate?: string,
Expand Down Expand Up @@ -392,7 +393,31 @@ export class MemoryService {
}
}

if (query) {
if (ids && ids.length > 0) {
// Search by specific IDs
// Query for all specified IDs
if (!this.db) {
throw new Error('Database not initialized');
}

const placeholders = ids.map(() => '?').join(',');
const stmt = this.db.prepare(`
Copy link
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@copilot please use this.stmts pattern add this query there so everything is in the same place

SELECT * FROM memories
WHERE id IN (${placeholders})
ORDER BY created_at DESC
`);

const idResults = stmt.all(...ids);

// Hydrate with tags
results = idResults.map((row: any) => {
const tagRows = this.stmts.getTagsForMemory.all(row.id) as Array<{ tag: string }>;
return {
...row,
tags: tagRows.map(t => t.tag)
};
});
} else if (query) {
// Use FTS for text search
let ftsResults: any[];
// Tokenize query into words and join with OR for flexible matching
Expand Down Expand Up @@ -869,8 +894,9 @@ export class MemoryService {
// Use existing search method to get memories
// Pass undefined for query to use tag search (if tags provided) or recent search (if no filters)
const memories = this.search(
undefined, // query - let search decide based on tags
undefined, // query - let search decide based on tags/ids
filters?.tags,
filters?.ids,
filters?.limit || 1000, // default high limit for export
undefined, // daysAgo
filters?.startDate?.toISOString(),
Expand Down
6 changes: 3 additions & 3 deletions src/tests/export-import-test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -174,7 +174,7 @@ async function runTests() {
assert(importResult.errors.length === 0, 'No import errors');

// Verify imported data
const importedMemories = importService.search(undefined, undefined, 10);
const importedMemories = importService.search(undefined, undefined, undefined, 10);
assert(importedMemories.length === 5, 'All 5 memories are searchable');

// ==========================================
Expand All @@ -197,7 +197,7 @@ async function runTests() {
console.log('\n[TEST 8] Verify relationship preservation');

// Check if relationships were restored
const memoriesWithRelationships = importService.search('TypeScript', undefined, 10);
const memoriesWithRelationships = importService.search('TypeScript', undefined, undefined, 10);
if (memoriesWithRelationships.length > 0) {
const related = importService.getRelated(memoriesWithRelationships[0].hash, 5);
console.log(` Found ${related.length} related memories (relationships preserved)`);
Expand All @@ -224,7 +224,7 @@ async function runTests() {
assert(importFilteredResult.success === true, 'Filtered import succeeded');
assert(importFilteredResult.imported === 3, 'Imported 3 work memories');

const workMemories = importFilteredService.search(undefined, ['work'], 10);
const workMemories = importFilteredService.search(undefined, ['work'], undefined, 10);
assert(workMemories.length === 3, 'All imported memories have work tag');

importFilteredService.close();
Expand Down
105 changes: 104 additions & 1 deletion src/tests/memory-server-tests.ts
Original file line number Diff line number Diff line change
Expand Up @@ -508,6 +508,107 @@ async function testUpdateNonExistentMemory(): Promise<void> {
console.log(`✓ Update correctly handled non-existent memory`);
}

/**
* Test search by IDs
*/
async function testSearchByIds(): Promise<void> {
// First, store some memories and capture their IDs
const memory1Result = await executeCommand(['store-memory', '--content', 'Memory with ID 1', '--tags', 'test-id']);
const memory1Output = parseJsonOutput(memory1Result.stdout);

const memory2Result = await executeCommand(['store-memory', '--content', 'Memory with ID 2', '--tags', 'test-id']);
const memory2Output = parseJsonOutput(memory2Result.stdout);

const memory3Result = await executeCommand(['store-memory', '--content', 'Memory with ID 3', '--tags', 'test-id']);
const memory3Output = parseJsonOutput(memory3Result.stdout);

// Get all memories to find their IDs
const allResult = await executeCommand(['search-memory', '--tags', 'test-id']);
const allOutput = parseJsonOutput(allResult.stdout);

if (!allOutput?.memories || allOutput.memories.length < 3) {
throw new Error('Failed to store test memories');
}

// Get the IDs of the first and third memories
const id1 = allOutput.memories.find((m: any) => m.content === 'Memory with ID 1')?.id;
const id3 = allOutput.memories.find((m: any) => m.content === 'Memory with ID 3')?.id;

if (!id1 || !id3) {
throw new Error('Could not find memory IDs');
}

// Search by specific IDs
const searchResult = await executeCommand(['search-memory', '--ids', `${id1},${id3}`]);
const searchOutput = parseJsonOutput(searchResult.stdout);

if (!searchOutput?.memories || searchOutput.memories.length !== 2) {
throw new Error(`Expected 2 memories, got ${searchOutput?.memories?.length || 0}`);
}

const foundIds = searchOutput.memories.map((m: any) => m.id);
if (!foundIds.includes(id1) || !foundIds.includes(id3)) {
throw new Error('Did not find expected memories by ID');
}

console.log(`✓ Found ${searchOutput.memories.length} memories by IDs`);
console.log(`✓ Verified IDs match requested IDs: ${id1}, ${id3}`);
}

/**
* Test export by IDs
*/
async function testExportByIds(): Promise<void> {
// First, store some memories
await executeCommand(['store-memory', '--content', 'Export test memory 1', '--tags', 'export-test']);
await executeCommand(['store-memory', '--content', 'Export test memory 2', '--tags', 'export-test']);
await executeCommand(['store-memory', '--content', 'Export test memory 3', '--tags', 'export-test']);

// Get all memories to find their IDs
const allResult = await executeCommand(['search-memory', '--tags', 'export-test']);
const allOutput = parseJsonOutput(allResult.stdout);

if (!allOutput?.memories || allOutput.memories.length < 3) {
throw new Error('Failed to store test memories for export');
}

// Get the IDs of first two memories
const id1 = allOutput.memories[0].id;
const id2 = allOutput.memories[1].id;

// Export by specific IDs
const exportPath = './test-export-by-ids.json';
const exportResult = await executeCommand(['export-memory', '--output', exportPath, '--ids', `${id1},${id2}`]);
const exportOutput = parseJsonOutput(exportResult.stdout);

if (!exportOutput?.success) {
throw new Error('Export by IDs failed');
}

if (exportOutput.totalMemories !== 2) {
throw new Error(`Expected 2 exported memories, got ${exportOutput.totalMemories}`);
}

// Verify the exported file contains the correct memories
const { readFileSync, unlinkSync } = await import('fs');
const exportData = JSON.parse(readFileSync(exportPath, 'utf-8'));

if (exportData.memories.length !== 2) {
throw new Error(`Expected 2 memories in export file, got ${exportData.memories.length}`);
}

const exportedIds = exportData.memories.map((m: any) => m.id);
if (!exportedIds.includes(id1) || !exportedIds.includes(id2)) {
throw new Error('Exported memories do not match requested IDs');
}

// Clean up
unlinkSync(exportPath);

console.log(`✓ Exported ${exportOutput.totalMemories} memories by IDs`);
console.log(`✓ Verified exported memories match requested IDs`);
}

/**
* Main test runner
*/
Expand All @@ -531,7 +632,9 @@ async function runAllTests(): Promise<void> {
{ 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 }
{ name: 'Improved Search (OR + BM25)', fn: testImprovedSearch },
{ name: 'Search by IDs', fn: testSearchByIds },
{ name: 'Export by IDs', fn: testExportByIds }
];

const results: TestResult[] = [];
Expand Down
14 changes: 7 additions & 7 deletions src/tests/migration-test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -89,21 +89,21 @@ function testMigration(): void {

// Test 1: Verify all memories are accessible
console.log('📊 Test 1: Verify all memories accessible');
const allMemories = service.search('', [], 10);
const allMemories = service.search('', [], undefined, 10);
assert.strictEqual(allMemories.length, 5, `Expected 5 memories, got ${allMemories.length}`);
console.log(`✅ All 5 memories accessible\n`);

// Test 2: Verify tag normalization
console.log('📊 Test 2: Verify tag normalization and indexing');
const typescriptResults = service.search(undefined, ['typescript'], 10);
const typescriptResults = service.search(undefined, ['typescript'], undefined, 10);
assert.strictEqual(typescriptResults.length, 2, `Expected 2 memories with 'typescript' tag, got ${typescriptResults.length}`);
console.log(`✅ Found ${typescriptResults.length} memories with 'typescript' tag`);

const testingResults = service.search(undefined, ['testing'], 10);
const testingResults = service.search(undefined, ['testing'], undefined, 10);
assert.strictEqual(testingResults.length, 2, `Expected 2 memories with 'testing' tag, got ${testingResults.length}`);
console.log(`✅ Found ${testingResults.length} memories with 'testing' tag`);

const optimizationResults = service.search(undefined, ['optimization'], 10);
const optimizationResults = service.search(undefined, ['optimization'], undefined, 10);
assert.strictEqual(optimizationResults.length, 2, `Expected 2 memories with 'optimization' tag, got ${optimizationResults.length}`);
console.log(`✅ Found ${optimizationResults.length} memories with 'optimization' tag\n`);

Expand Down Expand Up @@ -138,14 +138,14 @@ function testMigration(): void {

// Test 7: Verify FTS search still works
console.log('📊 Test 7: Verify FTS search functionality');
const ftsResults = service.search('TypeScript', undefined, 10);
const ftsResults = service.search('TypeScript', undefined, undefined, 10);
assert(ftsResults.length >= 2, `Expected at least 2 FTS results, got ${ftsResults.length}`);
console.log(`✅ FTS search working: ${ftsResults.length} results\n`);

// Test 8: Verify new memories work with migrated schema
console.log('📊 Test 8: Verify new memory insertion');
const newHash = service.store('New memory after migration', ['migration', 'test']);
const newMemory = service.search(undefined, ['migration'], 10);
const newMemory = service.search(undefined, ['migration'], undefined, 10);
assert(newMemory.length >= 1, 'New memory should be findable by tag');
console.log(`✅ New memory insertion works\n`);

Expand Down Expand Up @@ -240,7 +240,7 @@ function testPartialMigrationRecovery(): void {
assert.strictEqual(migrationsAfter.length, 3, `Expected 3 migrations, found ${migrationsAfter.length}`);

// Verify tags table exists and data migrated
const tagResults = service.search(undefined, ['recovery'], 10);
const tagResults = service.search(undefined, ['recovery'], undefined, 10);
assert.strictEqual(tagResults.length, 1, 'Should find memory by migrated tag');

console.log('✅ Partial migration recovery successful\n');
Expand Down
10 changes: 5 additions & 5 deletions src/tests/performance-benchmark.ts
Original file line number Diff line number Diff line change
Expand Up @@ -97,13 +97,13 @@ async function runBenchmarks() {
}

const tagSearchResult = benchmark('Search by tag (indexed)', 1000, () => {
service.search(undefined, ['search'], 10);
service.search(undefined, ['search'], undefined, 10);
});
results.push(tagSearchResult);
console.log(` Indexed tag query: ${tagSearchResult.avgMs.toFixed(2)}ms avg (${tagSearchResult.opsPerSecond} ops/sec)`);

const multiTagResult = benchmark('Search by multiple tags', 1000, () => {
service.search(undefined, ['test'], 10);
service.search(undefined, ['test'], undefined, 10);
});
results.push(multiTagResult);
console.log(` Multiple results: ${multiTagResult.avgMs.toFixed(2)}ms avg (${multiTagResult.opsPerSecond} ops/sec)`);
Expand All @@ -128,13 +128,13 @@ async function runBenchmarks() {

const ftsResults = [
benchmark('FTS: simple query', 1000, () => {
service.search('quick brown', undefined, 10);
service.search('quick brown', undefined, undefined, 10);
}),
benchmark('FTS: complex query', 1000, () => {
service.search('machine learning intelligence', undefined, 10);
service.search('machine learning intelligence', undefined, undefined, 10);
}),
benchmark('FTS: common word', 1000, () => {
service.search('performance', undefined, 10);
service.search('performance', undefined, undefined, 10);
})
];

Expand Down
15 changes: 8 additions & 7 deletions src/tests/time-range-test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -63,7 +63,7 @@ async function runTests() {

// Test 1: Search memories from TODAY ONLY (daysAgo=0) - Critical UTC edge case
console.log('Running: Search today only (daysAgo=0)...');
const todayOnly = service.search(undefined, undefined, 10, 0);
const todayOnly = service.search(undefined, undefined, undefined, 10, 0);
if (todayOnly.length === 1 && todayOnly[0].content === 'Memory from today') {
console.log(`✓ Found ${todayOnly.length} memory from today (UTC boundary test passed)`);
console.log(` - ${todayOnly[0].content}`);
Expand All @@ -78,7 +78,7 @@ async function runTests() {

// Test 2: Search memories from last 2 days
console.log('Running: Search last 2 days...');
const last2Days = service.search(undefined, undefined, 10, 2);
const last2Days = service.search(undefined, undefined, undefined, 10, 2);
if (last2Days.length === 2) {
console.log(`✓ Found ${last2Days.length} memories from last 2 days`);
console.log(` - ${last2Days.map(m => m.content).join('\n - ')}`);
Expand All @@ -91,7 +91,7 @@ async function runTests() {

// Test 3: Search memories from last 10 days
console.log('Running: Search last 10 days...');
const last10Days = service.search(undefined, undefined, 10, 10);
const last10Days = service.search(undefined, undefined, undefined, 10, 10);
if (last10Days.length === 3) {
console.log(`✓ Found ${last10Days.length} memories from last 10 days`);
console.log(` - ${last10Days.map(m => m.content).join('\n - ')}`);
Expand All @@ -104,7 +104,7 @@ async function runTests() {

// Test 4: Search memories from last 40 days
console.log('Running: Search last 40 days...');
const last40Days = service.search(undefined, undefined, 10, 40);
const last40Days = service.search(undefined, undefined, undefined, 10, 40);
if (last40Days.length === 4) {
console.log(`✓ Found ${last40Days.length} memories from last 40 days`);
passed++;
Expand All @@ -122,7 +122,8 @@ async function runTests() {
endDate.setDate(endDate.getDate() - 25);
const dateRange = service.search(
undefined,
undefined,
undefined,
undefined,
10,
undefined,
startDate.toISOString().split('T')[0], // Use YYYY-MM-DD format
Expand All @@ -140,7 +141,7 @@ async function runTests() {

// Test 6: Search with content query + time range
console.log('Running: Search with query and time range...');
const queryAndTime = service.search('Memory', undefined, 10, 10);
const queryAndTime = service.search('Memory', undefined, undefined, 10, 10);
if (queryAndTime.length === 3) {
console.log(`✓ Found ${queryAndTime.length} memories matching query within time range`);
passed++;
Expand All @@ -152,7 +153,7 @@ async function runTests() {

// Test 7: Search with tags + time range
console.log('Running: Search with tags and time range...');
const tagsAndTime = service.search(undefined, ['old'], 10, 40);
const tagsAndTime = service.search(undefined, ['old'], undefined, 10, 40);
if (tagsAndTime.length === 2) {
console.log(`✓ Found ${tagsAndTime.length} memories with tag within time range`);
passed++;
Expand Down
Loading