Refactor search functionality to utilize SearchManager

- Introduced SearchManager to handle search operations directly instead of proxying to MCP server.
- Updated WorkerService to initialize SearchManager after database setup.
- Modified SearchRoutes to call SearchManager methods for search operations.
- Adjusted SearchManager to manage timeline and formatting services.
- Enhanced error handling and logging for search operations.
This commit is contained in:
Alex Newman
2025-12-07 19:31:15 -05:00
parent c415ff5120
commit 7175b527a6
4 changed files with 271 additions and 179 deletions
+24 -3
View File
@@ -21,6 +21,9 @@ import { SSEBroadcaster } from './worker/SSEBroadcaster.js';
import { SDKAgent } from './worker/SDKAgent.js';
import { PaginationHelper } from './worker/PaginationHelper.js';
import { SettingsManager } from './worker/SettingsManager.js';
import { SearchManager } from './worker/SearchManager.js';
import { FormattingService } from './worker/FormattingService.js';
import { TimelineService } from './worker/TimelineService.js';
// Import HTTP layer
import { createMiddleware, summarizeRequestBody as summarizeBody } from './worker/http/middleware.js';
@@ -73,11 +76,12 @@ export class WorkerService {
version: '1.0.0'
}, { capabilities: {} });
// Initialize route handlers
// Initialize route handlers (SearchRoutes will use MCP client initially, then switch to SearchManager after DB init)
this.viewerRoutes = new ViewerRoutes(this.sseBroadcaster, this.dbManager, this.sessionManager);
this.sessionRoutes = new SessionRoutes(this.sessionManager, this.dbManager, this.sdkAgent, this.sseBroadcaster, this);
this.dataRoutes = new DataRoutes(this.paginationHelper, this.dbManager, this.sessionManager, this.sseBroadcaster, this, this.startTime);
this.searchRoutes = new SearchRoutes(this.mcpClient);
// SearchRoutes needs SearchManager which requires initialized DB - will be created in initializeBackground()
this.searchRoutes = null as any; // Temporary - will be set in initializeBackground()
this.settingsRoutes = new SettingsRoutes(this.settingsManager);
this.setupMiddleware();
@@ -104,7 +108,10 @@ export class WorkerService {
this.viewerRoutes.setupRoutes(this.app);
this.sessionRoutes.setupRoutes(this.app);
this.dataRoutes.setupRoutes(this.app);
this.searchRoutes.setupRoutes(this.app);
// searchRoutes is set up after database initialization in initializeBackground()
if (this.searchRoutes) {
this.searchRoutes.setupRoutes(this.app);
}
this.settingsRoutes.setupRoutes(this.app);
}
@@ -169,6 +176,20 @@ export class WorkerService {
// Initialize database (once, stays open)
await this.dbManager.initialize();
// Initialize search services (requires initialized database)
const formattingService = new FormattingService();
const timelineService = new TimelineService();
const searchManager = new SearchManager(
this.dbManager.getSessionSearch(),
this.dbManager.getSessionStore(),
this.dbManager.getChromaSync(),
formattingService,
timelineService
);
this.searchRoutes = new SearchRoutes(searchManager);
this.searchRoutes.setupRoutes(this.app); // Setup search routes now that SearchManager is ready
logger.info('WORKER', 'SearchManager initialized and search routes registered');
// Connect to MCP server
const mcpServerPath = path.join(__dirname, '..', '..', 'plugin', 'scripts', 'mcp-server.cjs');
const transport = new StdioClientTransport({
+61 -60
View File
@@ -6,6 +6,7 @@
* The MCP server now acts as a thin HTTP wrapper that calls these methods via HTTP.
*/
import { basename } from 'path';
import { SessionSearch } from '../sqlite/SessionSearch.js';
import { SessionStore } from '../sqlite/SessionStore.js';
import { ChromaSync } from '../sync/ChromaSync.js';
@@ -22,7 +23,7 @@ export class SearchManager {
private sessionStore: SessionStore,
private chromaSync: ChromaSync,
private formatter: FormattingService,
private timeline: TimelineService
private timelineService: TimelineService
) {}
/**
@@ -302,7 +303,7 @@ export class SearchManager {
let anchorId: string | number;
let anchorEpoch: number;
let timeline: any;
let timelineData: any;
// MODE 1: Query-based timeline
if (query) {
@@ -345,7 +346,7 @@ export class SearchManager {
anchorId = topResult.id;
anchorEpoch = topResult.created_at_epoch;
silentDebug(`[search-server] Query mode: Using observation #${topResult.id} as timeline anchor`);
timeline = this.sessionStore.getTimelineAroundObservation(topResult.id, topResult.created_at_epoch, depth_before, depth_after, project);
timelineData = this.sessionStore.getTimelineAroundObservation(topResult.id, topResult.created_at_epoch, depth_before, depth_after, project);
}
// MODE 2: Anchor-based timeline
else if (typeof anchor === 'number') {
@@ -362,7 +363,7 @@ export class SearchManager {
}
anchorId = anchor;
anchorEpoch = obs.created_at_epoch;
timeline = this.sessionStore.getTimelineAroundObservation(anchor, anchorEpoch, depth_before, depth_after, project);
timelineData = this.sessionStore.getTimelineAroundObservation(anchor, anchorEpoch, depth_before, depth_after, project);
} else if (typeof anchor === 'string') {
// Session ID or ISO timestamp
if (anchor.startsWith('S') || anchor.startsWith('#S')) {
@@ -380,7 +381,7 @@ export class SearchManager {
}
anchorEpoch = sessions[0].created_at_epoch;
anchorId = `S${sessionNum}`;
timeline = this.sessionStore.getTimelineAroundTimestamp(anchorEpoch, depth_before, depth_after, project);
timelineData = this.sessionStore.getTimelineAroundTimestamp(anchorEpoch, depth_before, depth_after, project);
} else {
// ISO timestamp
const date = new Date(anchor);
@@ -395,7 +396,7 @@ export class SearchManager {
}
anchorEpoch = date.getTime();
anchorId = anchor;
timeline = this.sessionStore.getTimelineAroundTimestamp(anchorEpoch, depth_before, depth_after, project);
timelineData = this.sessionStore.getTimelineAroundTimestamp(anchorEpoch, depth_before, depth_after, project);
}
} else {
return {
@@ -409,12 +410,12 @@ export class SearchManager {
// Combine, sort, and filter timeline items
const items: TimelineItem[] = [
...timeline.observations.map((obs: any) => ({ type: 'observation' as const, data: obs, epoch: obs.created_at_epoch })),
...timeline.sessions.map((sess: any) => ({ type: 'session' as const, data: sess, epoch: sess.created_at_epoch })),
...timeline.prompts.map((prompt: any) => ({ type: 'prompt' as const, data: prompt, epoch: prompt.created_at_epoch }))
...timelineData.observations.map((obs: any) => ({ type: 'observation' as const, data: obs, epoch: obs.created_at_epoch })),
...timelineData.sessions.map((sess: any) => ({ type: 'session' as const, data: sess, epoch: sess.created_at_epoch })),
...timelineData.prompts.map((prompt: any) => ({ type: 'prompt' as const, data: prompt, epoch: prompt.created_at_epoch }))
];
items.sort((a, b) => a.epoch - b.epoch);
const filteredItems = this.timeline.filterByDepth(items, anchorId, anchorEpoch, depth_before, depth_after);
const filteredItems = this.timelineService.filterByDepth(items, anchorId, anchorEpoch, depth_before, depth_after);
if (filteredItems.length === 0) {
return {
@@ -428,25 +429,25 @@ export class SearchManager {
}
// Format timeline (helper functions)
function formatDate(epochMs: number): string {
const formatDate = (epochMs: number): string => {
const date = new Date(epochMs);
return date.toLocaleString('en-US', {
month: 'short',
day: 'numeric',
year: 'numeric'
});
}
};
function formatTime(epochMs: number): string {
const formatTime = (epochMs: number): string => {
const date = new Date(epochMs);
return date.toLocaleString('en-US', {
hour: 'numeric',
minute: '2-digit',
hour12: true
});
}
};
function formatDateTime(epochMs: number): string {
const formatDateTime = (epochMs: number): string => {
const date = new Date(epochMs);
return date.toLocaleString('en-US', {
month: 'short',
@@ -455,12 +456,12 @@ export class SearchManager {
minute: '2-digit',
hour12: true
});
}
};
function estimateTokens(text: string | null): number {
const estimateTokens = (text: string | null): number => {
if (!text) return 0;
return Math.ceil(text.length / 4);
}
};
// Format results
const lines: string[] = [];
@@ -468,7 +469,7 @@ export class SearchManager {
// Header
if (query) {
const anchorObs = filteredItems.find(item => item.type === 'observation' && item.data.id === anchorId);
const anchorTitle = anchorObs ? (anchorObs.data.title || 'Untitled') : 'Unknown';
const anchorTitle = anchorObs && anchorObs.type === 'observation' ? ((anchorObs.data as ObservationSearchResult).title || 'Untitled') : 'Unknown';
lines.push(`# Timeline for query: "${query}"`);
lines.push(`**Anchor:** Observation #${anchorId} - ${anchorTitle}`);
} else {
@@ -522,7 +523,7 @@ export class SearchManager {
lastTime = '';
}
const sess = item.data;
const sess = item.data as SessionSummarySearchResult;
const title = sess.request || 'Session summary';
const link = `claude-mem://session-summary/${sess.id}`;
const marker = isAnchor ? ' ← **ANCHOR**' : '';
@@ -537,14 +538,14 @@ export class SearchManager {
lastTime = '';
}
const prompt = item.data;
const truncated = prompt.prompt.length > 100 ? prompt.prompt.substring(0, 100) + '...' : prompt.prompt;
const prompt = item.data as UserPromptSearchResult;
const truncated = prompt.prompt_text.length > 100 ? prompt.prompt_text.substring(0, 100) + '...' : prompt.prompt_text;
lines.push(`**💬 User Prompt #${prompt.prompt_number}** (${formatDateTime(item.epoch)})`);
lines.push(`> ${truncated}`);
lines.push('');
} else if (item.type === 'observation') {
const obs = item.data;
const obs = item.data as ObservationSearchResult;
const file = 'General';
if (file !== currentFile) {
@@ -1543,7 +1544,7 @@ export class SearchManager {
let anchorId: string | number = anchor;
// Resolve anchor and get timeline data
let timeline;
let timelineData;
if (typeof anchor === 'number') {
// Observation ID - use ID-based boundary detection
const obs = this.sessionStore.getObservationById(anchor);
@@ -1557,7 +1558,7 @@ export class SearchManager {
};
}
anchorEpoch = obs.created_at_epoch;
timeline = this.sessionStore.getTimelineAroundObservation(anchor, anchorEpoch, depth_before, depth_after, project);
timelineData = this.sessionStore.getTimelineAroundObservation(anchor, anchorEpoch, depth_before, depth_after, project);
} else if (typeof anchor === 'string') {
// Session ID or ISO timestamp
if (anchor.startsWith('S') || anchor.startsWith('#S')) {
@@ -1575,7 +1576,7 @@ export class SearchManager {
}
anchorEpoch = sessions[0].created_at_epoch;
anchorId = `S${sessionNum}`;
timeline = this.sessionStore.getTimelineAroundTimestamp(anchorEpoch, depth_before, depth_after, project);
timelineData = this.sessionStore.getTimelineAroundTimestamp(anchorEpoch, depth_before, depth_after, project);
} else {
// ISO timestamp
const date = new Date(anchor);
@@ -1589,7 +1590,7 @@ export class SearchManager {
};
}
anchorEpoch = date.getTime(); // Keep as milliseconds
timeline = this.sessionStore.getTimelineAroundTimestamp(anchorEpoch, depth_before, depth_after, project);
timelineData = this.sessionStore.getTimelineAroundTimestamp(anchorEpoch, depth_before, depth_after, project);
}
} else {
return {
@@ -1603,12 +1604,12 @@ export class SearchManager {
// Combine, sort, and filter timeline items
const items: TimelineItem[] = [
...timeline.observations.map(obs => ({ type: 'observation' as const, data: obs, epoch: obs.created_at_epoch })),
...timeline.sessions.map(sess => ({ type: 'session' as const, data: sess, epoch: sess.created_at_epoch })),
...timeline.prompts.map(prompt => ({ type: 'prompt' as const, data: prompt, epoch: prompt.created_at_epoch }))
...timelineData.observations.map(obs => ({ type: 'observation' as const, data: obs, epoch: obs.created_at_epoch })),
...timelineData.sessions.map(sess => ({ type: 'session' as const, data: sess, epoch: sess.created_at_epoch })),
...timelineData.prompts.map(prompt => ({ type: 'prompt' as const, data: prompt, epoch: prompt.created_at_epoch }))
];
items.sort((a, b) => a.epoch - b.epoch);
const filteredItems = this.timeline.filterByDepth(items, anchorId, anchorEpoch, depth_before, depth_after);
const filteredItems = this.timelineService.filterByDepth(items, anchorId, anchorEpoch, depth_before, depth_after);
if (filteredItems.length === 0) {
const anchorDate = new Date(anchorEpoch).toLocaleString();
@@ -1621,25 +1622,25 @@ export class SearchManager {
}
// Helper functions matching context-hook.ts
function formatDate(epochMs: number): string {
const formatDate = (epochMs: number): string => {
const date = new Date(epochMs);
return date.toLocaleString('en-US', {
month: 'short',
day: 'numeric',
year: 'numeric'
});
}
};
function formatTime(epochMs: number): string {
const formatTime = (epochMs: number): string => {
const date = new Date(epochMs);
return date.toLocaleString('en-US', {
hour: 'numeric',
minute: '2-digit',
hour12: true
});
}
};
function formatDateTime(epochMs: number): string {
const formatDateTime = (epochMs: number): string => {
const date = new Date(epochMs);
return date.toLocaleString('en-US', {
month: 'short',
@@ -1648,12 +1649,12 @@ export class SearchManager {
minute: '2-digit',
hour12: true
});
}
};
function estimateTokens(text: string | null): number {
const estimateTokens = (text: string | null): number => {
if (!text) return 0;
return Math.ceil(text.length / 4);
}
};
// Format results matching context-hook.ts exactly
const lines: string[] = [];
@@ -1709,7 +1710,7 @@ export class SearchManager {
}
// Render session
const sess = item.data;
const sess = item.data as SessionSummarySearchResult;
const title = sess.request || 'Session summary';
const link = `claude-mem://session-summary/${sess.id}`;
const marker = isAnchor ? ' ← **ANCHOR**' : '';
@@ -1726,15 +1727,15 @@ export class SearchManager {
}
// Render prompt
const prompt = item.data;
const truncated = prompt.prompt.length > 100 ? prompt.prompt.substring(0, 100) + '...' : prompt.prompt;
const prompt = item.data as UserPromptSearchResult;
const truncated = prompt.prompt_text.length > 100 ? prompt.prompt_text.substring(0, 100) + '...' : prompt.prompt_text;
lines.push(`**💬 User Prompt #${prompt.prompt_number}** (${formatDateTime(item.epoch)})`);
lines.push(`> ${truncated}`);
lines.push('');
} else if (item.type === 'observation') {
// Render observation in table
const obs = item.data;
const obs = item.data as ObservationSearchResult;
const file = 'General'; // Simplified for timeline view
// Check if we need a new file section
@@ -1888,7 +1889,7 @@ export class SearchManager {
silentDebug(`[search-server] Auto mode: Using observation #${topResult.id} as timeline anchor`);
// Get timeline around this observation
const timeline = this.sessionStore.getTimelineAroundObservation(
const timelineData = this.sessionStore.getTimelineAroundObservation(
topResult.id,
topResult.created_at_epoch,
depth_before,
@@ -1898,12 +1899,12 @@ export class SearchManager {
// Combine, sort, and filter timeline items
const items: TimelineItem[] = [
...timeline.observations.map(obs => ({ type: 'observation' as const, data: obs, epoch: obs.created_at_epoch })),
...timeline.sessions.map(sess => ({ type: 'session' as const, data: sess, epoch: sess.created_at_epoch })),
...timeline.prompts.map(prompt => ({ type: 'prompt' as const, data: prompt, epoch: prompt.created_at_epoch }))
...timelineData.observations.map(obs => ({ type: 'observation' as const, data: obs, epoch: obs.created_at_epoch })),
...timelineData.sessions.map(sess => ({ type: 'session' as const, data: sess, epoch: sess.created_at_epoch })),
...timelineData.prompts.map(prompt => ({ type: 'prompt' as const, data: prompt, epoch: prompt.created_at_epoch }))
];
items.sort((a, b) => a.epoch - b.epoch);
const filteredItems = this.timeline.filterByDepth(items, topResult.id, 0, depth_before, depth_after);
const filteredItems = this.timelineService.filterByDepth(items, topResult.id, 0, depth_before, depth_after);
if (filteredItems.length === 0) {
return {
@@ -1915,25 +1916,25 @@ export class SearchManager {
}
// Helper functions (reused from get_context_timeline)
function formatDate(epochMs: number): string {
const formatDate = (epochMs: number): string => {
const date = new Date(epochMs);
return date.toLocaleString('en-US', {
month: 'short',
day: 'numeric',
year: 'numeric'
});
}
};
function formatTime(epochMs: number): string {
const formatTime = (epochMs: number): string => {
const date = new Date(epochMs);
return date.toLocaleString('en-US', {
hour: 'numeric',
minute: '2-digit',
hour12: true
});
}
};
function formatDateTime(epochMs: number): string {
const formatDateTime = (epochMs: number): string => {
const date = new Date(epochMs);
return date.toLocaleString('en-US', {
month: 'short',
@@ -1942,12 +1943,12 @@ export class SearchManager {
minute: '2-digit',
hour12: true
});
}
};
function estimateTokens(text: string | null): number {
const estimateTokens = (text: string | null): number => {
if (!text) return 0;
return Math.ceil(text.length / 4);
}
};
// Format timeline (reused from get_context_timeline)
const lines: string[] = [];
@@ -2001,7 +2002,7 @@ export class SearchManager {
}
// Render session
const sess = item.data;
const sess = item.data as SessionSummarySearchResult;
const title = sess.request || 'Session summary';
const link = `claude-mem://session-summary/${sess.id}`;
@@ -2017,15 +2018,15 @@ export class SearchManager {
}
// Render prompt
const prompt = item.data;
const truncated = prompt.prompt.length > 100 ? prompt.prompt.substring(0, 100) + '...' : prompt.prompt;
const prompt = item.data as UserPromptSearchResult;
const truncated = prompt.prompt_text.length > 100 ? prompt.prompt_text.substring(0, 100) + '...' : prompt.prompt_text;
lines.push(`**💬 User Prompt #${prompt.prompt_number}** (${formatDateTime(item.epoch)})`);
lines.push(`> ${truncated}`);
lines.push('');
} else if (item.type === 'observation') {
// Render observation in table
const obs = item.data;
const obs = item.data as ObservationSearchResult;
const file = 'General'; // Simplified for timeline view
// Check if we need a new file section
+18 -60
View File
@@ -1,17 +1,17 @@
/**
* Search Routes
*
* Handles all search operations by proxying to the MCP search server.
* All endpoints call MCP tools via the client connection.
* Handles all search operations via SearchManager.
* All endpoints call SearchManager methods directly.
*/
import express, { Request, Response } from 'express';
import { Client } from '@modelcontextprotocol/sdk/client/index.js';
import { logger } from '../../../../utils/logger.js';
import { SearchManager } from '../../SearchManager.js';
export class SearchRoutes {
constructor(
private mcpClient: Client
private searchManager: SearchManager
) {}
setupRoutes(app: express.Application): void {
@@ -47,10 +47,7 @@ export class SearchRoutes {
*/
private async handleUnifiedSearch(req: Request, res: Response): Promise<void> {
try {
const result = await this.mcpClient.callTool({
name: 'search',
arguments: req.query
});
const result = await this.searchManager.search(req.query);
res.json(result.content);
} catch (error) {
logger.failure('WORKER', 'Unified search failed', {}, error as Error);
@@ -64,10 +61,7 @@ export class SearchRoutes {
*/
private async handleUnifiedTimeline(req: Request, res: Response): Promise<void> {
try {
const result = await this.mcpClient.callTool({
name: 'timeline',
arguments: req.query
});
const result = await this.searchManager.timeline(req.query);
res.json(result.content);
} catch (error) {
logger.failure('WORKER', 'Unified timeline failed', {}, error as Error);
@@ -81,10 +75,7 @@ export class SearchRoutes {
*/
private async handleDecisions(req: Request, res: Response): Promise<void> {
try {
const result = await this.mcpClient.callTool({
name: 'decisions',
arguments: req.query
});
const result = await this.searchManager.decisions(req.query);
res.json(result.content);
} catch (error) {
logger.failure('WORKER', 'Decisions search failed', {}, error as Error);
@@ -98,10 +89,7 @@ export class SearchRoutes {
*/
private async handleChanges(req: Request, res: Response): Promise<void> {
try {
const result = await this.mcpClient.callTool({
name: 'changes',
arguments: req.query
});
const result = await this.searchManager.changes(req.query);
res.json(result.content);
} catch (error) {
logger.failure('WORKER', 'Changes search failed', {}, error as Error);
@@ -115,10 +103,7 @@ export class SearchRoutes {
*/
private async handleHowItWorks(req: Request, res: Response): Promise<void> {
try {
const result = await this.mcpClient.callTool({
name: 'how_it_works',
arguments: req.query
});
const result = await this.searchManager.howItWorks(req.query);
res.json(result.content);
} catch (error) {
logger.failure('WORKER', 'How it works search failed', {}, error as Error);
@@ -132,10 +117,7 @@ export class SearchRoutes {
*/
private async handleSearchObservations(req: Request, res: Response): Promise<void> {
try {
const result = await this.mcpClient.callTool({
name: 'search_observations',
arguments: req.query
});
const result = await this.searchManager.searchObservations(req.query);
res.json(result.content);
} catch (error) {
logger.failure('WORKER', 'Search failed', {}, error as Error);
@@ -149,10 +131,7 @@ export class SearchRoutes {
*/
private async handleSearchSessions(req: Request, res: Response): Promise<void> {
try {
const result = await this.mcpClient.callTool({
name: 'search_sessions',
arguments: req.query
});
const result = await this.searchManager.searchSessions(req.query);
res.json(result.content);
} catch (error) {
logger.failure('WORKER', 'Search failed', {}, error as Error);
@@ -166,10 +145,7 @@ export class SearchRoutes {
*/
private async handleSearchPrompts(req: Request, res: Response): Promise<void> {
try {
const result = await this.mcpClient.callTool({
name: 'search_user_prompts',
arguments: req.query
});
const result = await this.searchManager.searchUserPrompts(req.query);
res.json(result.content);
} catch (error) {
logger.failure('WORKER', 'Search failed', {}, error as Error);
@@ -183,10 +159,7 @@ export class SearchRoutes {
*/
private async handleSearchByConcept(req: Request, res: Response): Promise<void> {
try {
const result = await this.mcpClient.callTool({
name: 'find_by_concept',
arguments: req.query
});
const result = await this.searchManager.findByConcept(req.query);
res.json(result.content);
} catch (error) {
logger.failure('WORKER', 'Search failed', {}, error as Error);
@@ -200,10 +173,7 @@ export class SearchRoutes {
*/
private async handleSearchByFile(req: Request, res: Response): Promise<void> {
try {
const result = await this.mcpClient.callTool({
name: 'find_by_file',
arguments: req.query
});
const result = await this.searchManager.findByFile(req.query);
res.json(result.content);
} catch (error) {
logger.failure('WORKER', 'Search failed', {}, error as Error);
@@ -217,10 +187,7 @@ export class SearchRoutes {
*/
private async handleSearchByType(req: Request, res: Response): Promise<void> {
try {
const result = await this.mcpClient.callTool({
name: 'find_by_type',
arguments: req.query
});
const result = await this.searchManager.findByType(req.query);
res.json(result.content);
} catch (error) {
logger.failure('WORKER', 'Search failed', {}, error as Error);
@@ -234,10 +201,7 @@ export class SearchRoutes {
*/
private async handleGetRecentContext(req: Request, res: Response): Promise<void> {
try {
const result = await this.mcpClient.callTool({
name: 'get_recent_context',
arguments: req.query
});
const result = await this.searchManager.getRecentContext(req.query);
res.json(result.content);
} catch (error) {
logger.failure('WORKER', 'Search failed', {}, error as Error);
@@ -251,10 +215,7 @@ export class SearchRoutes {
*/
private async handleGetContextTimeline(req: Request, res: Response): Promise<void> {
try {
const result = await this.mcpClient.callTool({
name: 'get_context_timeline',
arguments: req.query
});
const result = await this.searchManager.getContextTimeline(req.query);
res.json(result.content);
} catch (error) {
logger.failure('WORKER', 'Search failed', {}, error as Error);
@@ -352,10 +313,7 @@ export class SearchRoutes {
*/
private async handleGetTimelineByQuery(req: Request, res: Response): Promise<void> {
try {
const result = await this.mcpClient.callTool({
name: 'get_timeline_by_query',
arguments: req.query
});
const result = await this.searchManager.getTimelineByQuery(req.query);
res.json(result.content);
} catch (error) {
logger.failure('WORKER', 'Search failed', {}, error as Error);