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

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
228 changes: 214 additions & 14 deletions dist/mcp-server.js

Large diffs are not rendered by default.

36 changes: 30 additions & 6 deletions dist/search-cli.js
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,9 @@ const args = process.argv.slice(2);
let mode = 'both';
let after;
let before;
let project;
let sessionId;
let gitBranch;
let limit = 10;
const queries = [];
for (let i = 0; i < args.length; i++) {
Expand All @@ -20,10 +23,13 @@ MODES:
--text Exact string matching only (for git SHAs, error codes)

OPTIONS:
--after DATE Only conversations after YYYY-MM-DD
--before DATE Only conversations before YYYY-MM-DD
--limit N Max results (default: 10)
--help, -h Show this help
--after DATE Only conversations after YYYY-MM-DD
--before DATE Only conversations before YYYY-MM-DD
--project NAME Filter by project name (exact match)
--session-id ID Filter by session ID (exact match)
--git-branch BRANCH Filter by git branch (exact match)
--limit N Max results (default: 10)
--help, -h Show this help

EXAMPLES:
# Semantic search
Expand All @@ -35,6 +41,12 @@ EXAMPLES:
# Time filtering
episodic-memory search --after 2025-09-01 "refactoring"

# Filter by project
episodic-memory search --project my-app "authentication"

# Filter by git branch
episodic-memory search --git-branch feat/login "auth flow"

# Combine modes
episodic-memory search --both "React Router data loading"

Expand All @@ -55,6 +67,15 @@ EXAMPLES:
else if (arg === '--before') {
before = args[++i];
}
else if (arg === '--project') {
project = args[++i];
}
else if (arg === '--session-id') {
sessionId = args[++i];
}
else if (arg === '--git-branch') {
gitBranch = args[++i];
}
else if (arg === '--limit') {
limit = parseInt(args[++i]);
}
Expand All @@ -70,7 +91,7 @@ if (queries.length === 0) {
}
// Multi-concept search if multiple queries provided
if (queries.length > 1) {
const options = { limit, after, before };
const options = { limit, after, before, project, session_id: sessionId, git_branch: gitBranch };
searchMultipleConcepts(queries, options)
.then(async (results) => {
console.log(await formatMultiConceptResults(results, queries));
Expand All @@ -86,7 +107,10 @@ else {
mode,
limit,
after,
before
before,
project,
session_id: sessionId,
git_branch: gitBranch,
};
searchConversations(queries[0], options)
.then(async (results) => {
Expand Down
3 changes: 3 additions & 0 deletions dist/search.d.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,9 @@ export interface SearchOptions {
mode?: 'vector' | 'text' | 'both';
after?: string;
before?: string;
project?: string;
session_id?: string;
git_branch?: string;
}
export declare function searchConversations(query: string, options?: SearchOptions): Promise<SearchResult[]>;
export declare function formatResults(results: Array<SearchResult & {
Expand Down
56 changes: 45 additions & 11 deletions dist/search.js
Original file line number Diff line number Diff line change
Expand Up @@ -13,22 +13,46 @@ function validateISODate(dateStr, paramName) {
throw new Error(`Invalid ${paramName} date: "${dateStr}". Not a valid calendar date.`);
}
}
const SAFE_STRING_REGEX = /^[a-zA-Z0-9._\-\/\s@:~]+$/;
function validateMetadataFilter(value, paramName) {
if (value.length > 500) {
throw new Error(`${paramName} filter too long (max 500 characters)`);
}
if (!SAFE_STRING_REGEX.test(value)) {
throw new Error(`Invalid ${paramName} filter: "${value}". Contains unsupported characters.`);
}
}
export async function searchConversations(query, options = {}) {
const { limit = 10, mode = 'both', after, before } = options;
// Validate date parameters
const { limit = 10, mode = 'both', after, before, project, session_id, git_branch } = options;
// Validate parameters
if (after)
validateISODate(after, '--after');
if (before)
validateISODate(before, '--before');
if (project)
validateMetadataFilter(project, 'project');
if (session_id)
validateMetadataFilter(session_id, 'session_id');
if (git_branch)
validateMetadataFilter(git_branch, 'git_branch');
const db = initDatabase();
let results = [];
// Build time filter clause
const timeFilter = [];
// Build filter clause (time + metadata)
const filters = [];
if (after)
timeFilter.push(`e.timestamp >= '${after}'`);
filters.push(`e.timestamp >= '${after}'`);
if (before)
timeFilter.push(`e.timestamp <= '${before}'`);
const timeClause = timeFilter.length > 0 ? `AND ${timeFilter.join(' AND ')}` : '';
filters.push(`e.timestamp <= '${before}'`);
if (project)
filters.push(`e.project = '${project}'`);
if (session_id)
filters.push(`e.session_id = '${session_id}'`);
if (git_branch)
filters.push(`e.git_branch = '${git_branch}'`);
const filterClause = filters.length > 0 ? `AND ${filters.join(' AND ')}` : '';
// vec0 applies KNN before WHERE post-filter, so over-fetch when metadata filters are active
const hasMetadataFilter = project || session_id || git_branch;
const effectiveK = hasMetadataFilter ? limit * 3 : limit;
if (mode === 'vector' || mode === 'both') {
// Vector similarity search
await initEmbeddings();
Expand All @@ -43,15 +67,21 @@ export async function searchConversations(query, options = {}) {
e.archive_path,
e.line_start,
e.line_end,
e.session_id,
e.git_branch,
vec.distance
FROM vec_exchanges AS vec
JOIN exchanges AS e ON vec.id = e.id
WHERE vec.embedding MATCH ?
AND k = ?
${timeClause}
${filterClause}
ORDER BY vec.distance ASC
`);
results = stmt.all(Buffer.from(new Float32Array(queryEmbedding).buffer), limit);
results = stmt.all(Buffer.from(new Float32Array(queryEmbedding).buffer), effectiveK);
// Trim to requested limit after post-filtering
if (hasMetadataFilter && results.length > limit) {
results = results.slice(0, limit);
}
}
if (mode === 'text' || mode === 'both') {
// Text search
Expand All @@ -65,10 +95,12 @@ export async function searchConversations(query, options = {}) {
e.archive_path,
e.line_start,
e.line_end,
e.session_id,
e.git_branch,
0 as distance
FROM exchanges AS e
WHERE (e.user_message LIKE ? OR e.assistant_message LIKE ?)
${timeClause}
${filterClause}
ORDER BY e.timestamp DESC
LIMIT ?
`);
Expand Down Expand Up @@ -96,7 +128,9 @@ export async function searchConversations(query, options = {}) {
assistantMessage: row.assistant_message,
archivePath: row.archive_path,
lineStart: row.line_start,
lineEnd: row.line_end
lineEnd: row.line_end,
sessionId: row.session_id || undefined,
gitBranch: row.git_branch || undefined,
};
// Try to load summary if available
const summaryPath = row.archive_path.replace('.jsonl', '-summary.txt');
Expand Down
24 changes: 24 additions & 0 deletions src/mcp-server.ts
Original file line number Diff line number Diff line change
Expand Up @@ -61,6 +61,21 @@ const SearchInputSchema = z
.regex(/^\d{4}-\d{2}-\d{2}$/, 'Date must be in YYYY-MM-DD format')
.optional()
.describe('Only return conversations before this date (YYYY-MM-DD format)'),
project: z
.string()
.min(1)
.optional()
.describe('Filter by project name (exact match)'),
session_id: z
.string()
.min(1)
.optional()
.describe('Filter by session ID (exact match)'),
git_branch: z
.string()
.min(1)
.optional()
.describe('Filter by git branch (exact match)'),
response_format: ResponseFormatEnum.default('markdown').describe(
'Output format: "markdown" for human-readable or "json" for machine-readable (default: "markdown")'
),
Expand Down Expand Up @@ -136,6 +151,9 @@ server.setRequestHandler(ListToolsRequestSchema, async () => {
limit: { type: 'number', minimum: 1, maximum: 50, default: 10 },
after: { type: 'string', pattern: '^\\d{4}-\\d{2}-\\d{2}$' },
before: { type: 'string', pattern: '^\\d{4}-\\d{2}-\\d{2}$' },
project: { type: 'string', minLength: 1, description: 'Filter by project name' },
session_id: { type: 'string', minLength: 1, description: 'Filter by session ID' },
git_branch: { type: 'string', minLength: 1, description: 'Filter by git branch' },
response_format: { type: 'string', enum: ['markdown', 'json'], default: 'markdown' },
},
required: ['query'],
Expand Down Expand Up @@ -191,6 +209,9 @@ server.setRequestHandler(CallToolRequestSchema, async (request) => {
limit: params.limit,
after: params.after,
before: params.before,
project: params.project,
session_id: params.session_id,
git_branch: params.git_branch,
};

const results = await searchMultipleConcepts(params.query, options);
Expand All @@ -215,6 +236,9 @@ server.setRequestHandler(CallToolRequestSchema, async (request) => {
limit: params.limit,
after: params.after,
before: params.before,
project: params.project,
session_id: params.session_id,
git_branch: params.git_branch,
};

const results = await searchConversations(params.query, options);
Expand Down
33 changes: 27 additions & 6 deletions src/search-cli.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,9 @@ const args = process.argv.slice(2);
let mode: 'vector' | 'text' | 'both' = 'both';
let after: string | undefined;
let before: string | undefined;
let project: string | undefined;
let sessionId: string | undefined;
let gitBranch: string | undefined;
let limit = 10;
const queries: string[] = [];

Expand All @@ -24,10 +27,13 @@ MODES:
--text Exact string matching only (for git SHAs, error codes)

OPTIONS:
--after DATE Only conversations after YYYY-MM-DD
--before DATE Only conversations before YYYY-MM-DD
--limit N Max results (default: 10)
--help, -h Show this help
--after DATE Only conversations after YYYY-MM-DD
--before DATE Only conversations before YYYY-MM-DD
--project NAME Filter by project name (exact match)
--session-id ID Filter by session ID (exact match)
--git-branch BRANCH Filter by git branch (exact match)
--limit N Max results (default: 10)
--help, -h Show this help

EXAMPLES:
# Semantic search
Expand All @@ -39,6 +45,12 @@ EXAMPLES:
# Time filtering
episodic-memory search --after 2025-09-01 "refactoring"

# Filter by project
episodic-memory search --project my-app "authentication"

# Filter by git branch
episodic-memory search --git-branch feat/login "auth flow"

# Combine modes
episodic-memory search --both "React Router data loading"

Expand All @@ -54,6 +66,12 @@ EXAMPLES:
after = args[++i];
} else if (arg === '--before') {
before = args[++i];
} else if (arg === '--project') {
project = args[++i];
} else if (arg === '--session-id') {
sessionId = args[++i];
} else if (arg === '--git-branch') {
gitBranch = args[++i];
} else if (arg === '--limit') {
limit = parseInt(args[++i]);
} else {
Expand All @@ -70,7 +88,7 @@ if (queries.length === 0) {

// Multi-concept search if multiple queries provided
if (queries.length > 1) {
const options = { limit, after, before };
const options = { limit, after, before, project, session_id: sessionId, git_branch: gitBranch };

searchMultipleConcepts(queries, options)
.then(async results => {
Expand All @@ -86,7 +104,10 @@ if (queries.length > 1) {
mode,
limit,
after,
before
before,
project,
session_id: sessionId,
git_branch: gitBranch,
};

searchConversations(queries[0], options)
Expand Down
Loading