feat: Refactor Settings and Viewer routes to extend BaseRouteHandler for improved error handling
- Introduced BaseRouteHandler class to centralize error handling and response management. - Updated SettingsRoutes to use wrapHandler for automatic error logging and response. - Refactored ViewerRoutes to extend BaseRouteHandler and utilize wrapHandler for health check and UI serving. - Enhanced error handling in SettingsRoutes and ViewerRoutes for better maintainability and readability.
This commit is contained in:
File diff suppressed because one or more lines are too long
@@ -0,0 +1,82 @@
|
|||||||
|
/**
|
||||||
|
* BaseRouteHandler
|
||||||
|
*
|
||||||
|
* Base class for all route handlers providing:
|
||||||
|
* - Automatic try-catch wrapping with error logging
|
||||||
|
* - Integer parameter validation
|
||||||
|
* - Required body parameter validation
|
||||||
|
* - Standard HTTP response helpers
|
||||||
|
* - Centralized error handling
|
||||||
|
*/
|
||||||
|
|
||||||
|
import { Request, Response } from 'express';
|
||||||
|
import { logger } from '../../../utils/logger.js';
|
||||||
|
|
||||||
|
export abstract class BaseRouteHandler {
|
||||||
|
/**
|
||||||
|
* Wrap handler with automatic try-catch and error logging
|
||||||
|
*/
|
||||||
|
protected wrapHandler(
|
||||||
|
handler: (req: Request, res: Response) => void | Promise<void>
|
||||||
|
): (req: Request, res: Response) => void {
|
||||||
|
return (req: Request, res: Response): void => {
|
||||||
|
try {
|
||||||
|
const result = handler(req, res);
|
||||||
|
if (result instanceof Promise) {
|
||||||
|
result.catch(error => this.handleError(res, error as Error));
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
this.handleError(res, error as Error);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Parse and validate integer parameter
|
||||||
|
* Returns the integer value or sends 400 error response
|
||||||
|
*/
|
||||||
|
protected parseIntParam(req: Request, res: Response, paramName: string): number | null {
|
||||||
|
const value = parseInt(req.params[paramName], 10);
|
||||||
|
if (isNaN(value)) {
|
||||||
|
this.badRequest(res, `Invalid ${paramName}`);
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
return value;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Validate required body parameters
|
||||||
|
* Returns true if all required params present, sends 400 error otherwise
|
||||||
|
*/
|
||||||
|
protected validateRequired(req: Request, res: Response, params: string[]): boolean {
|
||||||
|
for (const param of params) {
|
||||||
|
if (req.body[param] === undefined || req.body[param] === null) {
|
||||||
|
this.badRequest(res, `Missing ${param}`);
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Send 400 Bad Request response
|
||||||
|
*/
|
||||||
|
protected badRequest(res: Response, message: string): void {
|
||||||
|
res.status(400).json({ error: message });
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Send 404 Not Found response
|
||||||
|
*/
|
||||||
|
protected notFound(res: Response, message: string): void {
|
||||||
|
res.status(404).json({ error: message });
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Centralized error logging and response
|
||||||
|
*/
|
||||||
|
protected handleError(res: Response, error: Error, context?: string): void {
|
||||||
|
logger.failure('WORKER', context || 'Request failed', {}, error);
|
||||||
|
res.status(500).json({ error: error.message });
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -11,14 +11,14 @@ import { readFileSync, statSync, existsSync } from 'fs';
|
|||||||
import { homedir } from 'os';
|
import { homedir } from 'os';
|
||||||
import { getPackageRoot } from '../../../../shared/paths.js';
|
import { getPackageRoot } from '../../../../shared/paths.js';
|
||||||
import { getWorkerPort } from '../../../../shared/worker-utils.js';
|
import { getWorkerPort } from '../../../../shared/worker-utils.js';
|
||||||
import { logger } from '../../../../utils/logger.js';
|
|
||||||
import { PaginationHelper } from '../../PaginationHelper.js';
|
import { PaginationHelper } from '../../PaginationHelper.js';
|
||||||
import { DatabaseManager } from '../../DatabaseManager.js';
|
import { DatabaseManager } from '../../DatabaseManager.js';
|
||||||
import { SessionManager } from '../../SessionManager.js';
|
import { SessionManager } from '../../SessionManager.js';
|
||||||
import { SSEBroadcaster } from '../../SSEBroadcaster.js';
|
import { SSEBroadcaster } from '../../SSEBroadcaster.js';
|
||||||
import type { WorkerService } from '../../../worker-service.js';
|
import type { WorkerService } from '../../../worker-service.js';
|
||||||
|
import { BaseRouteHandler } from '../BaseRouteHandler.js';
|
||||||
|
|
||||||
export class DataRoutes {
|
export class DataRoutes extends BaseRouteHandler {
|
||||||
constructor(
|
constructor(
|
||||||
private paginationHelper: PaginationHelper,
|
private paginationHelper: PaginationHelper,
|
||||||
private dbManager: DatabaseManager,
|
private dbManager: DatabaseManager,
|
||||||
@@ -26,7 +26,9 @@ export class DataRoutes {
|
|||||||
private sseBroadcaster: SSEBroadcaster,
|
private sseBroadcaster: SSEBroadcaster,
|
||||||
private workerService: WorkerService,
|
private workerService: WorkerService,
|
||||||
private startTime: number
|
private startTime: number
|
||||||
) {}
|
) {
|
||||||
|
super();
|
||||||
|
}
|
||||||
|
|
||||||
setupRoutes(app: express.Application): void {
|
setupRoutes(app: express.Application): void {
|
||||||
// Pagination endpoints
|
// Pagination endpoints
|
||||||
@@ -51,131 +53,91 @@ export class DataRoutes {
|
|||||||
/**
|
/**
|
||||||
* Get paginated observations
|
* Get paginated observations
|
||||||
*/
|
*/
|
||||||
private handleGetObservations(req: Request, res: Response): void {
|
private handleGetObservations = this.wrapHandler((req: Request, res: Response): void => {
|
||||||
try {
|
|
||||||
const { offset, limit, project } = this.parsePaginationParams(req);
|
const { offset, limit, project } = this.parsePaginationParams(req);
|
||||||
const result = this.paginationHelper.getObservations(offset, limit, project);
|
const result = this.paginationHelper.getObservations(offset, limit, project);
|
||||||
res.json(result);
|
res.json(result);
|
||||||
} catch (error) {
|
});
|
||||||
logger.failure('WORKER', 'Get observations failed', {}, error as Error);
|
|
||||||
res.status(500).json({ error: (error as Error).message });
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Get paginated summaries
|
* Get paginated summaries
|
||||||
*/
|
*/
|
||||||
private handleGetSummaries(req: Request, res: Response): void {
|
private handleGetSummaries = this.wrapHandler((req: Request, res: Response): void => {
|
||||||
try {
|
|
||||||
const { offset, limit, project } = this.parsePaginationParams(req);
|
const { offset, limit, project } = this.parsePaginationParams(req);
|
||||||
const result = this.paginationHelper.getSummaries(offset, limit, project);
|
const result = this.paginationHelper.getSummaries(offset, limit, project);
|
||||||
res.json(result);
|
res.json(result);
|
||||||
} catch (error) {
|
});
|
||||||
logger.failure('WORKER', 'Get summaries failed', {}, error as Error);
|
|
||||||
res.status(500).json({ error: (error as Error).message });
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Get paginated user prompts
|
* Get paginated user prompts
|
||||||
*/
|
*/
|
||||||
private handleGetPrompts(req: Request, res: Response): void {
|
private handleGetPrompts = this.wrapHandler((req: Request, res: Response): void => {
|
||||||
try {
|
|
||||||
const { offset, limit, project } = this.parsePaginationParams(req);
|
const { offset, limit, project } = this.parsePaginationParams(req);
|
||||||
const result = this.paginationHelper.getPrompts(offset, limit, project);
|
const result = this.paginationHelper.getPrompts(offset, limit, project);
|
||||||
res.json(result);
|
res.json(result);
|
||||||
} catch (error) {
|
});
|
||||||
logger.failure('WORKER', 'Get prompts failed', {}, error as Error);
|
|
||||||
res.status(500).json({ error: (error as Error).message });
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Get observation by ID
|
* Get observation by ID
|
||||||
* GET /api/observation/:id
|
* GET /api/observation/:id
|
||||||
*/
|
*/
|
||||||
private handleGetObservationById(req: Request, res: Response): void {
|
private handleGetObservationById = this.wrapHandler((req: Request, res: Response): void => {
|
||||||
try {
|
const id = this.parseIntParam(req, res, 'id');
|
||||||
const id = parseInt(req.params.id, 10);
|
if (id === null) return;
|
||||||
if (isNaN(id)) {
|
|
||||||
res.status(400).json({ error: 'Invalid observation ID' });
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
const store = this.dbManager.getSessionStore();
|
const store = this.dbManager.getSessionStore();
|
||||||
const observation = store.getObservationById(id);
|
const observation = store.getObservationById(id);
|
||||||
|
|
||||||
if (!observation) {
|
if (!observation) {
|
||||||
res.status(404).json({ error: `Observation #${id} not found` });
|
this.notFound(res, `Observation #${id} not found`);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
res.json(observation);
|
res.json(observation);
|
||||||
} catch (error) {
|
});
|
||||||
logger.failure('WORKER', 'Get observation by ID failed', {}, error as Error);
|
|
||||||
res.status(500).json({ error: (error as Error).message });
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Get session by ID
|
* Get session by ID
|
||||||
* GET /api/session/:id
|
* GET /api/session/:id
|
||||||
*/
|
*/
|
||||||
private handleGetSessionById(req: Request, res: Response): void {
|
private handleGetSessionById = this.wrapHandler((req: Request, res: Response): void => {
|
||||||
try {
|
const id = this.parseIntParam(req, res, 'id');
|
||||||
const id = parseInt(req.params.id, 10);
|
if (id === null) return;
|
||||||
if (isNaN(id)) {
|
|
||||||
res.status(400).json({ error: 'Invalid session ID' });
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
const store = this.dbManager.getSessionStore();
|
const store = this.dbManager.getSessionStore();
|
||||||
const sessions = store.getSessionSummariesByIds([id]);
|
const sessions = store.getSessionSummariesByIds([id]);
|
||||||
|
|
||||||
if (sessions.length === 0) {
|
if (sessions.length === 0) {
|
||||||
res.status(404).json({ error: `Session #${id} not found` });
|
this.notFound(res, `Session #${id} not found`);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
res.json(sessions[0]);
|
res.json(sessions[0]);
|
||||||
} catch (error) {
|
});
|
||||||
logger.failure('WORKER', 'Get session by ID failed', {}, error as Error);
|
|
||||||
res.status(500).json({ error: (error as Error).message });
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Get user prompt by ID
|
* Get user prompt by ID
|
||||||
* GET /api/prompt/:id
|
* GET /api/prompt/:id
|
||||||
*/
|
*/
|
||||||
private handleGetPromptById(req: Request, res: Response): void {
|
private handleGetPromptById = this.wrapHandler((req: Request, res: Response): void => {
|
||||||
try {
|
const id = this.parseIntParam(req, res, 'id');
|
||||||
const id = parseInt(req.params.id, 10);
|
if (id === null) return;
|
||||||
if (isNaN(id)) {
|
|
||||||
res.status(400).json({ error: 'Invalid prompt ID' });
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
const store = this.dbManager.getSessionStore();
|
const store = this.dbManager.getSessionStore();
|
||||||
const prompts = store.getUserPromptsByIds([id]);
|
const prompts = store.getUserPromptsByIds([id]);
|
||||||
|
|
||||||
if (prompts.length === 0) {
|
if (prompts.length === 0) {
|
||||||
res.status(404).json({ error: `Prompt #${id} not found` });
|
this.notFound(res, `Prompt #${id} not found`);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
res.json(prompts[0]);
|
res.json(prompts[0]);
|
||||||
} catch (error) {
|
});
|
||||||
logger.failure('WORKER', 'Get prompt by ID failed', {}, error as Error);
|
|
||||||
res.status(500).json({ error: (error as Error).message });
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Get database statistics (with worker metadata)
|
* Get database statistics (with worker metadata)
|
||||||
*/
|
*/
|
||||||
private handleGetStats(req: Request, res: Response): void {
|
private handleGetStats = this.wrapHandler((req: Request, res: Response): void => {
|
||||||
try {
|
|
||||||
const db = this.dbManager.getSessionStore().db;
|
const db = this.dbManager.getSessionStore().db;
|
||||||
|
|
||||||
// Read version from package.json
|
// Read version from package.json
|
||||||
@@ -217,18 +179,13 @@ export class DataRoutes {
|
|||||||
summaries: totalSummaries.count
|
summaries: totalSummaries.count
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
} catch (error) {
|
});
|
||||||
logger.failure('WORKER', 'Get stats failed', {}, error as Error);
|
|
||||||
res.status(500).json({ error: (error as Error).message });
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Get list of distinct projects from observations
|
* Get list of distinct projects from observations
|
||||||
* GET /api/projects
|
* GET /api/projects
|
||||||
*/
|
*/
|
||||||
private handleGetProjects(req: Request, res: Response): void {
|
private handleGetProjects = this.wrapHandler((req: Request, res: Response): void => {
|
||||||
try {
|
|
||||||
const db = this.dbManager.getSessionStore().db;
|
const db = this.dbManager.getSessionStore().db;
|
||||||
|
|
||||||
const rows = db.prepare(`
|
const rows = db.prepare(`
|
||||||
@@ -242,42 +199,32 @@ export class DataRoutes {
|
|||||||
const projects = rows.map(row => row.project);
|
const projects = rows.map(row => row.project);
|
||||||
|
|
||||||
res.json({ projects });
|
res.json({ projects });
|
||||||
} catch (error) {
|
});
|
||||||
logger.failure('WORKER', 'Get projects failed', {}, error as Error);
|
|
||||||
res.status(500).json({ error: (error as Error).message });
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Get current processing status
|
* Get current processing status
|
||||||
* GET /api/processing-status
|
* GET /api/processing-status
|
||||||
*/
|
*/
|
||||||
private handleGetProcessingStatus(req: Request, res: Response): void {
|
private handleGetProcessingStatus = this.wrapHandler((req: Request, res: Response): void => {
|
||||||
const isProcessing = this.sessionManager.isAnySessionProcessing();
|
const isProcessing = this.sessionManager.isAnySessionProcessing();
|
||||||
const queueDepth = this.sessionManager.getTotalActiveWork(); // Includes queued + actively processing
|
const queueDepth = this.sessionManager.getTotalActiveWork(); // Includes queued + actively processing
|
||||||
res.json({ isProcessing, queueDepth });
|
res.json({ isProcessing, queueDepth });
|
||||||
}
|
});
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Set processing status (called by hooks)
|
* Set processing status (called by hooks)
|
||||||
* NOTE: This now broadcasts computed status based on active processing (ignores input)
|
* NOTE: This now broadcasts computed status based on active processing (ignores input)
|
||||||
*/
|
*/
|
||||||
private handleSetProcessing(req: Request, res: Response): void {
|
private handleSetProcessing = this.wrapHandler((req: Request, res: Response): void => {
|
||||||
try {
|
|
||||||
// Broadcast current computed status (ignores manual input)
|
// Broadcast current computed status (ignores manual input)
|
||||||
this.workerService.broadcastProcessingStatus();
|
this.workerService.broadcastProcessingStatus();
|
||||||
|
|
||||||
const isProcessing = this.sessionManager.isAnySessionProcessing();
|
const isProcessing = this.sessionManager.isAnySessionProcessing();
|
||||||
const queueDepth = this.sessionManager.getTotalQueueDepth();
|
const queueDepth = this.sessionManager.getTotalQueueDepth();
|
||||||
const activeSessions = this.sessionManager.getActiveSessionCount();
|
const activeSessions = this.sessionManager.getActiveSessionCount();
|
||||||
logger.debug('WORKER', 'Processing status broadcast', { isProcessing, queueDepth, activeSessions });
|
|
||||||
|
|
||||||
res.json({ status: 'ok', isProcessing });
|
res.json({ status: 'ok', isProcessing });
|
||||||
} catch (error) {
|
});
|
||||||
logger.failure('WORKER', 'Failed to broadcast processing status', {}, error as Error);
|
|
||||||
res.status(500).json({ error: (error as Error).message });
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Parse pagination parameters from request query
|
* Parse pagination parameters from request query
|
||||||
|
|||||||
@@ -6,13 +6,15 @@
|
|||||||
*/
|
*/
|
||||||
|
|
||||||
import express, { Request, Response } from 'express';
|
import express, { Request, Response } from 'express';
|
||||||
import { logger } from '../../../../utils/logger.js';
|
|
||||||
import { SearchManager } from '../../SearchManager.js';
|
import { SearchManager } from '../../SearchManager.js';
|
||||||
|
import { BaseRouteHandler } from '../BaseRouteHandler.js';
|
||||||
|
|
||||||
export class SearchRoutes {
|
export class SearchRoutes extends BaseRouteHandler {
|
||||||
constructor(
|
constructor(
|
||||||
private searchManager: SearchManager
|
private searchManager: SearchManager
|
||||||
) {}
|
) {
|
||||||
|
super();
|
||||||
|
}
|
||||||
|
|
||||||
setupRoutes(app: express.Application): void {
|
setupRoutes(app: express.Application): void {
|
||||||
// Unified endpoints (new consolidated API)
|
// Unified endpoints (new consolidated API)
|
||||||
@@ -45,194 +47,128 @@ export class SearchRoutes {
|
|||||||
* Unified search (observations + sessions + prompts)
|
* Unified search (observations + sessions + prompts)
|
||||||
* GET /api/search?query=...&type=observations&format=index&limit=20
|
* GET /api/search?query=...&type=observations&format=index&limit=20
|
||||||
*/
|
*/
|
||||||
private async handleUnifiedSearch(req: Request, res: Response): Promise<void> {
|
private handleUnifiedSearch = this.wrapHandler(async (req: Request, res: Response): Promise<void> => {
|
||||||
try {
|
|
||||||
const result = await this.searchManager.search(req.query);
|
const result = await this.searchManager.search(req.query);
|
||||||
res.json(result.content);
|
res.json(result.content);
|
||||||
} catch (error) {
|
});
|
||||||
logger.failure('WORKER', 'Unified search failed', {}, error as Error);
|
|
||||||
res.status(500).json({ error: (error as Error).message });
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Unified timeline (anchor or query-based)
|
* Unified timeline (anchor or query-based)
|
||||||
* GET /api/timeline?anchor=123 OR GET /api/timeline?query=...
|
* GET /api/timeline?anchor=123 OR GET /api/timeline?query=...
|
||||||
*/
|
*/
|
||||||
private async handleUnifiedTimeline(req: Request, res: Response): Promise<void> {
|
private handleUnifiedTimeline = this.wrapHandler(async (req: Request, res: Response): Promise<void> => {
|
||||||
try {
|
|
||||||
const result = await this.searchManager.timeline(req.query);
|
const result = await this.searchManager.timeline(req.query);
|
||||||
res.json(result.content);
|
res.json(result.content);
|
||||||
} catch (error) {
|
});
|
||||||
logger.failure('WORKER', 'Unified timeline failed', {}, error as Error);
|
|
||||||
res.status(500).json({ error: (error as Error).message });
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Semantic shortcut for finding decision observations
|
* Semantic shortcut for finding decision observations
|
||||||
* GET /api/decisions?format=index&limit=20
|
* GET /api/decisions?format=index&limit=20
|
||||||
*/
|
*/
|
||||||
private async handleDecisions(req: Request, res: Response): Promise<void> {
|
private handleDecisions = this.wrapHandler(async (req: Request, res: Response): Promise<void> => {
|
||||||
try {
|
|
||||||
const result = await this.searchManager.decisions(req.query);
|
const result = await this.searchManager.decisions(req.query);
|
||||||
res.json(result.content);
|
res.json(result.content);
|
||||||
} catch (error) {
|
});
|
||||||
logger.failure('WORKER', 'Decisions search failed', {}, error as Error);
|
|
||||||
res.status(500).json({ error: (error as Error).message });
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Semantic shortcut for finding change-related observations
|
* Semantic shortcut for finding change-related observations
|
||||||
* GET /api/changes?format=index&limit=20
|
* GET /api/changes?format=index&limit=20
|
||||||
*/
|
*/
|
||||||
private async handleChanges(req: Request, res: Response): Promise<void> {
|
private handleChanges = this.wrapHandler(async (req: Request, res: Response): Promise<void> => {
|
||||||
try {
|
|
||||||
const result = await this.searchManager.changes(req.query);
|
const result = await this.searchManager.changes(req.query);
|
||||||
res.json(result.content);
|
res.json(result.content);
|
||||||
} catch (error) {
|
});
|
||||||
logger.failure('WORKER', 'Changes search failed', {}, error as Error);
|
|
||||||
res.status(500).json({ error: (error as Error).message });
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Semantic shortcut for finding "how it works" explanations
|
* Semantic shortcut for finding "how it works" explanations
|
||||||
* GET /api/how-it-works?format=index&limit=20
|
* GET /api/how-it-works?format=index&limit=20
|
||||||
*/
|
*/
|
||||||
private async handleHowItWorks(req: Request, res: Response): Promise<void> {
|
private handleHowItWorks = this.wrapHandler(async (req: Request, res: Response): Promise<void> => {
|
||||||
try {
|
|
||||||
const result = await this.searchManager.howItWorks(req.query);
|
const result = await this.searchManager.howItWorks(req.query);
|
||||||
res.json(result.content);
|
res.json(result.content);
|
||||||
} catch (error) {
|
});
|
||||||
logger.failure('WORKER', 'How it works search failed', {}, error as Error);
|
|
||||||
res.status(500).json({ error: (error as Error).message });
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Search observations (use /api/search?type=observations instead)
|
* Search observations (use /api/search?type=observations instead)
|
||||||
* GET /api/search/observations?query=...&format=index&limit=20&project=...
|
* GET /api/search/observations?query=...&format=index&limit=20&project=...
|
||||||
*/
|
*/
|
||||||
private async handleSearchObservations(req: Request, res: Response): Promise<void> {
|
private handleSearchObservations = this.wrapHandler(async (req: Request, res: Response): Promise<void> => {
|
||||||
try {
|
|
||||||
const result = await this.searchManager.searchObservations(req.query);
|
const result = await this.searchManager.searchObservations(req.query);
|
||||||
res.json(result.content);
|
res.json(result.content);
|
||||||
} catch (error) {
|
});
|
||||||
logger.failure('WORKER', 'Search failed', {}, error as Error);
|
|
||||||
res.status(500).json({ error: (error as Error).message });
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Search session summaries
|
* Search session summaries
|
||||||
* GET /api/search/sessions?query=...&format=index&limit=20
|
* GET /api/search/sessions?query=...&format=index&limit=20
|
||||||
*/
|
*/
|
||||||
private async handleSearchSessions(req: Request, res: Response): Promise<void> {
|
private handleSearchSessions = this.wrapHandler(async (req: Request, res: Response): Promise<void> => {
|
||||||
try {
|
|
||||||
const result = await this.searchManager.searchSessions(req.query);
|
const result = await this.searchManager.searchSessions(req.query);
|
||||||
res.json(result.content);
|
res.json(result.content);
|
||||||
} catch (error) {
|
});
|
||||||
logger.failure('WORKER', 'Search failed', {}, error as Error);
|
|
||||||
res.status(500).json({ error: (error as Error).message });
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Search user prompts
|
* Search user prompts
|
||||||
* GET /api/search/prompts?query=...&format=index&limit=20
|
* GET /api/search/prompts?query=...&format=index&limit=20
|
||||||
*/
|
*/
|
||||||
private async handleSearchPrompts(req: Request, res: Response): Promise<void> {
|
private handleSearchPrompts = this.wrapHandler(async (req: Request, res: Response): Promise<void> => {
|
||||||
try {
|
|
||||||
const result = await this.searchManager.searchUserPrompts(req.query);
|
const result = await this.searchManager.searchUserPrompts(req.query);
|
||||||
res.json(result.content);
|
res.json(result.content);
|
||||||
} catch (error) {
|
});
|
||||||
logger.failure('WORKER', 'Search failed', {}, error as Error);
|
|
||||||
res.status(500).json({ error: (error as Error).message });
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Search observations by concept
|
* Search observations by concept
|
||||||
* GET /api/search/by-concept?concept=discovery&format=index&limit=5
|
* GET /api/search/by-concept?concept=discovery&format=index&limit=5
|
||||||
*/
|
*/
|
||||||
private async handleSearchByConcept(req: Request, res: Response): Promise<void> {
|
private handleSearchByConcept = this.wrapHandler(async (req: Request, res: Response): Promise<void> => {
|
||||||
try {
|
|
||||||
const result = await this.searchManager.findByConcept(req.query);
|
const result = await this.searchManager.findByConcept(req.query);
|
||||||
res.json(result.content);
|
res.json(result.content);
|
||||||
} catch (error) {
|
});
|
||||||
logger.failure('WORKER', 'Search failed', {}, error as Error);
|
|
||||||
res.status(500).json({ error: (error as Error).message });
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Search by file path
|
* Search by file path
|
||||||
* GET /api/search/by-file?filePath=...&format=index&limit=10
|
* GET /api/search/by-file?filePath=...&format=index&limit=10
|
||||||
*/
|
*/
|
||||||
private async handleSearchByFile(req: Request, res: Response): Promise<void> {
|
private handleSearchByFile = this.wrapHandler(async (req: Request, res: Response): Promise<void> => {
|
||||||
try {
|
|
||||||
const result = await this.searchManager.findByFile(req.query);
|
const result = await this.searchManager.findByFile(req.query);
|
||||||
res.json(result.content);
|
res.json(result.content);
|
||||||
} catch (error) {
|
});
|
||||||
logger.failure('WORKER', 'Search failed', {}, error as Error);
|
|
||||||
res.status(500).json({ error: (error as Error).message });
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Search observations by type
|
* Search observations by type
|
||||||
* GET /api/search/by-type?type=bugfix&format=index&limit=10
|
* GET /api/search/by-type?type=bugfix&format=index&limit=10
|
||||||
*/
|
*/
|
||||||
private async handleSearchByType(req: Request, res: Response): Promise<void> {
|
private handleSearchByType = this.wrapHandler(async (req: Request, res: Response): Promise<void> => {
|
||||||
try {
|
|
||||||
const result = await this.searchManager.findByType(req.query);
|
const result = await this.searchManager.findByType(req.query);
|
||||||
res.json(result.content);
|
res.json(result.content);
|
||||||
} catch (error) {
|
});
|
||||||
logger.failure('WORKER', 'Search failed', {}, error as Error);
|
|
||||||
res.status(500).json({ error: (error as Error).message });
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Get recent context (summaries and observations for a project)
|
* Get recent context (summaries and observations for a project)
|
||||||
* GET /api/context/recent?project=...&limit=3
|
* GET /api/context/recent?project=...&limit=3
|
||||||
*/
|
*/
|
||||||
private async handleGetRecentContext(req: Request, res: Response): Promise<void> {
|
private handleGetRecentContext = this.wrapHandler(async (req: Request, res: Response): Promise<void> => {
|
||||||
try {
|
|
||||||
const result = await this.searchManager.getRecentContext(req.query);
|
const result = await this.searchManager.getRecentContext(req.query);
|
||||||
res.json(result.content);
|
res.json(result.content);
|
||||||
} catch (error) {
|
});
|
||||||
logger.failure('WORKER', 'Search failed', {}, error as Error);
|
|
||||||
res.status(500).json({ error: (error as Error).message });
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Get context timeline around an anchor point
|
* Get context timeline around an anchor point
|
||||||
* GET /api/context/timeline?anchor=123&depth_before=10&depth_after=10&project=...
|
* GET /api/context/timeline?anchor=123&depth_before=10&depth_after=10&project=...
|
||||||
*/
|
*/
|
||||||
private async handleGetContextTimeline(req: Request, res: Response): Promise<void> {
|
private handleGetContextTimeline = this.wrapHandler(async (req: Request, res: Response): Promise<void> => {
|
||||||
try {
|
|
||||||
const result = await this.searchManager.getContextTimeline(req.query);
|
const result = await this.searchManager.getContextTimeline(req.query);
|
||||||
res.json(result.content);
|
res.json(result.content);
|
||||||
} catch (error) {
|
});
|
||||||
logger.failure('WORKER', 'Search failed', {}, error as Error);
|
|
||||||
res.status(500).json({ error: (error as Error).message });
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Generate context preview for settings modal
|
* Generate context preview for settings modal
|
||||||
* GET /api/context/preview?project=...
|
* GET /api/context/preview?project=...
|
||||||
*/
|
*/
|
||||||
private async handleContextPreview(req: Request, res: Response): Promise<void> {
|
private handleContextPreview = this.wrapHandler(async (req: Request, res: Response): Promise<void> => {
|
||||||
try {
|
|
||||||
const projectName = req.query.project as string;
|
const projectName = req.query.project as string;
|
||||||
|
|
||||||
if (!projectName) {
|
if (!projectName) {
|
||||||
res.status(400).json({ error: 'Project parameter is required' });
|
this.badRequest(res, 'Project parameter is required');
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -254,14 +190,7 @@ export class SearchRoutes {
|
|||||||
// Return as plain text
|
// Return as plain text
|
||||||
res.setHeader('Content-Type', 'text/plain; charset=utf-8');
|
res.setHeader('Content-Type', 'text/plain; charset=utf-8');
|
||||||
res.send(contextText);
|
res.send(contextText);
|
||||||
} catch (error) {
|
|
||||||
logger.failure('WORKER', 'Context preview generation failed', {}, error as Error);
|
|
||||||
res.status(500).json({
|
|
||||||
error: 'Failed to generate context preview',
|
|
||||||
message: (error as Error).message
|
|
||||||
});
|
});
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Context injection endpoint for hooks
|
* Context injection endpoint for hooks
|
||||||
@@ -270,13 +199,12 @@ export class SearchRoutes {
|
|||||||
* Returns pre-formatted context string ready for display.
|
* Returns pre-formatted context string ready for display.
|
||||||
* Use colors=true for ANSI-colored terminal output.
|
* Use colors=true for ANSI-colored terminal output.
|
||||||
*/
|
*/
|
||||||
private async handleContextInject(req: Request, res: Response): Promise<void> {
|
private handleContextInject = this.wrapHandler(async (req: Request, res: Response): Promise<void> => {
|
||||||
try {
|
|
||||||
const projectName = req.query.project as string;
|
const projectName = req.query.project as string;
|
||||||
const useColors = req.query.colors === 'true';
|
const useColors = req.query.colors === 'true';
|
||||||
|
|
||||||
if (!projectName) {
|
if (!projectName) {
|
||||||
res.status(400).json({ error: 'Project parameter is required' });
|
this.badRequest(res, 'Project parameter is required');
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -298,34 +226,22 @@ export class SearchRoutes {
|
|||||||
// Return as plain text
|
// Return as plain text
|
||||||
res.setHeader('Content-Type', 'text/plain; charset=utf-8');
|
res.setHeader('Content-Type', 'text/plain; charset=utf-8');
|
||||||
res.send(contextText);
|
res.send(contextText);
|
||||||
} catch (error) {
|
|
||||||
logger.failure('WORKER', 'Context injection failed', {}, error as Error);
|
|
||||||
res.status(500).json({
|
|
||||||
error: 'Failed to generate context',
|
|
||||||
message: (error as Error).message
|
|
||||||
});
|
});
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Get timeline by query (search first, then get timeline around best match)
|
* Get timeline by query (search first, then get timeline around best match)
|
||||||
* GET /api/timeline/by-query?query=...&mode=auto&depth_before=10&depth_after=10
|
* GET /api/timeline/by-query?query=...&mode=auto&depth_before=10&depth_after=10
|
||||||
*/
|
*/
|
||||||
private async handleGetTimelineByQuery(req: Request, res: Response): Promise<void> {
|
private handleGetTimelineByQuery = this.wrapHandler(async (req: Request, res: Response): Promise<void> => {
|
||||||
try {
|
|
||||||
const result = await this.searchManager.getTimelineByQuery(req.query);
|
const result = await this.searchManager.getTimelineByQuery(req.query);
|
||||||
res.json(result.content);
|
res.json(result.content);
|
||||||
} catch (error) {
|
});
|
||||||
logger.failure('WORKER', 'Search failed', {}, error as Error);
|
|
||||||
res.status(500).json({ error: (error as Error).message });
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Get search help documentation
|
* Get search help documentation
|
||||||
* GET /api/search/help
|
* GET /api/search/help
|
||||||
*/
|
*/
|
||||||
private handleSearchHelp(req: Request, res: Response): void {
|
private handleSearchHelp = this.wrapHandler((req: Request, res: Response): void => {
|
||||||
res.json({
|
res.json({
|
||||||
title: 'Claude-Mem Search API',
|
title: 'Claude-Mem Search API',
|
||||||
description: 'HTTP API for searching persistent memory',
|
description: 'HTTP API for searching persistent memory',
|
||||||
@@ -440,5 +356,5 @@ export class SearchRoutes {
|
|||||||
'curl "http://localhost:37777/api/context/timeline?anchor=123&depth_before=5&depth_after=5"'
|
'curl "http://localhost:37777/api/context/timeline?anchor=123&depth_before=5&depth_after=5"'
|
||||||
]
|
]
|
||||||
});
|
});
|
||||||
}
|
});
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -14,15 +14,18 @@ import { DatabaseManager } from '../../DatabaseManager.js';
|
|||||||
import { SDKAgent } from '../../SDKAgent.js';
|
import { SDKAgent } from '../../SDKAgent.js';
|
||||||
import { SSEBroadcaster } from '../../SSEBroadcaster.js';
|
import { SSEBroadcaster } from '../../SSEBroadcaster.js';
|
||||||
import type { WorkerService } from '../../../worker-service.js';
|
import type { WorkerService } from '../../../worker-service.js';
|
||||||
|
import { BaseRouteHandler } from '../BaseRouteHandler.js';
|
||||||
|
|
||||||
export class SessionRoutes {
|
export class SessionRoutes extends BaseRouteHandler {
|
||||||
constructor(
|
constructor(
|
||||||
private sessionManager: SessionManager,
|
private sessionManager: SessionManager,
|
||||||
private dbManager: DatabaseManager,
|
private dbManager: DatabaseManager,
|
||||||
private sdkAgent: SDKAgent,
|
private sdkAgent: SDKAgent,
|
||||||
private sseBroadcaster: SSEBroadcaster,
|
private sseBroadcaster: SSEBroadcaster,
|
||||||
private workerService: WorkerService
|
private workerService: WorkerService
|
||||||
) {}
|
) {
|
||||||
|
super();
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Ensures SDK agent generator is running for a session
|
* Ensures SDK agent generator is running for a session
|
||||||
@@ -66,9 +69,10 @@ export class SessionRoutes {
|
|||||||
/**
|
/**
|
||||||
* Initialize a new session
|
* Initialize a new session
|
||||||
*/
|
*/
|
||||||
private handleSessionInit(req: Request, res: Response): void {
|
private handleSessionInit = this.wrapHandler((req: Request, res: Response): void => {
|
||||||
try {
|
const sessionDbId = this.parseIntParam(req, res, 'sessionDbId');
|
||||||
const sessionDbId = parseInt(req.params.sessionDbId, 10);
|
if (sessionDbId === null) return;
|
||||||
|
|
||||||
const { userPrompt, promptNumber } = req.body;
|
const { userPrompt, promptNumber } = req.body;
|
||||||
const session = this.sessionManager.initializeSession(sessionDbId, userPrompt, promptNumber);
|
const session = this.sessionManager.initializeSession(sessionDbId, userPrompt, promptNumber);
|
||||||
|
|
||||||
@@ -153,19 +157,16 @@ export class SessionRoutes {
|
|||||||
});
|
});
|
||||||
|
|
||||||
res.json({ status: 'initialized', sessionDbId, port: getWorkerPort() });
|
res.json({ status: 'initialized', sessionDbId, port: getWorkerPort() });
|
||||||
} catch (error) {
|
});
|
||||||
logger.failure('WORKER', 'Session init failed', {}, error as Error);
|
|
||||||
res.status(500).json({ error: (error as Error).message });
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Queue observations for processing
|
* Queue observations for processing
|
||||||
* CRITICAL: Ensures SDK agent is running to process the queue (ALWAYS SAVE EVERYTHING)
|
* CRITICAL: Ensures SDK agent is running to process the queue (ALWAYS SAVE EVERYTHING)
|
||||||
*/
|
*/
|
||||||
private handleObservations(req: Request, res: Response): void {
|
private handleObservations = this.wrapHandler((req: Request, res: Response): void => {
|
||||||
try {
|
const sessionDbId = this.parseIntParam(req, res, 'sessionDbId');
|
||||||
const sessionDbId = parseInt(req.params.sessionDbId, 10);
|
if (sessionDbId === null) return;
|
||||||
|
|
||||||
const { tool_name, tool_input, tool_response, prompt_number, cwd } = req.body;
|
const { tool_name, tool_input, tool_response, prompt_number, cwd } = req.body;
|
||||||
|
|
||||||
this.sessionManager.queueObservation(sessionDbId, {
|
this.sessionManager.queueObservation(sessionDbId, {
|
||||||
@@ -189,19 +190,16 @@ export class SessionRoutes {
|
|||||||
});
|
});
|
||||||
|
|
||||||
res.json({ status: 'queued' });
|
res.json({ status: 'queued' });
|
||||||
} catch (error) {
|
});
|
||||||
logger.failure('WORKER', 'Observation queuing failed', {}, error as Error);
|
|
||||||
res.status(500).json({ error: (error as Error).message });
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Queue summarize request
|
* Queue summarize request
|
||||||
* CRITICAL: Ensures SDK agent is running to process the queue (ALWAYS SAVE EVERYTHING)
|
* CRITICAL: Ensures SDK agent is running to process the queue (ALWAYS SAVE EVERYTHING)
|
||||||
*/
|
*/
|
||||||
private handleSummarize(req: Request, res: Response): void {
|
private handleSummarize = this.wrapHandler((req: Request, res: Response): void => {
|
||||||
try {
|
const sessionDbId = this.parseIntParam(req, res, 'sessionDbId');
|
||||||
const sessionDbId = parseInt(req.params.sessionDbId, 10);
|
if (sessionDbId === null) return;
|
||||||
|
|
||||||
const { last_user_message, last_assistant_message } = req.body;
|
const { last_user_message, last_assistant_message } = req.body;
|
||||||
|
|
||||||
this.sessionManager.queueSummarize(sessionDbId, last_user_message, last_assistant_message);
|
this.sessionManager.queueSummarize(sessionDbId, last_user_message, last_assistant_message);
|
||||||
@@ -213,18 +211,15 @@ export class SessionRoutes {
|
|||||||
this.workerService.broadcastProcessingStatus();
|
this.workerService.broadcastProcessingStatus();
|
||||||
|
|
||||||
res.json({ status: 'queued' });
|
res.json({ status: 'queued' });
|
||||||
} catch (error) {
|
});
|
||||||
logger.failure('WORKER', 'Summarize queuing failed', {}, error as Error);
|
|
||||||
res.status(500).json({ error: (error as Error).message });
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Get session status
|
* Get session status
|
||||||
*/
|
*/
|
||||||
private handleSessionStatus(req: Request, res: Response): void {
|
private handleSessionStatus = this.wrapHandler((req: Request, res: Response): void => {
|
||||||
try {
|
const sessionDbId = this.parseIntParam(req, res, 'sessionDbId');
|
||||||
const sessionDbId = parseInt(req.params.sessionDbId, 10);
|
if (sessionDbId === null) return;
|
||||||
|
|
||||||
const session = this.sessionManager.getSession(sessionDbId);
|
const session = this.sessionManager.getSession(sessionDbId);
|
||||||
|
|
||||||
if (!session) {
|
if (!session) {
|
||||||
@@ -239,18 +234,15 @@ export class SessionRoutes {
|
|||||||
queueLength: session.pendingMessages.length,
|
queueLength: session.pendingMessages.length,
|
||||||
uptime: Date.now() - session.startTime
|
uptime: Date.now() - session.startTime
|
||||||
});
|
});
|
||||||
} catch (error) {
|
});
|
||||||
logger.failure('WORKER', 'Session status failed', {}, error as Error);
|
|
||||||
res.status(500).json({ error: (error as Error).message });
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Delete a session
|
* Delete a session
|
||||||
*/
|
*/
|
||||||
private async handleSessionDelete(req: Request, res: Response): Promise<void> {
|
private handleSessionDelete = this.wrapHandler(async (req: Request, res: Response): Promise<void> => {
|
||||||
try {
|
const sessionDbId = this.parseIntParam(req, res, 'sessionDbId');
|
||||||
const sessionDbId = parseInt(req.params.sessionDbId, 10);
|
if (sessionDbId === null) return;
|
||||||
|
|
||||||
await this.sessionManager.deleteSession(sessionDbId);
|
await this.sessionManager.deleteSession(sessionDbId);
|
||||||
|
|
||||||
// Mark session complete in database
|
// Mark session complete in database
|
||||||
@@ -263,23 +255,15 @@ export class SessionRoutes {
|
|||||||
});
|
});
|
||||||
|
|
||||||
res.json({ status: 'deleted' });
|
res.json({ status: 'deleted' });
|
||||||
} catch (error) {
|
});
|
||||||
logger.failure('WORKER', 'Session delete failed', {}, error as Error);
|
|
||||||
res.status(500).json({ error: (error as Error).message });
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Complete a session (backward compatibility for cleanup-hook)
|
* Complete a session (backward compatibility for cleanup-hook)
|
||||||
* cleanup-hook expects POST /sessions/:sessionDbId/complete instead of DELETE
|
* cleanup-hook expects POST /sessions/:sessionDbId/complete instead of DELETE
|
||||||
*/
|
*/
|
||||||
private async handleSessionComplete(req: Request, res: Response): Promise<void> {
|
private handleSessionComplete = this.wrapHandler(async (req: Request, res: Response): Promise<void> => {
|
||||||
try {
|
const sessionDbId = this.parseIntParam(req, res, 'sessionDbId');
|
||||||
const sessionDbId = parseInt(req.params.sessionDbId, 10);
|
if (sessionDbId === null) return;
|
||||||
if (isNaN(sessionDbId)) {
|
|
||||||
res.status(400).json({ success: false, error: 'Invalid session ID' });
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
await this.sessionManager.deleteSession(sessionDbId);
|
await this.sessionManager.deleteSession(sessionDbId);
|
||||||
|
|
||||||
@@ -297,24 +281,18 @@ export class SessionRoutes {
|
|||||||
});
|
});
|
||||||
|
|
||||||
res.json({ success: true });
|
res.json({ success: true });
|
||||||
} catch (error) {
|
});
|
||||||
logger.failure('WORKER', 'Session complete failed', {}, error as Error);
|
|
||||||
res.status(500).json({ success: false, error: String(error) });
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Queue observations by claudeSessionId (post-tool-use-hook uses this)
|
* Queue observations by claudeSessionId (post-tool-use-hook uses this)
|
||||||
* POST /api/sessions/observations
|
* POST /api/sessions/observations
|
||||||
* Body: { claudeSessionId, tool_name, tool_input, tool_response, cwd }
|
* Body: { claudeSessionId, tool_name, tool_input, tool_response, cwd }
|
||||||
*/
|
*/
|
||||||
private handleObservationsByClaudeId(req: Request, res: Response): void {
|
private handleObservationsByClaudeId = this.wrapHandler((req: Request, res: Response): void => {
|
||||||
try {
|
|
||||||
const { claudeSessionId, tool_name, tool_input, tool_response, cwd } = req.body;
|
const { claudeSessionId, tool_name, tool_input, tool_response, cwd } = req.body;
|
||||||
|
|
||||||
if (!claudeSessionId) {
|
if (!claudeSessionId) {
|
||||||
res.status(400).json({ error: 'Missing claudeSessionId' });
|
return this.badRequest(res, 'Missing claudeSessionId');
|
||||||
return;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
const store = this.dbManager.getSessionStore();
|
const store = this.dbManager.getSessionStore();
|
||||||
@@ -377,11 +355,7 @@ export class SessionRoutes {
|
|||||||
});
|
});
|
||||||
|
|
||||||
res.json({ status: 'queued' });
|
res.json({ status: 'queued' });
|
||||||
} catch (error) {
|
});
|
||||||
logger.failure('WORKER', 'Observation by claudeId failed', {}, error as Error);
|
|
||||||
res.status(500).json({ error: (error as Error).message });
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Queue summarize by claudeSessionId (summary-hook uses this)
|
* Queue summarize by claudeSessionId (summary-hook uses this)
|
||||||
@@ -390,13 +364,11 @@ export class SessionRoutes {
|
|||||||
*
|
*
|
||||||
* Checks privacy, queues summarize request for SDK agent
|
* Checks privacy, queues summarize request for SDK agent
|
||||||
*/
|
*/
|
||||||
private handleSummarizeByClaudeId(req: Request, res: Response): void {
|
private handleSummarizeByClaudeId = this.wrapHandler((req: Request, res: Response): void => {
|
||||||
try {
|
|
||||||
const { claudeSessionId, last_user_message, last_assistant_message } = req.body;
|
const { claudeSessionId, last_user_message, last_assistant_message } = req.body;
|
||||||
|
|
||||||
if (!claudeSessionId) {
|
if (!claudeSessionId) {
|
||||||
res.status(400).json({ error: 'Missing claudeSessionId' });
|
return this.badRequest(res, 'Missing claudeSessionId');
|
||||||
return;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
const store = this.dbManager.getSessionStore();
|
const store = this.dbManager.getSessionStore();
|
||||||
@@ -426,11 +398,7 @@ export class SessionRoutes {
|
|||||||
this.workerService.broadcastProcessingStatus();
|
this.workerService.broadcastProcessingStatus();
|
||||||
|
|
||||||
res.json({ status: 'queued' });
|
res.json({ status: 'queued' });
|
||||||
} catch (error) {
|
});
|
||||||
logger.failure('WORKER', 'Summarize by claudeId failed', {}, error as Error);
|
|
||||||
res.status(500).json({ error: (error as Error).message });
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Complete session by claudeSessionId (cleanup-hook uses this)
|
* Complete session by claudeSessionId (cleanup-hook uses this)
|
||||||
@@ -439,13 +407,11 @@ export class SessionRoutes {
|
|||||||
*
|
*
|
||||||
* Marks session complete, stops SDK agent, broadcasts status
|
* Marks session complete, stops SDK agent, broadcasts status
|
||||||
*/
|
*/
|
||||||
private async handleSessionCompleteByClaudeId(req: Request, res: Response): Promise<void> {
|
private handleSessionCompleteByClaudeId = this.wrapHandler(async (req: Request, res: Response): Promise<void> => {
|
||||||
try {
|
|
||||||
const { claudeSessionId } = req.body;
|
const { claudeSessionId } = req.body;
|
||||||
|
|
||||||
if (!claudeSessionId) {
|
if (!claudeSessionId) {
|
||||||
res.status(400).json({ success: false, error: 'Missing claudeSessionId' });
|
return this.badRequest(res, 'Missing claudeSessionId');
|
||||||
return;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
const store = this.dbManager.getSessionStore();
|
const store = this.dbManager.getSessionStore();
|
||||||
@@ -477,9 +443,5 @@ export class SessionRoutes {
|
|||||||
});
|
});
|
||||||
|
|
||||||
res.json({ success: true });
|
res.json({ success: true });
|
||||||
} catch (error) {
|
});
|
||||||
logger.failure('WORKER', 'Session complete by claudeId failed', {}, error as Error);
|
|
||||||
res.status(500).json({ success: false, error: String(error) });
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -21,11 +21,14 @@ import {
|
|||||||
ObservationType,
|
ObservationType,
|
||||||
ObservationConcept
|
ObservationConcept
|
||||||
} from '../../../../constants/observation-metadata.js';
|
} from '../../../../constants/observation-metadata.js';
|
||||||
|
import { BaseRouteHandler } from '../BaseRouteHandler.js';
|
||||||
|
|
||||||
export class SettingsRoutes {
|
export class SettingsRoutes extends BaseRouteHandler {
|
||||||
constructor(
|
constructor(
|
||||||
private settingsManager: SettingsManager
|
private settingsManager: SettingsManager
|
||||||
) {}
|
) {
|
||||||
|
super();
|
||||||
|
}
|
||||||
|
|
||||||
setupRoutes(app: express.Application): void {
|
setupRoutes(app: express.Application): void {
|
||||||
// Settings endpoints
|
// Settings endpoints
|
||||||
@@ -45,8 +48,7 @@ export class SettingsRoutes {
|
|||||||
/**
|
/**
|
||||||
* Get environment settings (from ~/.claude/settings.json)
|
* Get environment settings (from ~/.claude/settings.json)
|
||||||
*/
|
*/
|
||||||
private handleGetSettings(req: Request, res: Response): void {
|
private handleGetSettings = this.wrapHandler((req: Request, res: Response): void => {
|
||||||
try {
|
|
||||||
const settingsPath = path.join(homedir(), '.claude-mem', 'settings.json');
|
const settingsPath = path.join(homedir(), '.claude-mem', 'settings.json');
|
||||||
|
|
||||||
if (!existsSync(settingsPath)) {
|
if (!existsSync(settingsPath)) {
|
||||||
@@ -98,17 +100,12 @@ export class SettingsRoutes {
|
|||||||
CLAUDE_MEM_CONTEXT_SHOW_LAST_SUMMARY: env.CLAUDE_MEM_CONTEXT_SHOW_LAST_SUMMARY || 'true',
|
CLAUDE_MEM_CONTEXT_SHOW_LAST_SUMMARY: env.CLAUDE_MEM_CONTEXT_SHOW_LAST_SUMMARY || 'true',
|
||||||
CLAUDE_MEM_CONTEXT_SHOW_LAST_MESSAGE: env.CLAUDE_MEM_CONTEXT_SHOW_LAST_MESSAGE || 'false',
|
CLAUDE_MEM_CONTEXT_SHOW_LAST_MESSAGE: env.CLAUDE_MEM_CONTEXT_SHOW_LAST_MESSAGE || 'false',
|
||||||
});
|
});
|
||||||
} catch (error) {
|
});
|
||||||
logger.failure('WORKER', 'Get settings failed', {}, error as Error);
|
|
||||||
res.status(500).json({ error: (error as Error).message });
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Update environment settings (in ~/.claude/settings.json) with validation
|
* Update environment settings (in ~/.claude/settings.json) with validation
|
||||||
*/
|
*/
|
||||||
private handleUpdateSettings(req: Request, res: Response): void {
|
private handleUpdateSettings = this.wrapHandler((req: Request, res: Response): void => {
|
||||||
try {
|
|
||||||
// Validate CLAUDE_MEM_CONTEXT_OBSERVATIONS
|
// Validate CLAUDE_MEM_CONTEXT_OBSERVATIONS
|
||||||
if (req.body.CLAUDE_MEM_CONTEXT_OBSERVATIONS) {
|
if (req.body.CLAUDE_MEM_CONTEXT_OBSERVATIONS) {
|
||||||
const obsCount = parseInt(req.body.CLAUDE_MEM_CONTEXT_OBSERVATIONS, 10);
|
const obsCount = parseInt(req.body.CLAUDE_MEM_CONTEXT_OBSERVATIONS, 10);
|
||||||
@@ -184,65 +181,45 @@ export class SettingsRoutes {
|
|||||||
|
|
||||||
logger.info('WORKER', 'Settings updated');
|
logger.info('WORKER', 'Settings updated');
|
||||||
res.json({ success: true, message: 'Settings updated successfully' });
|
res.json({ success: true, message: 'Settings updated successfully' });
|
||||||
} catch (error) {
|
});
|
||||||
logger.failure('WORKER', 'Update settings failed', {}, error as Error);
|
|
||||||
res.status(500).json({ success: false, error: String(error) });
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* GET /api/mcp/status - Check if MCP search server is enabled
|
* GET /api/mcp/status - Check if MCP search server is enabled
|
||||||
*/
|
*/
|
||||||
private handleGetMcpStatus(req: Request, res: Response): void {
|
private handleGetMcpStatus = this.wrapHandler((req: Request, res: Response): void => {
|
||||||
try {
|
|
||||||
const enabled = this.isMcpEnabled();
|
const enabled = this.isMcpEnabled();
|
||||||
res.json({ enabled });
|
res.json({ enabled });
|
||||||
} catch (error) {
|
});
|
||||||
logger.failure('WORKER', 'Get MCP status failed', {}, error as Error);
|
|
||||||
res.status(500).json({ error: (error as Error).message });
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* POST /api/mcp/toggle - Toggle MCP search server on/off
|
* POST /api/mcp/toggle - Toggle MCP search server on/off
|
||||||
* Body: { enabled: boolean }
|
* Body: { enabled: boolean }
|
||||||
*/
|
*/
|
||||||
private handleToggleMcp(req: Request, res: Response): void {
|
private handleToggleMcp = this.wrapHandler((req: Request, res: Response): void => {
|
||||||
try {
|
|
||||||
const { enabled } = req.body;
|
const { enabled } = req.body;
|
||||||
|
|
||||||
if (typeof enabled !== 'boolean') {
|
if (typeof enabled !== 'boolean') {
|
||||||
res.status(400).json({ error: 'enabled must be a boolean' });
|
this.badRequest(res, 'enabled must be a boolean');
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
this.toggleMcp(enabled);
|
this.toggleMcp(enabled);
|
||||||
res.json({ success: true, enabled: this.isMcpEnabled() });
|
res.json({ success: true, enabled: this.isMcpEnabled() });
|
||||||
} catch (error) {
|
});
|
||||||
logger.failure('WORKER', 'Toggle MCP failed', {}, error as Error);
|
|
||||||
res.status(500).json({ success: false, error: (error as Error).message });
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* GET /api/branch/status - Get current branch information
|
* GET /api/branch/status - Get current branch information
|
||||||
*/
|
*/
|
||||||
private handleGetBranchStatus(req: Request, res: Response): void {
|
private handleGetBranchStatus = this.wrapHandler((req: Request, res: Response): void => {
|
||||||
try {
|
|
||||||
const info = getBranchInfo();
|
const info = getBranchInfo();
|
||||||
res.json(info);
|
res.json(info);
|
||||||
} catch (error) {
|
});
|
||||||
logger.failure('WORKER', 'Failed to get branch status', {}, error as Error);
|
|
||||||
res.status(500).json({ error: (error as Error).message });
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* POST /api/branch/switch - Switch to a different branch
|
* POST /api/branch/switch - Switch to a different branch
|
||||||
* Body: { branch: "main" | "beta/7.0" }
|
* Body: { branch: "main" | "beta/7.0" }
|
||||||
*/
|
*/
|
||||||
private async handleSwitchBranch(req: Request, res: Response): Promise<void> {
|
private handleSwitchBranch = this.wrapHandler(async (req: Request, res: Response): Promise<void> => {
|
||||||
try {
|
|
||||||
const { branch } = req.body;
|
const { branch } = req.body;
|
||||||
|
|
||||||
if (!branch) {
|
if (!branch) {
|
||||||
@@ -273,17 +250,12 @@ export class SettingsRoutes {
|
|||||||
}
|
}
|
||||||
|
|
||||||
res.json(result);
|
res.json(result);
|
||||||
} catch (error) {
|
});
|
||||||
logger.failure('WORKER', 'Branch switch failed', {}, error as Error);
|
|
||||||
res.status(500).json({ success: false, error: (error as Error).message });
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* POST /api/branch/update - Pull latest updates for current branch
|
* POST /api/branch/update - Pull latest updates for current branch
|
||||||
*/
|
*/
|
||||||
private async handleUpdateBranch(req: Request, res: Response): Promise<void> {
|
private handleUpdateBranch = this.wrapHandler(async (req: Request, res: Response): Promise<void> => {
|
||||||
try {
|
|
||||||
logger.info('WORKER', 'Branch update requested');
|
logger.info('WORKER', 'Branch update requested');
|
||||||
|
|
||||||
const result = await pullUpdates();
|
const result = await pullUpdates();
|
||||||
@@ -297,11 +269,7 @@ export class SettingsRoutes {
|
|||||||
}
|
}
|
||||||
|
|
||||||
res.json(result);
|
res.json(result);
|
||||||
} catch (error) {
|
});
|
||||||
logger.failure('WORKER', 'Branch update failed', {}, error as Error);
|
|
||||||
res.status(500).json({ success: false, error: (error as Error).message });
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Validate context settings from request body
|
* Validate context settings from request body
|
||||||
|
|||||||
@@ -9,17 +9,19 @@ import express, { Request, Response } from 'express';
|
|||||||
import path from 'path';
|
import path from 'path';
|
||||||
import { readFileSync } from 'fs';
|
import { readFileSync } from 'fs';
|
||||||
import { getPackageRoot } from '../../../../shared/paths.js';
|
import { getPackageRoot } from '../../../../shared/paths.js';
|
||||||
import { logger } from '../../../../utils/logger.js';
|
|
||||||
import { SSEBroadcaster } from '../../SSEBroadcaster.js';
|
import { SSEBroadcaster } from '../../SSEBroadcaster.js';
|
||||||
import { DatabaseManager } from '../../DatabaseManager.js';
|
import { DatabaseManager } from '../../DatabaseManager.js';
|
||||||
import { SessionManager } from '../../SessionManager.js';
|
import { SessionManager } from '../../SessionManager.js';
|
||||||
|
import { BaseRouteHandler } from '../BaseRouteHandler.js';
|
||||||
|
|
||||||
export class ViewerRoutes {
|
export class ViewerRoutes extends BaseRouteHandler {
|
||||||
constructor(
|
constructor(
|
||||||
private sseBroadcaster: SSEBroadcaster,
|
private sseBroadcaster: SSEBroadcaster,
|
||||||
private dbManager: DatabaseManager,
|
private dbManager: DatabaseManager,
|
||||||
private sessionManager: SessionManager
|
private sessionManager: SessionManager
|
||||||
) {}
|
) {
|
||||||
|
super();
|
||||||
|
}
|
||||||
|
|
||||||
setupRoutes(app: express.Application): void {
|
setupRoutes(app: express.Application): void {
|
||||||
app.get('/health', this.handleHealth.bind(this));
|
app.get('/health', this.handleHealth.bind(this));
|
||||||
@@ -30,30 +32,25 @@ export class ViewerRoutes {
|
|||||||
/**
|
/**
|
||||||
* Health check endpoint
|
* Health check endpoint
|
||||||
*/
|
*/
|
||||||
private handleHealth(req: Request, res: Response): void {
|
private handleHealth = this.wrapHandler((req: Request, res: Response): void => {
|
||||||
res.json({ status: 'ok', timestamp: Date.now() });
|
res.json({ status: 'ok', timestamp: Date.now() });
|
||||||
}
|
});
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Serve viewer UI
|
* Serve viewer UI
|
||||||
*/
|
*/
|
||||||
private handleViewerUI(req: Request, res: Response): void {
|
private handleViewerUI = this.wrapHandler((req: Request, res: Response): void => {
|
||||||
try {
|
|
||||||
const packageRoot = getPackageRoot();
|
const packageRoot = getPackageRoot();
|
||||||
const viewerPath = path.join(packageRoot, 'plugin', 'ui', 'viewer.html');
|
const viewerPath = path.join(packageRoot, 'plugin', 'ui', 'viewer.html');
|
||||||
const html = readFileSync(viewerPath, 'utf-8');
|
const html = readFileSync(viewerPath, 'utf-8');
|
||||||
res.setHeader('Content-Type', 'text/html');
|
res.setHeader('Content-Type', 'text/html');
|
||||||
res.send(html);
|
res.send(html);
|
||||||
} catch (error) {
|
});
|
||||||
logger.failure('WORKER', 'Viewer UI error', {}, error as Error);
|
|
||||||
res.status(500).json({ error: 'Failed to load viewer UI' });
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* SSE stream endpoint
|
* SSE stream endpoint
|
||||||
*/
|
*/
|
||||||
private handleSSEStream(req: Request, res: Response): void {
|
private handleSSEStream = this.wrapHandler((req: Request, res: Response): void => {
|
||||||
// Setup SSE headers
|
// Setup SSE headers
|
||||||
res.setHeader('Content-Type', 'text/event-stream');
|
res.setHeader('Content-Type', 'text/event-stream');
|
||||||
res.setHeader('Cache-Control', 'no-cache');
|
res.setHeader('Cache-Control', 'no-cache');
|
||||||
@@ -78,5 +75,5 @@ export class ViewerRoutes {
|
|||||||
isProcessing,
|
isProcessing,
|
||||||
queueDepth
|
queueDepth
|
||||||
});
|
});
|
||||||
}
|
});
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user