fix: Simplify search endpoint parameters to avoid bracket encoding

Replace complex array/object parameters with simple comma-separated strings
and flat date parameters to eliminate annoying URL bracket encoding issues.

Changes:
- Add normalizeParams() helper to convert URL-friendly params to internal format
- Replace obs_type/concepts/files arrays with comma-separated strings
- Replace dateRange object with dateStart/dateEnd scalar params
- Update all tool schemas to use simplified parameters
- Add normalization to all tool handlers

Examples of new simplified URLs:
- Before: ?concepts[]=decision&concepts[]=bugfix&dateRange[start]=2024-01-01
- After: ?concepts=decision,bugfix&dateStart=2024-01-01

All endpoints tested and working correctly.

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude <noreply@anthropic.com>
This commit is contained in:
Alex Newman
2025-11-30 18:46:39 -05:00
parent 4eb6557fbb
commit 50535499d9
3 changed files with 187 additions and 164 deletions
+2 -2
View File
@@ -1,12 +1,12 @@
{ {
"name": "claude-mem", "name": "claude-mem",
"version": "6.0.9", "version": "6.3.6",
"lockfileVersion": 3, "lockfileVersion": 3,
"requires": true, "requires": true,
"packages": { "packages": {
"": { "": {
"name": "claude-mem", "name": "claude-mem",
"version": "6.0.9", "version": "6.3.6",
"license": "AGPL-3.0", "license": "AGPL-3.0",
"dependencies": { "dependencies": {
"@anthropic-ai/claude-agent-sdk": "^0.1.27", "@anthropic-ai/claude-agent-sdk": "^0.1.27",
File diff suppressed because one or more lines are too long
+90 -67
View File
@@ -378,20 +378,62 @@ function formatUserPromptResult(prompt: UserPromptSearchResult): string {
} }
/** /**
* Common filter schema * Helper to normalize query parameters from URL-friendly format
* Converts comma-separated strings to arrays and flattens date params
*/
function normalizeParams(args: any): any {
const normalized: any = { ...args };
// Parse comma-separated concepts into array
if (normalized.concepts && typeof normalized.concepts === 'string') {
normalized.concepts = normalized.concepts.split(',').map((s: string) => s.trim()).filter(Boolean);
}
// Parse comma-separated files into array
if (normalized.files && typeof normalized.files === 'string') {
normalized.files = normalized.files.split(',').map((s: string) => s.trim()).filter(Boolean);
}
// Parse comma-separated obs_type into array
if (normalized.obs_type && typeof normalized.obs_type === 'string') {
normalized.obs_type = normalized.obs_type.split(',').map((s: string) => s.trim()).filter(Boolean);
}
// Parse comma-separated type (for filterSchema) into array
if (normalized.type && typeof normalized.type === 'string' && normalized.type.includes(',')) {
normalized.type = normalized.type.split(',').map((s: string) => s.trim()).filter(Boolean);
}
// Flatten dateStart/dateEnd into dateRange object
if (normalized.dateStart || normalized.dateEnd) {
normalized.dateRange = {
start: normalized.dateStart,
end: normalized.dateEnd
};
delete normalized.dateStart;
delete normalized.dateEnd;
}
return normalized;
}
/**
* Common filter schema (accepts simple strings that get normalized to arrays)
*/ */
const filterSchema = z.object({ const filterSchema = z.object({
project: z.string().optional().describe('Filter by project name'), project: z.string().optional().describe('Filter by project name'),
type: z.union([ type: z.union([
z.enum(['decision', 'bugfix', 'feature', 'refactor', 'discovery', 'change']), z.enum(['decision', 'bugfix', 'feature', 'refactor', 'discovery', 'change']),
z.array(z.enum(['decision', 'bugfix', 'feature', 'refactor', 'discovery', 'change'])) z.array(z.enum(['decision', 'bugfix', 'feature', 'refactor', 'discovery', 'change']))
]).optional().describe('Filter by observation type'), ]).optional().describe('Filter by observation type (single value or comma-separated list)'),
concepts: z.union([z.string(), z.array(z.string())]).optional().describe('Filter by concept tags'), concepts: z.union([z.string(), z.array(z.string())]).optional().describe('Filter by concept tags (single value or comma-separated list)'),
files: z.union([z.string(), z.array(z.string())]).optional().describe('Filter by file paths (partial match)'), files: z.union([z.string(), z.array(z.string())]).optional().describe('Filter by file paths (single value or comma-separated list for partial match)'),
dateStart: z.union([z.string(), z.number()]).optional().describe('Start date (ISO string or epoch)'),
dateEnd: z.union([z.string(), z.number()]).optional().describe('End date (ISO string or epoch)'),
dateRange: z.object({ dateRange: z.object({
start: z.union([z.string(), z.number()]).optional().describe('Start date (ISO string or epoch)'), start: z.union([z.string(), z.number()]).optional().describe('Start date (ISO string or epoch)'),
end: z.union([z.string(), z.number()]).optional().describe('End date (ISO string or epoch)') end: z.union([z.string(), z.number()]).optional().describe('End date (ISO string or epoch)')
}).optional().describe('Filter by date range'), }).optional().describe('Filter by date range (use dateStart/dateEnd instead for simpler URLs)'),
limit: z.number().min(1).max(100).default(20).describe('Maximum number of results'), limit: z.number().min(1).max(100).default(20).describe('Maximum number of results'),
offset: z.number().min(0).default(0).describe('Number of results to skip'), offset: z.number().min(0).default(0).describe('Number of results to skip'),
orderBy: z.enum(['relevance', 'date_desc', 'date_asc']).default('date_desc').describe('Sort order') orderBy: z.enum(['relevance', 'date_desc', 'date_asc']).default('date_desc').describe('Sort order')
@@ -406,30 +448,21 @@ const tools = [
query: z.string().optional().describe('Natural language search query for semantic ranking via ChromaDB vector search. Optional - omit for date-filtered queries only (Chroma cannot filter by date, requires direct SQLite).'), query: z.string().optional().describe('Natural language search query for semantic ranking via ChromaDB vector search. Optional - omit for date-filtered queries only (Chroma cannot filter by date, requires direct SQLite).'),
format: z.enum(['index', 'full']).default('index').describe('Output format: "index" for titles/dates only (default, RECOMMENDED for initial search), "full" for complete details (use only after reviewing index results)'), format: z.enum(['index', 'full']).default('index').describe('Output format: "index" for titles/dates only (default, RECOMMENDED for initial search), "full" for complete details (use only after reviewing index results)'),
type: z.enum(['observations', 'sessions', 'prompts']).optional().describe('Filter by document type (observations, sessions, or prompts). Omit to search all types.'), type: z.enum(['observations', 'sessions', 'prompts']).optional().describe('Filter by document type (observations, sessions, or prompts). Omit to search all types.'),
obs_type: z.union([ obs_type: z.string().optional().describe('Filter observations by type (single value or comma-separated list: decision,bugfix,feature,refactor,discovery,change). Only applies when type="observations"'),
z.enum(['decision', 'bugfix', 'feature', 'refactor', 'discovery', 'change']), concepts: z.string().optional().describe('Filter by concept tags (single value or comma-separated list). Only applies when type="observations"'),
z.array(z.enum(['decision', 'bugfix', 'feature', 'refactor', 'discovery', 'change'])) files: z.string().optional().describe('Filter by file paths (single value or comma-separated list for partial match). Only applies when type="observations"'),
]).optional().describe('Filter observations by type. Only applies when type="observations"'),
concepts: z.union([
z.string(),
z.array(z.string())
]).optional().describe('Filter by concept tags. Only applies when type="observations"'),
files: z.union([
z.string(),
z.array(z.string())
]).optional().describe('Filter by file paths (partial match). Only applies when type="observations"'),
project: z.string().optional().describe('Filter by project name'), project: z.string().optional().describe('Filter by project name'),
dateRange: z.object({ dateStart: z.union([z.string(), z.number()]).optional().describe('Start date for filtering (ISO string or epoch timestamp)'),
start: z.union([z.string(), z.number()]).optional().describe('Start date (ISO string or epoch)'), dateEnd: z.union([z.string(), z.number()]).optional().describe('End date for filtering (ISO string or epoch timestamp)'),
end: z.union([z.string(), z.number()]).optional().describe('End date (ISO string or epoch)')
}).optional().describe('Filter by date range'),
limit: z.number().min(1).max(100).default(20).describe('Maximum number of results'), limit: z.number().min(1).max(100).default(20).describe('Maximum number of results'),
offset: z.number().min(0).default(0).describe('Number of results to skip'), offset: z.number().min(0).default(0).describe('Number of results to skip'),
orderBy: z.enum(['relevance', 'date_desc', 'date_asc']).default('date_desc').describe('Sort order') orderBy: z.enum(['relevance', 'date_desc', 'date_asc']).default('date_desc').describe('Sort order')
}), }),
handler: async (args: any) => { handler: async (args: any) => {
try { try {
const { query, format = 'index', type, obs_type, concepts, files, ...options } = args; // Normalize URL-friendly params to internal format
const normalized = normalizeParams(args);
const { query, format = 'index', type, obs_type, concepts, files, ...options } = normalized;
let observations: ObservationSearchResult[] = []; let observations: ObservationSearchResult[] = [];
let sessions: SessionSummarySearchResult[] = []; let sessions: SessionSummarySearchResult[] = [];
let prompts: UserPromptSearchResult[] = []; let prompts: UserPromptSearchResult[] = [];
@@ -969,17 +1002,16 @@ const tools = [
query: z.string().optional().describe('Search query to filter decisions semantically'), query: z.string().optional().describe('Search query to filter decisions semantically'),
format: z.enum(['index', 'full']).default('index').describe('Output format: "index" for titles/dates only (default), "full" for complete details'), format: z.enum(['index', 'full']).default('index').describe('Output format: "index" for titles/dates only (default), "full" for complete details'),
project: z.string().optional().describe('Filter by project name'), project: z.string().optional().describe('Filter by project name'),
dateRange: z.object({ dateStart: z.union([z.string(), z.number()]).optional().describe('Start date for filtering (ISO string or epoch timestamp)'),
start: z.union([z.string(), z.number()]).optional(), dateEnd: z.union([z.string(), z.number()]).optional().describe('End date for filtering (ISO string or epoch timestamp)'),
end: z.union([z.string(), z.number()]).optional()
}).optional().describe('Filter by date range'),
limit: z.number().min(1).max(100).default(20).describe('Maximum number of results'), limit: z.number().min(1).max(100).default(20).describe('Maximum number of results'),
offset: z.number().min(0).default(0).describe('Number of results to skip'), offset: z.number().min(0).default(0).describe('Number of results to skip'),
orderBy: z.enum(['relevance', 'date_desc', 'date_asc']).default('date_desc').describe('Sort order') orderBy: z.enum(['relevance', 'date_desc', 'date_asc']).default('date_desc').describe('Sort order')
}), }),
handler: async (args: any) => { handler: async (args: any) => {
try { try {
const { query, format = 'index', ...filters } = args; const normalized = normalizeParams(args);
const { query, format = 'index', ...filters } = normalized;
let results: ObservationSearchResult[] = []; let results: ObservationSearchResult[] = [];
// Search for decision-type observations // Search for decision-type observations
@@ -1069,17 +1101,16 @@ const tools = [
inputSchema: z.object({ inputSchema: z.object({
format: z.enum(['index', 'full']).default('index').describe('Output format: "index" for titles/dates only (default), "full" for complete details'), format: z.enum(['index', 'full']).default('index').describe('Output format: "index" for titles/dates only (default), "full" for complete details'),
project: z.string().optional().describe('Filter by project name'), project: z.string().optional().describe('Filter by project name'),
dateRange: z.object({ dateStart: z.union([z.string(), z.number()]).optional().describe('Start date for filtering (ISO string or epoch timestamp)'),
start: z.union([z.string(), z.number()]).optional(), dateEnd: z.union([z.string(), z.number()]).optional().describe('End date for filtering (ISO string or epoch timestamp)'),
end: z.union([z.string(), z.number()]).optional()
}).optional().describe('Filter by date range'),
limit: z.number().min(1).max(100).default(20).describe('Maximum number of results'), limit: z.number().min(1).max(100).default(20).describe('Maximum number of results'),
offset: z.number().min(0).default(0).describe('Number of results to skip'), offset: z.number().min(0).default(0).describe('Number of results to skip'),
orderBy: z.enum(['relevance', 'date_desc', 'date_asc']).default('date_desc').describe('Sort order') orderBy: z.enum(['relevance', 'date_desc', 'date_asc']).default('date_desc').describe('Sort order')
}), }),
handler: async (args: any) => { handler: async (args: any) => {
try { try {
const { format = 'index', ...filters } = args; const normalized = normalizeParams(args);
const { format = 'index', ...filters } = normalized;
let results: ObservationSearchResult[] = []; let results: ObservationSearchResult[] = [];
// Search for change-type observations and change-related concepts // Search for change-type observations and change-related concepts
@@ -1177,17 +1208,16 @@ const tools = [
inputSchema: z.object({ inputSchema: z.object({
format: z.enum(['index', 'full']).default('index').describe('Output format: "index" for titles/dates only (default), "full" for complete details'), format: z.enum(['index', 'full']).default('index').describe('Output format: "index" for titles/dates only (default), "full" for complete details'),
project: z.string().optional().describe('Filter by project name'), project: z.string().optional().describe('Filter by project name'),
dateRange: z.object({ dateStart: z.union([z.string(), z.number()]).optional().describe('Start date for filtering (ISO string or epoch timestamp)'),
start: z.union([z.string(), z.number()]).optional(), dateEnd: z.union([z.string(), z.number()]).optional().describe('End date for filtering (ISO string or epoch timestamp)'),
end: z.union([z.string(), z.number()]).optional()
}).optional().describe('Filter by date range'),
limit: z.number().min(1).max(100).default(20).describe('Maximum number of results'), limit: z.number().min(1).max(100).default(20).describe('Maximum number of results'),
offset: z.number().min(0).default(0).describe('Number of results to skip'), offset: z.number().min(0).default(0).describe('Number of results to skip'),
orderBy: z.enum(['relevance', 'date_desc', 'date_asc']).default('date_desc').describe('Sort order') orderBy: z.enum(['relevance', 'date_desc', 'date_asc']).default('date_desc').describe('Sort order')
}), }),
handler: async (args: any) => { handler: async (args: any) => {
try { try {
const { format = 'index', ...filters } = args; const normalized = normalizeParams(args);
const { format = 'index', ...filters } = normalized;
let results: ObservationSearchResult[] = []; let results: ObservationSearchResult[] = [];
// Search for how-it-works concept observations // Search for how-it-works concept observations
@@ -1267,7 +1297,8 @@ const tools = [
}), }),
handler: async (args: any) => { handler: async (args: any) => {
try { try {
const { query, format = 'index', ...options } = args; const normalized = normalizeParams(args);
const { query, format = 'index', ...options } = normalized;
let results: ObservationSearchResult[] = []; let results: ObservationSearchResult[] = [];
// Vector-first search via ChromaDB // Vector-first search via ChromaDB
@@ -1345,17 +1376,16 @@ const tools = [
query: z.string().describe('Natural language search query for semantic ranking via ChromaDB vector search'), query: z.string().describe('Natural language search query for semantic ranking via ChromaDB vector search'),
format: z.enum(['index', 'full']).default('index').describe('Output format: "index" for titles/dates only (default, RECOMMENDED for initial search), "full" for complete details (use only after reviewing index results)'), format: z.enum(['index', 'full']).default('index').describe('Output format: "index" for titles/dates only (default, RECOMMENDED for initial search), "full" for complete details (use only after reviewing index results)'),
project: z.string().optional().describe('Filter by project name'), project: z.string().optional().describe('Filter by project name'),
dateRange: z.object({ dateStart: z.union([z.string(), z.number()]).optional().describe('Start date for filtering (ISO string or epoch timestamp)'),
start: z.union([z.string(), z.number()]).optional(), dateEnd: z.union([z.string(), z.number()]).optional().describe('End date for filtering (ISO string or epoch timestamp)'),
end: z.union([z.string(), z.number()]).optional()
}).optional().describe('Filter by date range'),
limit: z.number().min(1).max(100).default(20).describe('Maximum number of results'), limit: z.number().min(1).max(100).default(20).describe('Maximum number of results'),
offset: z.number().min(0).default(0).describe('Number of results to skip'), offset: z.number().min(0).default(0).describe('Number of results to skip'),
orderBy: z.enum(['relevance', 'date_desc', 'date_asc']).default('date_desc').describe('Sort order') orderBy: z.enum(['relevance', 'date_desc', 'date_asc']).default('date_desc').describe('Sort order')
}), }),
handler: async (args: any) => { handler: async (args: any) => {
try { try {
const { query, format = 'index', ...options } = args; const normalized = normalizeParams(args);
const { query, format = 'index', ...options } = normalized;
let results: SessionSummarySearchResult[] = []; let results: SessionSummarySearchResult[] = [];
// Vector-first search via ChromaDB // Vector-first search via ChromaDB
@@ -1433,17 +1463,16 @@ const tools = [
concept: z.string().describe('Concept tag to search for. Available: discovery, problem-solution, what-changed, how-it-works, pattern, gotcha, change'), concept: z.string().describe('Concept tag to search for. Available: discovery, problem-solution, what-changed, how-it-works, pattern, gotcha, change'),
format: z.enum(['index', 'full']).default('index').describe('Output format: "index" for titles/dates only (default, RECOMMENDED for initial search), "full" for complete details (use only after reviewing index results)'), format: z.enum(['index', 'full']).default('index').describe('Output format: "index" for titles/dates only (default, RECOMMENDED for initial search), "full" for complete details (use only after reviewing index results)'),
project: z.string().optional().describe('Filter by project name'), project: z.string().optional().describe('Filter by project name'),
dateRange: z.object({ dateStart: z.union([z.string(), z.number()]).optional().describe('Start date for filtering (ISO string or epoch timestamp)'),
start: z.union([z.string(), z.number()]).optional(), dateEnd: z.union([z.string(), z.number()]).optional().describe('End date for filtering (ISO string or epoch timestamp)'),
end: z.union([z.string(), z.number()]).optional()
}).optional().describe('Filter by date range'),
limit: z.number().min(1).max(100).default(20).describe('Maximum results. IMPORTANT: Start with 3-5 to avoid exceeding MCP token limits, even in index mode.'), limit: z.number().min(1).max(100).default(20).describe('Maximum results. IMPORTANT: Start with 3-5 to avoid exceeding MCP token limits, even in index mode.'),
offset: z.number().min(0).default(0).describe('Number of results to skip'), offset: z.number().min(0).default(0).describe('Number of results to skip'),
orderBy: z.enum(['relevance', 'date_desc', 'date_asc']).default('date_desc').describe('Sort order') orderBy: z.enum(['relevance', 'date_desc', 'date_asc']).default('date_desc').describe('Sort order')
}), }),
handler: async (args: any) => { handler: async (args: any) => {
try { try {
const { concept, format = 'index', ...filters } = args; const normalized = normalizeParams(args);
const { concept, format = 'index', ...filters } = normalized;
let results: ObservationSearchResult[] = []; let results: ObservationSearchResult[] = [];
// Metadata-first, semantic-enhanced search // Metadata-first, semantic-enhanced search
@@ -1533,17 +1562,16 @@ const tools = [
filePath: z.string().describe('File path to search for (supports partial matching)'), filePath: z.string().describe('File path to search for (supports partial matching)'),
format: z.enum(['index', 'full']).default('index').describe('Output format: "index" for titles/dates only (default, RECOMMENDED for initial search), "full" for complete details (use only after reviewing index results)'), format: z.enum(['index', 'full']).default('index').describe('Output format: "index" for titles/dates only (default, RECOMMENDED for initial search), "full" for complete details (use only after reviewing index results)'),
project: z.string().optional().describe('Filter by project name'), project: z.string().optional().describe('Filter by project name'),
dateRange: z.object({ dateStart: z.union([z.string(), z.number()]).optional().describe('Start date for filtering (ISO string or epoch timestamp)'),
start: z.union([z.string(), z.number()]).optional(), dateEnd: z.union([z.string(), z.number()]).optional().describe('End date for filtering (ISO string or epoch timestamp)'),
end: z.union([z.string(), z.number()]).optional()
}).optional().describe('Filter by date range'),
limit: z.number().min(1).max(100).default(20).describe('Maximum results. IMPORTANT: Start with 3-5 to avoid exceeding MCP token limits, even in index mode.'), limit: z.number().min(1).max(100).default(20).describe('Maximum results. IMPORTANT: Start with 3-5 to avoid exceeding MCP token limits, even in index mode.'),
offset: z.number().min(0).default(0).describe('Number of results to skip'), offset: z.number().min(0).default(0).describe('Number of results to skip'),
orderBy: z.enum(['relevance', 'date_desc', 'date_asc']).default('date_desc').describe('Sort order') orderBy: z.enum(['relevance', 'date_desc', 'date_asc']).default('date_desc').describe('Sort order')
}), }),
handler: async (args: any) => { handler: async (args: any) => {
try { try {
const { filePath, format = 'index', ...filters } = args; const normalized = normalizeParams(args);
const { filePath, format = 'index', ...filters } = normalized;
let observations: ObservationSearchResult[] = []; let observations: ObservationSearchResult[] = [];
let sessions: SessionSummarySearchResult[] = []; let sessions: SessionSummarySearchResult[] = [];
@@ -1660,23 +1688,19 @@ const tools = [
name: 'find_by_type', name: 'find_by_type',
description: 'Find observations of a specific type (decision, bugfix, feature, refactor, discovery, change). IMPORTANT: Always use index format first (default) to get an overview with minimal token usage, then use format: "full" only for specific items of interest.', description: 'Find observations of a specific type (decision, bugfix, feature, refactor, discovery, change). IMPORTANT: Always use index format first (default) to get an overview with minimal token usage, then use format: "full" only for specific items of interest.',
inputSchema: z.object({ inputSchema: z.object({
type: z.union([ type: z.string().describe('Observation type(s) to filter by (single value or comma-separated list: decision,bugfix,feature,refactor,discovery,change)'),
z.enum(['decision', 'bugfix', 'feature', 'refactor', 'discovery', 'change']),
z.array(z.enum(['decision', 'bugfix', 'feature', 'refactor', 'discovery', 'change']))
]).describe('Observation type(s) to filter by'),
format: z.enum(['index', 'full']).default('index').describe('Output format: "index" for titles/dates only (default, RECOMMENDED for initial search), "full" for complete details (use only after reviewing index results)'), format: z.enum(['index', 'full']).default('index').describe('Output format: "index" for titles/dates only (default, RECOMMENDED for initial search), "full" for complete details (use only after reviewing index results)'),
project: z.string().optional().describe('Filter by project name'), project: z.string().optional().describe('Filter by project name'),
dateRange: z.object({ dateStart: z.union([z.string(), z.number()]).optional().describe('Start date for filtering (ISO string or epoch timestamp)'),
start: z.union([z.string(), z.number()]).optional(), dateEnd: z.union([z.string(), z.number()]).optional().describe('End date for filtering (ISO string or epoch timestamp)'),
end: z.union([z.string(), z.number()]).optional()
}).optional().describe('Filter by date range'),
limit: z.number().min(1).max(100).default(20).describe('Maximum results. IMPORTANT: Start with 3-5 to avoid exceeding MCP token limits, even in index mode.'), limit: z.number().min(1).max(100).default(20).describe('Maximum results. IMPORTANT: Start with 3-5 to avoid exceeding MCP token limits, even in index mode.'),
offset: z.number().min(0).default(0).describe('Number of results to skip'), offset: z.number().min(0).default(0).describe('Number of results to skip'),
orderBy: z.enum(['relevance', 'date_desc', 'date_asc']).default('date_desc').describe('Sort order') orderBy: z.enum(['relevance', 'date_desc', 'date_asc']).default('date_desc').describe('Sort order')
}), }),
handler: async (args: any) => { handler: async (args: any) => {
try { try {
const { type, format = 'index', ...filters } = args; const normalized = normalizeParams(args);
const { type, format = 'index', ...filters } = normalized;
const typeStr = Array.isArray(type) ? type.join(', ') : type; const typeStr = Array.isArray(type) ? type.join(', ') : type;
let results: ObservationSearchResult[] = []; let results: ObservationSearchResult[] = [];
@@ -1905,17 +1929,16 @@ const tools = [
query: z.string().describe('Natural language search query for semantic ranking via ChromaDB vector search'), query: z.string().describe('Natural language search query for semantic ranking via ChromaDB vector search'),
format: z.enum(['index', 'full']).default('index').describe('Output format: "index" for truncated prompts/dates (default, RECOMMENDED for initial search), "full" for complete prompt text (use only after reviewing index results)'), format: z.enum(['index', 'full']).default('index').describe('Output format: "index" for truncated prompts/dates (default, RECOMMENDED for initial search), "full" for complete prompt text (use only after reviewing index results)'),
project: z.string().optional().describe('Filter by project name'), project: z.string().optional().describe('Filter by project name'),
dateRange: z.object({ dateStart: z.union([z.string(), z.number()]).optional().describe('Start date for filtering (ISO string or epoch timestamp)'),
start: z.union([z.string(), z.number()]).optional(), dateEnd: z.union([z.string(), z.number()]).optional().describe('End date for filtering (ISO string or epoch timestamp)'),
end: z.union([z.string(), z.number()]).optional()
}).optional().describe('Filter by date range'),
limit: z.number().min(1).max(100).default(20).describe('Maximum number of results'), limit: z.number().min(1).max(100).default(20).describe('Maximum number of results'),
offset: z.number().min(0).default(0).describe('Number of results to skip'), offset: z.number().min(0).default(0).describe('Number of results to skip'),
orderBy: z.enum(['relevance', 'date_desc', 'date_asc']).default('date_desc').describe('Sort order') orderBy: z.enum(['relevance', 'date_desc', 'date_asc']).default('date_desc').describe('Sort order')
}), }),
handler: async (args: any) => { handler: async (args: any) => {
try { try {
const { query, format = 'index', ...options } = args; const normalized = normalizeParams(args);
const { query, format = 'index', ...options } = normalized;
let results: UserPromptSearchResult[] = []; let results: UserPromptSearchResult[] = [];
// Vector-first search via ChromaDB // Vector-first search via ChromaDB