Fix project filter synchronization issues in viewer UI (#70)
* feat: Enhance session and summary handling - Update SQL query in SessionStore to exclude null or empty projects. - Add 'investigated' field to Summary interface for better tracking. - Modify App component to handle pagination more efficiently based on current filters. - Update SummaryCard to display the 'investigated' field if present. - Refactor usePagination hook to reset pagination state when filters change. - Adjust mergeAndDeduplicateByProject function to ensure it only merges unfiltered data. * refactor: address PR feedback - remove redundancies and fix dependency cycles Fixes based on PR #70 review feedback: Required: - Fixed useEffect dependency cycle in App.tsx (removed handleLoadMore from deps) Recommended: - Removed redundant filter detection from App.tsx (usePagination handles this) - Removed redundant stateRef update effect from usePagination.ts - Simplified mergeAndDeduplicateByProject by removing defensive validation - Removed unused imports (useRef, useEffect) All changes follow CLAUDE.md principles: DRY, fail-fast, no defensive programming. 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude <noreply@anthropic.com> * fix: Reset paginated data arrays when filter changes Critical fix for data mixing bug identified in PR review follow-up. Problem: When filter changes, usePagination correctly resets its offset to 0, but the paginated state arrays (observations, summaries, prompts) were NOT being reset. This caused data from different projects to mix together because setState appends to the existing array. Example: - User views Project A: [A1, A2, A3] - User switches to Project B - API fetches [B1, B2, B3] - setState does: [...prev, ...new] = [A1, A2, A3, B1, B2, B3] ❌ Solution: Added a separate useEffect that resets all three paginated arrays when currentFilter changes. This happens BEFORE handleLoadMore fetches new data, ensuring clean state for the new filter. Files changed: - src/ui/viewer/App.tsx: Added useEffect to reset arrays on filter change - plugin/ui/viewer-bundle.js: Built UI bundle Testing: 1. Select Project A, verify data loads 2. Switch to Project B 3. Verify ONLY Project B data is shown (no mixing) 4. Switch back to "All Projects" 5. Verify all data appears correctly 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude <noreply@anthropic.com> * refactor: Combine filter change useEffect hooks for guaranteed order Merged two separate useEffect hooks into one to: 1. Guarantee execution order (reset THEN load) 2. Reduce complexity (one hook instead of two) 3. Make intent clearer with single comment Before: - useEffect #1: handleLoadMore() on filter change - useEffect #2: Reset arrays on filter change - React could run these in any order After: - Single useEffect: Reset arrays THEN handleLoadMore() - Execution order is now guaranteed Files changed: - src/ui/viewer/App.tsx: Combined useEffect hooks - plugin/ui/viewer-bundle.js: Built UI bundle 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude <noreply@anthropic.com> --------- Co-authored-by: Claude <noreply@anthropic.com>
This commit is contained in:
@@ -1,5 +1,5 @@
|
||||
#!/usr/bin/env node
|
||||
import{Server as he}from"@modelcontextprotocol/sdk/server/index.js";import{StdioServerTransport as _e}from"@modelcontextprotocol/sdk/server/stdio.js";import{Client as fe}from"@modelcontextprotocol/sdk/client/index.js";import{StdioClientTransport as Ee}from"@modelcontextprotocol/sdk/client/stdio.js";import{CallToolRequestSchema as be,ListToolsRequestSchema as ge}from"@modelcontextprotocol/sdk/types.js";import{z as i}from"zod";import{zodToJsonSchema as Te}from"zod-to-json-schema";import{basename as Se}from"path";import pe from"better-sqlite3";import{join as L,dirname as ce,basename as xe}from"path";import{homedir as ee}from"os";import{existsSync as De,mkdirSync as de}from"fs";import{fileURLToPath as le}from"url";function ue(){return typeof __dirname<"u"?__dirname:ce(le(import.meta.url))}var $e=ue(),w=process.env.CLAUDE_MEM_DATA_DIR||L(ee(),".claude-mem"),V=process.env.CLAUDE_CONFIG_DIR||L(ee(),".claude"),ke=L(w,"archives"),Fe=L(w,"logs"),Me=L(w,"trash"),Ue=L(w,"backups"),je=L(w,"settings.json"),X=L(w,"claude-mem.db"),te=L(w,"vector-db"),Be=L(V,"settings.json"),Xe=L(V,"commands"),Pe=L(V,"CLAUDE.md");function P(c){de(c,{recursive:!0})}var G=class{db;constructor(e){e||(P(w),e=X),this.db=new pe(e),this.db.pragma("journal_mode = WAL"),this.ensureFTSTables()}ensureFTSTables(){try{if(this.db.prepare("SELECT name FROM sqlite_master WHERE type='table' AND name LIKE '%_fts'").all().some(r=>r.name==="observations_fts"||r.name==="session_summaries_fts"))return;console.error("[SessionSearch] Creating FTS5 tables..."),this.db.exec(`
|
||||
import{Server as he}from"@modelcontextprotocol/sdk/server/index.js";import{StdioServerTransport as _e}from"@modelcontextprotocol/sdk/server/stdio.js";import{Client as fe}from"@modelcontextprotocol/sdk/client/index.js";import{StdioClientTransport as Ee}from"@modelcontextprotocol/sdk/client/stdio.js";import{CallToolRequestSchema as be,ListToolsRequestSchema as ge}from"@modelcontextprotocol/sdk/types.js";import{z as i}from"zod";import{zodToJsonSchema as Te}from"zod-to-json-schema";import{basename as Se}from"path";import pe from"better-sqlite3";import{join as L,dirname as ce,basename as xe}from"path";import{homedir as ee}from"os";import{existsSync as De,mkdirSync as de}from"fs";import{fileURLToPath as le}from"url";function ue(){return typeof __dirname<"u"?__dirname:ce(le(import.meta.url))}var $e=ue(),w=process.env.CLAUDE_MEM_DATA_DIR||L(ee(),".claude-mem"),V=process.env.CLAUDE_CONFIG_DIR||L(ee(),".claude"),ke=L(w,"archives"),Fe=L(w,"logs"),Ue=L(w,"trash"),Me=L(w,"backups"),je=L(w,"settings.json"),X=L(w,"claude-mem.db"),te=L(w,"vector-db"),Be=L(V,"settings.json"),Xe=L(V,"commands"),Pe=L(V,"CLAUDE.md");function P(c){de(c,{recursive:!0})}var G=class{db;constructor(e){e||(P(w),e=X),this.db=new pe(e),this.db.pragma("journal_mode = WAL"),this.ensureFTSTables()}ensureFTSTables(){try{if(this.db.prepare("SELECT name FROM sqlite_master WHERE type='table' AND name LIKE '%_fts'").all().some(r=>r.name==="observations_fts"||r.name==="session_summaries_fts"))return;console.error("[SessionSearch] Creating FTS5 tables..."),this.db.exec(`
|
||||
CREATE VIRTUAL TABLE IF NOT EXISTS observations_fts USING fts5(
|
||||
title,
|
||||
subtitle,
|
||||
@@ -352,6 +352,7 @@ ${e.stack}`:e.message;if(Array.isArray(e))return`[${e.length} items]`;let s=Obje
|
||||
`).all(e)}getAllProjects(){return this.db.prepare(`
|
||||
SELECT DISTINCT project
|
||||
FROM sdk_sessions
|
||||
WHERE project IS NOT NULL AND project != ''
|
||||
ORDER BY project ASC
|
||||
`).all().map(r=>r.project)}getRecentSessionsWithStatus(e,s=3){return this.db.prepare(`
|
||||
SELECT * FROM (
|
||||
@@ -532,7 +533,7 @@ ${e.stack}`:e.message;if(Array.isArray(e))return`[${e.length} items]`;let s=Obje
|
||||
JOIN sdk_sessions s ON up.claude_session_id = s.claude_session_id
|
||||
WHERE up.created_at_epoch >= ? AND up.created_at_epoch <= ? ${o.replace("project","s.project")}
|
||||
ORDER BY up.created_at_epoch ASC
|
||||
`;try{let f=this.db.prepare(p).all(d,l,...a),h=this.db.prepare(u).all(d,l,...a),b=this.db.prepare(m).all(d,l,...a);return{observations:f,sessions:h.map(_=>({id:_.id,sdk_session_id:_.sdk_session_id,project:_.project,request:_.request,completed:_.completed,next_steps:_.next_steps,created_at:_.created_at,created_at_epoch:_.created_at_epoch})),prompts:b.map(_=>({id:_.id,claude_session_id:_.claude_session_id,project:_.project,prompt:_.prompt_text,created_at:_.created_at,created_at_epoch:_.created_at_epoch}))}}catch(f){return console.error("[SessionStore] Error querying timeline records:",f.message),{observations:[],sessions:[],prompts:[]}}}close(){this.db.close()}};var $,N,k=null,ye="cm__claude-mem";try{$=new G,N=new H}catch(c){console.error("[search-server] Failed to initialize search:",c.message),process.exit(1)}async function U(c,e,s){if(!k)throw new Error("Chroma client not initialized");let t=(await k.callTool({name:"chroma_query_documents",arguments:{collection_name:ye,query_texts:[c],n_results:e,include:["documents","metadatas","distances"],where:s}})).content[0]?.text||"",n;try{n=JSON.parse(t)}catch(p){return console.error("[search-server] Failed to parse Chroma response as JSON:",p),{ids:[],distances:[],metadatas:[]}}let o=[],a=n.ids?.[0]||[];for(let p of a){let u=p.match(/obs_(\d+)_/),m=p.match(/summary_(\d+)_/),f=p.match(/prompt_(\d+)/),h=null;u?h=parseInt(u[1],10):m?h=parseInt(m[1],10):f&&(h=parseInt(f[1],10)),h!==null&&!o.includes(h)&&o.push(h)}let d=n.distances?.[0]||[],l=n.metadatas?.[0]||[];return{ids:o,distances:d,metadatas:l}}function j(){return`
|
||||
`;try{let f=this.db.prepare(p).all(d,l,...a),h=this.db.prepare(u).all(d,l,...a),b=this.db.prepare(m).all(d,l,...a);return{observations:f,sessions:h.map(_=>({id:_.id,sdk_session_id:_.sdk_session_id,project:_.project,request:_.request,completed:_.completed,next_steps:_.next_steps,created_at:_.created_at,created_at_epoch:_.created_at_epoch})),prompts:b.map(_=>({id:_.id,claude_session_id:_.claude_session_id,project:_.project,prompt:_.prompt_text,created_at:_.created_at,created_at_epoch:_.created_at_epoch}))}}catch(f){return console.error("[SessionStore] Error querying timeline records:",f.message),{observations:[],sessions:[],prompts:[]}}}close(){this.db.close()}};var $,N,k=null,ye="cm__claude-mem";try{$=new G,N=new H}catch(c){console.error("[search-server] Failed to initialize search:",c.message),process.exit(1)}async function M(c,e,s){if(!k)throw new Error("Chroma client not initialized");let t=(await k.callTool({name:"chroma_query_documents",arguments:{collection_name:ye,query_texts:[c],n_results:e,include:["documents","metadatas","distances"],where:s}})).content[0]?.text||"",n;try{n=JSON.parse(t)}catch(p){return console.error("[search-server] Failed to parse Chroma response as JSON:",p),{ids:[],distances:[],metadatas:[]}}let o=[],a=n.ids?.[0]||[];for(let p of a){let u=p.match(/obs_(\d+)_/),m=p.match(/summary_(\d+)_/),f=p.match(/prompt_(\d+)/),h=null;u?h=parseInt(u[1],10):m?h=parseInt(m[1],10):f&&(h=parseInt(f[1],10)),h!==null&&!o.includes(h)&&o.push(h)}let d=n.distances?.[0]||[],l=n.metadatas?.[0]||[];return{ids:o,distances:d,metadatas:l}}function j(){return`
|
||||
---
|
||||
\u{1F4A1} Search Strategy:
|
||||
ALWAYS search with index format FIRST to get an overview and identify relevant results.
|
||||
@@ -556,7 +557,7 @@ Other tips:
|
||||
`)}function Re(c,e){let s=new Date(c.created_at_epoch).toLocaleString();return`${e+1}. "${c.prompt_text}"
|
||||
Date: ${s} | Prompt #${c.prompt_number}
|
||||
Source: claude-mem://user-prompt/${c.id}`}function ve(c){let e=[];e.push(`## User Prompt #${c.prompt_number}`),e.push(`*Source: claude-mem://user-prompt/${c.id}*`),e.push(""),e.push(c.prompt_text),e.push(""),e.push("---");let s=new Date(c.created_at_epoch).toLocaleString();return e.push(`Date: ${s}`),e.join(`
|
||||
`)}var Oe=i.object({project:i.string().optional().describe("Filter by project name"),type:i.union([i.enum(["decision","bugfix","feature","refactor","discovery","change"]),i.array(i.enum(["decision","bugfix","feature","refactor","discovery","change"]))]).optional().describe("Filter by observation type"),concepts:i.union([i.string(),i.array(i.string())]).optional().describe("Filter by concept tags"),files:i.union([i.string(),i.array(i.string())]).optional().describe("Filter by file paths (partial match)"),dateRange:i.object({start:i.union([i.string(),i.number()]).optional().describe("Start date (ISO string or epoch)"),end:i.union([i.string(),i.number()]).optional().describe("End date (ISO string or epoch)")}).optional().describe("Filter by date range"),limit:i.number().min(1).max(100).default(20).describe("Maximum number of results"),offset:i.number().min(0).default(0).describe("Number of results to skip"),orderBy:i.enum(["relevance","date_desc","date_asc"]).default("date_desc").describe("Sort order")}),oe=[{name:"search_observations",description:'Search observations using full-text search across titles, narratives, facts, and concepts. 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:i.object({query:i.string().describe("Search query for FTS5 full-text search"),format:i.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)'),...Oe.shape}),handler:async c=>{try{let{query:e,format:s="index",...r}=c,t=[];if(k)try{console.error("[search-server] Using hybrid semantic search (Chroma + SQLite)");let o=await U(e,100);if(console.error(`[search-server] Chroma returned ${o.ids.length} semantic matches`),o.ids.length>0){let a=Date.now()-7776e6,d=o.ids.filter((l,p)=>{let u=o.metadatas[p];return u&&u.created_at_epoch>a});if(console.error(`[search-server] ${d.length} results within 90-day window`),d.length>0){let l=r.limit||20;t=N.getObservationsByIds(d,{orderBy:"date_desc",limit:l}),console.error(`[search-server] Hydrated ${t.length} observations from SQLite`)}}}catch(o){console.error("[search-server] Chroma query failed, falling back to FTS5:",o.message)}if(t.length===0&&(console.error("[search-server] Using FTS5 keyword search"),t=$.searchObservations(e,r)),t.length===0)return{content:[{type:"text",text:`No observations found matching "${e}"`}]};let n;if(s==="index"){let o=`Found ${t.length} observation(s) matching "${e}":
|
||||
`)}var Oe=i.object({project:i.string().optional().describe("Filter by project name"),type:i.union([i.enum(["decision","bugfix","feature","refactor","discovery","change"]),i.array(i.enum(["decision","bugfix","feature","refactor","discovery","change"]))]).optional().describe("Filter by observation type"),concepts:i.union([i.string(),i.array(i.string())]).optional().describe("Filter by concept tags"),files:i.union([i.string(),i.array(i.string())]).optional().describe("Filter by file paths (partial match)"),dateRange:i.object({start:i.union([i.string(),i.number()]).optional().describe("Start date (ISO string or epoch)"),end:i.union([i.string(),i.number()]).optional().describe("End date (ISO string or epoch)")}).optional().describe("Filter by date range"),limit:i.number().min(1).max(100).default(20).describe("Maximum number of results"),offset:i.number().min(0).default(0).describe("Number of results to skip"),orderBy:i.enum(["relevance","date_desc","date_asc"]).default("date_desc").describe("Sort order")}),oe=[{name:"search_observations",description:'Search observations using full-text search across titles, narratives, facts, and concepts. 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:i.object({query:i.string().describe("Search query for FTS5 full-text search"),format:i.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)'),...Oe.shape}),handler:async c=>{try{let{query:e,format:s="index",...r}=c,t=[];if(k)try{console.error("[search-server] Using hybrid semantic search (Chroma + SQLite)");let o=await M(e,100);if(console.error(`[search-server] Chroma returned ${o.ids.length} semantic matches`),o.ids.length>0){let a=Date.now()-7776e6,d=o.ids.filter((l,p)=>{let u=o.metadatas[p];return u&&u.created_at_epoch>a});if(console.error(`[search-server] ${d.length} results within 90-day window`),d.length>0){let l=r.limit||20;t=N.getObservationsByIds(d,{orderBy:"date_desc",limit:l}),console.error(`[search-server] Hydrated ${t.length} observations from SQLite`)}}}catch(o){console.error("[search-server] Chroma query failed, falling back to FTS5:",o.message)}if(t.length===0&&(console.error("[search-server] Using FTS5 keyword search"),t=$.searchObservations(e,r)),t.length===0)return{content:[{type:"text",text:`No observations found matching "${e}"`}]};let n;if(s==="index"){let o=`Found ${t.length} observation(s) matching "${e}":
|
||||
|
||||
`,a=t.map((d,l)=>q(d,l));n=o+a.join(`
|
||||
|
||||
@@ -564,7 +565,7 @@ Other tips:
|
||||
|
||||
---
|
||||
|
||||
`);return{content:[{type:"text",text:n}]}}catch(e){return{content:[{type:"text",text:`Search failed: ${e.message}`}],isError:!0}}}},{name:"search_sessions",description:'Search session summaries using full-text search across requests, completions, learnings, and notes. 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:i.object({query:i.string().describe("Search query for FTS5 full-text search"),format:i.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:i.string().optional().describe("Filter by project name"),dateRange:i.object({start:i.union([i.string(),i.number()]).optional(),end:i.union([i.string(),i.number()]).optional()}).optional().describe("Filter by date range"),limit:i.number().min(1).max(100).default(20).describe("Maximum number of results"),offset:i.number().min(0).default(0).describe("Number of results to skip"),orderBy:i.enum(["relevance","date_desc","date_asc"]).default("date_desc").describe("Sort order")}),handler:async c=>{try{let{query:e,format:s="index",...r}=c,t=[];if(k)try{console.error("[search-server] Using hybrid semantic search for sessions");let o=await U(e,100,{doc_type:"session_summary"});if(console.error(`[search-server] Chroma returned ${o.ids.length} semantic matches`),o.ids.length>0){let a=Date.now()-7776e6,d=o.ids.filter((l,p)=>{let u=o.metadatas[p];return u&&u.created_at_epoch>a});if(console.error(`[search-server] ${d.length} results within 90-day window`),d.length>0){let l=r.limit||20;t=N.getSessionSummariesByIds(d,{orderBy:"date_desc",limit:l}),console.error(`[search-server] Hydrated ${t.length} sessions from SQLite`)}}}catch(o){console.error("[search-server] Chroma query failed, falling back to FTS5:",o.message)}if(t.length===0&&(console.error("[search-server] Using FTS5 keyword search"),t=$.searchSessions(e,r)),t.length===0)return{content:[{type:"text",text:`No sessions found matching "${e}"`}]};let n;if(s==="index"){let o=`Found ${t.length} session(s) matching "${e}":
|
||||
`);return{content:[{type:"text",text:n}]}}catch(e){return{content:[{type:"text",text:`Search failed: ${e.message}`}],isError:!0}}}},{name:"search_sessions",description:'Search session summaries using full-text search across requests, completions, learnings, and notes. 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:i.object({query:i.string().describe("Search query for FTS5 full-text search"),format:i.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:i.string().optional().describe("Filter by project name"),dateRange:i.object({start:i.union([i.string(),i.number()]).optional(),end:i.union([i.string(),i.number()]).optional()}).optional().describe("Filter by date range"),limit:i.number().min(1).max(100).default(20).describe("Maximum number of results"),offset:i.number().min(0).default(0).describe("Number of results to skip"),orderBy:i.enum(["relevance","date_desc","date_asc"]).default("date_desc").describe("Sort order")}),handler:async c=>{try{let{query:e,format:s="index",...r}=c,t=[];if(k)try{console.error("[search-server] Using hybrid semantic search for sessions");let o=await M(e,100,{doc_type:"session_summary"});if(console.error(`[search-server] Chroma returned ${o.ids.length} semantic matches`),o.ids.length>0){let a=Date.now()-7776e6,d=o.ids.filter((l,p)=>{let u=o.metadatas[p];return u&&u.created_at_epoch>a});if(console.error(`[search-server] ${d.length} results within 90-day window`),d.length>0){let l=r.limit||20;t=N.getSessionSummariesByIds(d,{orderBy:"date_desc",limit:l}),console.error(`[search-server] Hydrated ${t.length} sessions from SQLite`)}}}catch(o){console.error("[search-server] Chroma query failed, falling back to FTS5:",o.message)}if(t.length===0&&(console.error("[search-server] Using FTS5 keyword search"),t=$.searchSessions(e,r)),t.length===0)return{content:[{type:"text",text:`No sessions found matching "${e}"`}]};let n;if(s==="index"){let o=`Found ${t.length} session(s) matching "${e}":
|
||||
|
||||
`,a=t.map((d,l)=>re(d,l));n=o+a.join(`
|
||||
|
||||
@@ -572,7 +573,7 @@ Other tips:
|
||||
|
||||
---
|
||||
|
||||
`);return{content:[{type:"text",text:n}]}}catch(e){return{content:[{type:"text",text:`Search failed: ${e.message}`}],isError:!0}}}},{name:"find_by_concept",description:'Find observations tagged with a specific concept. Available concepts: "discovery", "problem-solution", "what-changed", "how-it-works", "pattern", "gotcha", "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:i.object({concept:i.string().describe("Concept tag to search for. Available: discovery, problem-solution, what-changed, how-it-works, pattern, gotcha, change"),format:i.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:i.string().optional().describe("Filter by project name"),dateRange:i.object({start:i.union([i.string(),i.number()]).optional(),end:i.union([i.string(),i.number()]).optional()}).optional().describe("Filter by date range"),limit:i.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:i.number().min(0).default(0).describe("Number of results to skip"),orderBy:i.enum(["relevance","date_desc","date_asc"]).default("date_desc").describe("Sort order")}),handler:async c=>{try{let{concept:e,format:s="index",...r}=c,t=[];if(k)try{console.error("[search-server] Using metadata-first + semantic ranking for concept search");let o=$.findByConcept(e,r);if(console.error(`[search-server] Found ${o.length} observations with concept "${e}"`),o.length>0){let a=o.map(p=>p.id),d=await U(e,Math.min(a.length,100)),l=[];for(let p of d.ids)a.includes(p)&&!l.includes(p)&&l.push(p);console.error(`[search-server] Chroma ranked ${l.length} results by semantic relevance`),l.length>0&&(t=N.getObservationsByIds(l,{limit:r.limit||20}),t.sort((p,u)=>l.indexOf(p.id)-l.indexOf(u.id)))}}catch(o){console.error("[search-server] Chroma ranking failed, using SQLite order:",o.message)}if(t.length===0&&(console.error("[search-server] Using SQLite-only concept search"),t=$.findByConcept(e,r)),t.length===0)return{content:[{type:"text",text:`No observations found with concept "${e}"`}]};let n;if(s==="index"){let o=`Found ${t.length} observation(s) with concept "${e}":
|
||||
`);return{content:[{type:"text",text:n}]}}catch(e){return{content:[{type:"text",text:`Search failed: ${e.message}`}],isError:!0}}}},{name:"find_by_concept",description:'Find observations tagged with a specific concept. Available concepts: "discovery", "problem-solution", "what-changed", "how-it-works", "pattern", "gotcha", "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:i.object({concept:i.string().describe("Concept tag to search for. Available: discovery, problem-solution, what-changed, how-it-works, pattern, gotcha, change"),format:i.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:i.string().optional().describe("Filter by project name"),dateRange:i.object({start:i.union([i.string(),i.number()]).optional(),end:i.union([i.string(),i.number()]).optional()}).optional().describe("Filter by date range"),limit:i.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:i.number().min(0).default(0).describe("Number of results to skip"),orderBy:i.enum(["relevance","date_desc","date_asc"]).default("date_desc").describe("Sort order")}),handler:async c=>{try{let{concept:e,format:s="index",...r}=c,t=[];if(k)try{console.error("[search-server] Using metadata-first + semantic ranking for concept search");let o=$.findByConcept(e,r);if(console.error(`[search-server] Found ${o.length} observations with concept "${e}"`),o.length>0){let a=o.map(p=>p.id),d=await M(e,Math.min(a.length,100)),l=[];for(let p of d.ids)a.includes(p)&&!l.includes(p)&&l.push(p);console.error(`[search-server] Chroma ranked ${l.length} results by semantic relevance`),l.length>0&&(t=N.getObservationsByIds(l,{limit:r.limit||20}),t.sort((p,u)=>l.indexOf(p.id)-l.indexOf(u.id)))}}catch(o){console.error("[search-server] Chroma ranking failed, using SQLite order:",o.message)}if(t.length===0&&(console.error("[search-server] Using SQLite-only concept search"),t=$.findByConcept(e,r)),t.length===0)return{content:[{type:"text",text:`No observations found with concept "${e}"`}]};let n;if(s==="index"){let o=`Found ${t.length} observation(s) with concept "${e}":
|
||||
|
||||
`,a=t.map((d,l)=>q(d,l));n=o+a.join(`
|
||||
|
||||
@@ -580,7 +581,7 @@ Other tips:
|
||||
|
||||
---
|
||||
|
||||
`);return{content:[{type:"text",text:n}]}}catch(e){return{content:[{type:"text",text:`Search failed: ${e.message}`}],isError:!0}}}},{name:"find_by_file",description:'Find observations and sessions that reference a specific file path. 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:i.object({filePath:i.string().describe("File path to search for (supports partial matching)"),format:i.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:i.string().optional().describe("Filter by project name"),dateRange:i.object({start:i.union([i.string(),i.number()]).optional(),end:i.union([i.string(),i.number()]).optional()}).optional().describe("Filter by date range"),limit:i.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:i.number().min(0).default(0).describe("Number of results to skip"),orderBy:i.enum(["relevance","date_desc","date_asc"]).default("date_desc").describe("Sort order")}),handler:async c=>{try{let{filePath:e,format:s="index",...r}=c,t=[],n=[];if(k)try{console.error("[search-server] Using metadata-first + semantic ranking for file search");let d=$.findByFile(e,r);if(console.error(`[search-server] Found ${d.observations.length} observations, ${d.sessions.length} sessions for file "${e}"`),n=d.sessions,d.observations.length>0){let l=d.observations.map(m=>m.id),p=await U(e,Math.min(l.length,100)),u=[];for(let m of p.ids)l.includes(m)&&!u.includes(m)&&u.push(m);console.error(`[search-server] Chroma ranked ${u.length} observations by semantic relevance`),u.length>0&&(t=N.getObservationsByIds(u,{limit:r.limit||20}),t.sort((m,f)=>u.indexOf(m.id)-u.indexOf(f.id)))}}catch(d){console.error("[search-server] Chroma ranking failed, using SQLite order:",d.message)}if(t.length===0&&n.length===0){console.error("[search-server] Using SQLite-only file search");let d=$.findByFile(e,r);t=d.observations,n=d.sessions}let o=t.length+n.length;if(o===0)return{content:[{type:"text",text:`No results found for file "${e}"`}]};let a;if(s==="index"){let d=`Found ${o} result(s) for file "${e}":
|
||||
`);return{content:[{type:"text",text:n}]}}catch(e){return{content:[{type:"text",text:`Search failed: ${e.message}`}],isError:!0}}}},{name:"find_by_file",description:'Find observations and sessions that reference a specific file path. 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:i.object({filePath:i.string().describe("File path to search for (supports partial matching)"),format:i.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:i.string().optional().describe("Filter by project name"),dateRange:i.object({start:i.union([i.string(),i.number()]).optional(),end:i.union([i.string(),i.number()]).optional()}).optional().describe("Filter by date range"),limit:i.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:i.number().min(0).default(0).describe("Number of results to skip"),orderBy:i.enum(["relevance","date_desc","date_asc"]).default("date_desc").describe("Sort order")}),handler:async c=>{try{let{filePath:e,format:s="index",...r}=c,t=[],n=[];if(k)try{console.error("[search-server] Using metadata-first + semantic ranking for file search");let d=$.findByFile(e,r);if(console.error(`[search-server] Found ${d.observations.length} observations, ${d.sessions.length} sessions for file "${e}"`),n=d.sessions,d.observations.length>0){let l=d.observations.map(m=>m.id),p=await M(e,Math.min(l.length,100)),u=[];for(let m of p.ids)l.includes(m)&&!u.includes(m)&&u.push(m);console.error(`[search-server] Chroma ranked ${u.length} observations by semantic relevance`),u.length>0&&(t=N.getObservationsByIds(u,{limit:r.limit||20}),t.sort((m,f)=>u.indexOf(m.id)-u.indexOf(f.id)))}}catch(d){console.error("[search-server] Chroma ranking failed, using SQLite order:",d.message)}if(t.length===0&&n.length===0){console.error("[search-server] Using SQLite-only file search");let d=$.findByFile(e,r);t=d.observations,n=d.sessions}let o=t.length+n.length;if(o===0)return{content:[{type:"text",text:`No results found for file "${e}"`}]};let a;if(s==="index"){let d=`Found ${o} result(s) for file "${e}":
|
||||
|
||||
`,l=[];t.forEach((p,u)=>{l.push(q(p,u))}),n.forEach((p,u)=>{l.push(re(p,u+t.length))}),a=d+l.join(`
|
||||
|
||||
@@ -588,7 +589,7 @@ Other tips:
|
||||
|
||||
---
|
||||
|
||||
`)}return{content:[{type:"text",text:a}]}}catch(e){return{content:[{type:"text",text:`Search failed: ${e.message}`}],isError:!0}}}},{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.',inputSchema:i.object({type:i.union([i.enum(["decision","bugfix","feature","refactor","discovery","change"]),i.array(i.enum(["decision","bugfix","feature","refactor","discovery","change"]))]).describe("Observation type(s) to filter by"),format:i.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:i.string().optional().describe("Filter by project name"),dateRange:i.object({start:i.union([i.string(),i.number()]).optional(),end:i.union([i.string(),i.number()]).optional()}).optional().describe("Filter by date range"),limit:i.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:i.number().min(0).default(0).describe("Number of results to skip"),orderBy:i.enum(["relevance","date_desc","date_asc"]).default("date_desc").describe("Sort order")}),handler:async c=>{try{let{type:e,format:s="index",...r}=c,t=Array.isArray(e)?e.join(", "):e,n=[];if(k)try{console.error("[search-server] Using metadata-first + semantic ranking for type search");let a=$.findByType(e,r);if(console.error(`[search-server] Found ${a.length} observations with type "${t}"`),a.length>0){let d=a.map(u=>u.id),l=await U(t,Math.min(d.length,100)),p=[];for(let u of l.ids)d.includes(u)&&!p.includes(u)&&p.push(u);console.error(`[search-server] Chroma ranked ${p.length} results by semantic relevance`),p.length>0&&(n=N.getObservationsByIds(p,{limit:r.limit||20}),n.sort((u,m)=>p.indexOf(u.id)-p.indexOf(m.id)))}}catch(a){console.error("[search-server] Chroma ranking failed, using SQLite order:",a.message)}if(n.length===0&&(console.error("[search-server] Using SQLite-only type search"),n=$.findByType(e,r)),n.length===0)return{content:[{type:"text",text:`No observations found with type "${t}"`}]};let o;if(s==="index"){let a=`Found ${n.length} observation(s) with type "${t}":
|
||||
`)}return{content:[{type:"text",text:a}]}}catch(e){return{content:[{type:"text",text:`Search failed: ${e.message}`}],isError:!0}}}},{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.',inputSchema:i.object({type:i.union([i.enum(["decision","bugfix","feature","refactor","discovery","change"]),i.array(i.enum(["decision","bugfix","feature","refactor","discovery","change"]))]).describe("Observation type(s) to filter by"),format:i.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:i.string().optional().describe("Filter by project name"),dateRange:i.object({start:i.union([i.string(),i.number()]).optional(),end:i.union([i.string(),i.number()]).optional()}).optional().describe("Filter by date range"),limit:i.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:i.number().min(0).default(0).describe("Number of results to skip"),orderBy:i.enum(["relevance","date_desc","date_asc"]).default("date_desc").describe("Sort order")}),handler:async c=>{try{let{type:e,format:s="index",...r}=c,t=Array.isArray(e)?e.join(", "):e,n=[];if(k)try{console.error("[search-server] Using metadata-first + semantic ranking for type search");let a=$.findByType(e,r);if(console.error(`[search-server] Found ${a.length} observations with type "${t}"`),a.length>0){let d=a.map(u=>u.id),l=await M(t,Math.min(d.length,100)),p=[];for(let u of l.ids)d.includes(u)&&!p.includes(u)&&p.push(u);console.error(`[search-server] Chroma ranked ${p.length} results by semantic relevance`),p.length>0&&(n=N.getObservationsByIds(p,{limit:r.limit||20}),n.sort((u,m)=>p.indexOf(u.id)-p.indexOf(m.id)))}}catch(a){console.error("[search-server] Chroma ranking failed, using SQLite order:",a.message)}if(n.length===0&&(console.error("[search-server] Using SQLite-only type search"),n=$.findByType(e,r)),n.length===0)return{content:[{type:"text",text:`No observations found with type "${t}"`}]};let o;if(s==="index"){let a=`Found ${n.length} observation(s) with type "${t}":
|
||||
|
||||
`,d=n.map((l,p)=>q(l,p));o=a+d.join(`
|
||||
|
||||
@@ -599,7 +600,7 @@ Other tips:
|
||||
`);return{content:[{type:"text",text:o}]}}catch(e){return{content:[{type:"text",text:`Search failed: ${e.message}`}],isError:!0}}}},{name:"get_recent_context",description:"Get recent session context including summaries and observations for a project",inputSchema:i.object({project:i.string().optional().describe("Project name (defaults to current working directory basename)"),limit:i.number().min(1).max(10).default(3).describe("Number of recent sessions to retrieve")}),handler:async c=>{try{let e=c.project||Se(process.cwd()),s=c.limit||3,r=N.getRecentSessionsWithStatus(e,s);if(r.length===0)return{content:[{type:"text",text:`# Recent Session Context
|
||||
|
||||
No previous sessions found for project "${e}".`}]};let t=[];t.push("# Recent Session Context"),t.push(""),t.push(`Showing last ${r.length} session(s) for **${e}**:`),t.push("");for(let n of r)if(n.sdk_session_id){if(t.push("---"),t.push(""),n.has_summary){let o=N.getSummaryForSession(n.sdk_session_id);if(o){let a=o.prompt_number?` (Prompt #${o.prompt_number})`:"";if(t.push(`**Summary${a}**`),t.push(""),o.request&&t.push(`**Request:** ${o.request}`),o.completed&&t.push(`**Completed:** ${o.completed}`),o.learned&&t.push(`**Learned:** ${o.learned}`),o.next_steps&&t.push(`**Next Steps:** ${o.next_steps}`),o.files_read)try{let l=JSON.parse(o.files_read);Array.isArray(l)&&l.length>0&&t.push(`**Files Read:** ${l.join(", ")}`)}catch{o.files_read.trim()&&t.push(`**Files Read:** ${o.files_read}`)}if(o.files_edited)try{let l=JSON.parse(o.files_edited);Array.isArray(l)&&l.length>0&&t.push(`**Files Edited:** ${l.join(", ")}`)}catch{o.files_edited.trim()&&t.push(`**Files Edited:** ${o.files_edited}`)}let d=new Date(o.created_at).toLocaleString();t.push(`**Date:** ${d}`)}}else if(n.status==="active"){t.push("**In Progress**"),t.push(""),n.user_prompt&&t.push(`**Request:** ${n.user_prompt}`);let o=N.getObservationsForSession(n.sdk_session_id);if(o.length>0){t.push(""),t.push(`**Observations (${o.length}):**`);for(let d of o)t.push(`- ${d.title}`)}else t.push(""),t.push("*No observations yet*");t.push(""),t.push("**Status:** Active - summary pending");let a=new Date(n.started_at).toLocaleString();t.push(`**Date:** ${a}`)}else{t.push(`**${n.status.charAt(0).toUpperCase()+n.status.slice(1)}**`),t.push(""),n.user_prompt&&t.push(`**Request:** ${n.user_prompt}`),t.push(""),t.push(`**Status:** ${n.status} - no summary available`);let o=new Date(n.started_at).toLocaleString();t.push(`**Date:** ${o}`)}t.push("")}return{content:[{type:"text",text:t.join(`
|
||||
`)}]}}catch(e){return{content:[{type:"text",text:`Failed to get recent context: ${e.message}`}],isError:!0}}}},{name:"search_user_prompts",description:'Search raw user prompts with full-text search. Use this to find what the user actually said/requested across all sessions. 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:i.object({query:i.string().describe("Search query for FTS5 full-text search"),format:i.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:i.string().optional().describe("Filter by project name"),dateRange:i.object({start:i.union([i.string(),i.number()]).optional(),end:i.union([i.string(),i.number()]).optional()}).optional().describe("Filter by date range"),limit:i.number().min(1).max(100).default(20).describe("Maximum number of results"),offset:i.number().min(0).default(0).describe("Number of results to skip"),orderBy:i.enum(["relevance","date_desc","date_asc"]).default("date_desc").describe("Sort order")}),handler:async c=>{try{let{query:e,format:s="index",...r}=c,t=[];if(k)try{console.error("[search-server] Using hybrid semantic search for user prompts");let o=await U(e,100,{doc_type:"user_prompt"});if(console.error(`[search-server] Chroma returned ${o.ids.length} semantic matches`),o.ids.length>0){let a=Date.now()-7776e6,d=o.ids.filter((l,p)=>{let u=o.metadatas[p];return u&&u.created_at_epoch>a});if(console.error(`[search-server] ${d.length} results within 90-day window`),d.length>0){let l=r.limit||20;t=N.getUserPromptsByIds(d,{orderBy:"date_desc",limit:l}),console.error(`[search-server] Hydrated ${t.length} user prompts from SQLite`)}}}catch(o){console.error("[search-server] Chroma query failed, falling back to FTS5:",o.message)}if(t.length===0&&(console.error("[search-server] Using FTS5 keyword search"),t=$.searchUserPrompts(e,r)),t.length===0)return{content:[{type:"text",text:`No user prompts found matching "${e}"`}]};let n;if(s==="index"){let o=`Found ${t.length} user prompt(s) matching "${e}":
|
||||
`)}]}}catch(e){return{content:[{type:"text",text:`Failed to get recent context: ${e.message}`}],isError:!0}}}},{name:"search_user_prompts",description:'Search raw user prompts with full-text search. Use this to find what the user actually said/requested across all sessions. 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:i.object({query:i.string().describe("Search query for FTS5 full-text search"),format:i.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:i.string().optional().describe("Filter by project name"),dateRange:i.object({start:i.union([i.string(),i.number()]).optional(),end:i.union([i.string(),i.number()]).optional()}).optional().describe("Filter by date range"),limit:i.number().min(1).max(100).default(20).describe("Maximum number of results"),offset:i.number().min(0).default(0).describe("Number of results to skip"),orderBy:i.enum(["relevance","date_desc","date_asc"]).default("date_desc").describe("Sort order")}),handler:async c=>{try{let{query:e,format:s="index",...r}=c,t=[];if(k)try{console.error("[search-server] Using hybrid semantic search for user prompts");let o=await M(e,100,{doc_type:"user_prompt"});if(console.error(`[search-server] Chroma returned ${o.ids.length} semantic matches`),o.ids.length>0){let a=Date.now()-7776e6,d=o.ids.filter((l,p)=>{let u=o.metadatas[p];return u&&u.created_at_epoch>a});if(console.error(`[search-server] ${d.length} results within 90-day window`),d.length>0){let l=r.limit||20;t=N.getUserPromptsByIds(d,{orderBy:"date_desc",limit:l}),console.error(`[search-server] Hydrated ${t.length} user prompts from SQLite`)}}}catch(o){console.error("[search-server] Chroma query failed, falling back to FTS5:",o.message)}if(t.length===0&&(console.error("[search-server] Using FTS5 keyword search"),t=$.searchUserPrompts(e,r)),t.length===0)return{content:[{type:"text",text:`No user prompts found matching "${e}"`}]};let n;if(s==="index"){let o=`Found ${t.length} user prompt(s) matching "${e}":
|
||||
|
||||
`,a=t.map((d,l)=>Re(d,l));n=o+a.join(`
|
||||
|
||||
@@ -607,7 +608,7 @@ No previous sessions found for project "${e}".`}]};let t=[];t.push("# Recent Ses
|
||||
|
||||
---
|
||||
|
||||
`);return{content:[{type:"text",text:n}]}}catch(e){return{content:[{type:"text",text:`Search failed: ${e.message}`}],isError:!0}}}},{name:"get_context_timeline",description:'Get a unified timeline of context (observations, sessions, and prompts) around a specific point in time. All record types are interleaved chronologically. Useful for understanding "what was happening when X occurred". Returns depth_before records before anchor + anchor + depth_after records after (total: depth_before + 1 + depth_after mixed records).',inputSchema:i.object({anchor:i.union([i.number().describe("Observation ID to center timeline around"),i.string().describe("Session ID (format: S123) or ISO timestamp to center timeline around")]).describe('Anchor point: observation ID, session ID (e.g., "S123"), or ISO timestamp'),depth_before:i.number().min(0).max(50).default(10).describe("Number of records to retrieve before anchor, not including anchor (default: 10)"),depth_after:i.number().min(0).max(50).default(10).describe("Number of records to retrieve after anchor, not including anchor (default: 10)"),project:i.string().optional().describe("Filter by project name")}),handler:async c=>{try{let f=function(g){return new Date(g).toLocaleString("en-US",{month:"short",day:"numeric",year:"numeric"})},h=function(g){return new Date(g).toLocaleString("en-US",{hour:"numeric",minute:"2-digit",hour12:!0})},b=function(g){return new Date(g).toLocaleString("en-US",{month:"short",day:"numeric",hour:"numeric",minute:"2-digit",hour12:!0})},_=function(g){return g?Math.ceil(g.length/4):0};var e=f,s=h,r=b,t=_;let{anchor:n,depth_before:o=10,depth_after:a=10,project:d}=c,l,p=n,u;if(typeof n=="number"){let g=N.getObservationById(n);if(!g)return{content:[{type:"text",text:`Observation #${n} not found`}],isError:!0};l=g.created_at_epoch,u=N.getTimelineAroundObservation(n,l,o,a,d)}else if(typeof n=="string")if(n.startsWith("S")||n.startsWith("#S")){let g=n.replace(/^#?S/,""),I=parseInt(g,10),S=N.getSessionSummariesByIds([I]);if(S.length===0)return{content:[{type:"text",text:`Session #${I} not found`}],isError:!0};l=S[0].created_at_epoch,p=`S${I}`,u=N.getTimelineAroundTimestamp(l,o,a,d)}else{let g=new Date(n);if(isNaN(g.getTime()))return{content:[{type:"text",text:`Invalid timestamp: ${n}`}],isError:!0};l=g.getTime(),u=N.getTimelineAroundTimestamp(l,o,a,d)}else return{content:[{type:"text",text:'Invalid anchor: must be observation ID (number), session ID (e.g., "S123"), or ISO timestamp'}],isError:!0};let m=[...u.observations.map(g=>({type:"observation",data:g,epoch:g.created_at_epoch})),...u.sessions.map(g=>({type:"session",data:g,epoch:g.created_at_epoch})),...u.prompts.map(g=>({type:"prompt",data:g,epoch:g.created_at_epoch}))];if(m.sort((g,I)=>g.epoch-I.epoch),m.length===0)return{content:[{type:"text",text:`No context found around ${new Date(l).toLocaleString()} (${o} records before, ${a} records after)`}]};let E=[];E.push(`# Timeline around anchor: ${p}`),E.push(`**Window:** ${o} records before \u2192 ${a} records after | **Items:** ${m.length} (${u.observations.length} obs, ${u.sessions.length} sessions, ${u.prompts.length} prompts)`),E.push(""),E.push("**Legend:** \u{1F3AF} session-request | \u{1F534} bugfix | \u{1F7E3} feature | \u{1F504} refactor | \u2705 change | \u{1F535} discovery | \u{1F9E0} decision"),E.push("");let x=new Map;for(let g of m){let I=f(g.epoch);x.has(I)||x.set(I,[]),x.get(I).push(g)}let T=Array.from(x.entries()).sort((g,I)=>{let S=new Date(g[0]).getTime(),O=new Date(I[0]).getTime();return S-O});for(let[g,I]of T){E.push(`### ${g}`),E.push("");let S=null,O="",C=!1;for(let v of I){let F=typeof p=="number"&&v.type==="observation"&&v.data.id===p||typeof p=="string"&&p.startsWith("S")&&v.type==="session"&&`S${v.data.id}`===p;if(v.type==="session"){C&&(E.push(""),C=!1,S=null,O="");let y=v.data,M=y.request||"Session summary",R=`claude-mem://session-summary/${y.id}`,A=F?" \u2190 **ANCHOR**":"";E.push(`**\u{1F3AF} #S${y.id}** ${M} (${b(v.epoch)}) [\u2192](${R})${A}`),E.push("")}else if(v.type==="prompt"){C&&(E.push(""),C=!1,S=null,O="");let y=v.data,M=y.prompt.length>100?y.prompt.substring(0,100)+"...":y.prompt;E.push(`**\u{1F4AC} User Prompt #${y.prompt_number}** (${b(v.epoch)})`),E.push(`> ${M}`),E.push("")}else if(v.type==="observation"){let y=v.data,M="General";M!==S&&(C&&E.push(""),E.push(`**${M}**`),E.push("| ID | Time | T | Title | Tokens |"),E.push("|----|------|---|-------|--------|"),S=M,C=!0,O="");let R="\u2022";switch(y.type){case"bugfix":R="\u{1F534}";break;case"feature":R="\u{1F7E3}";break;case"refactor":R="\u{1F504}";break;case"change":R="\u2705";break;case"discovery":R="\u{1F535}";break;case"decision":R="\u{1F9E0}";break}let A=h(v.epoch),D=y.title||"Untitled",B=_(y.narrative),Y=A!==O?A:"\u2033";O=A;let Z=F?" \u2190 **ANCHOR**":"";E.push(`| #${y.id} | ${Y} | ${R} | ${D}${Z} | ~${B} |`)}}C&&E.push("")}return{content:[{type:"text",text:E.join(`
|
||||
`)}]}}catch(n){return{content:[{type:"text",text:`Timeline query failed: ${n.message}`}],isError:!0}}}},{name:"get_timeline_by_query",description:'Search for observations using natural language and get timeline context around the best match. Two modes: "auto" (default) automatically uses top result as timeline anchor; "interactive" returns top matches for you to choose from. This combines search + timeline into a single operation for faster context discovery.',inputSchema:i.object({query:i.string().describe("Natural language search query to find relevant observations"),mode:i.enum(["auto","interactive"]).default("auto").describe("auto: Automatically use top search result as timeline anchor. interactive: Show top N search results for manual anchor selection."),depth_before:i.number().min(0).max(50).default(10).describe("Number of timeline records before anchor (default: 10)"),depth_after:i.number().min(0).max(50).default(10).describe("Number of timeline records after anchor (default: 10)"),limit:i.number().min(1).max(20).default(5).describe("For interactive mode: number of top search results to display (default: 5)"),project:i.string().optional().describe("Filter by project name")}),handler:async c=>{try{let{query:n,mode:o="auto",depth_before:a=10,depth_after:d=10,limit:l=5,project:p}=c,u=[];if(k)try{console.error("[search-server] Using hybrid semantic search for timeline query");let m=await U(n,100);if(console.error(`[search-server] Chroma returned ${m.ids.length} semantic matches`),m.ids.length>0){let f=Date.now()-7776e6,h=m.ids.filter((b,_)=>{let E=m.metadatas[_];return E&&E.created_at_epoch>f});console.error(`[search-server] ${h.length} results within 90-day window`),h.length>0&&(u=N.getObservationsByIds(h,{orderBy:"date_desc",limit:o==="auto"?1:l}),console.error(`[search-server] Hydrated ${u.length} observations from SQLite`))}}catch(m){console.error("[search-server] Chroma query failed, falling back to FTS5:",m.message)}if(u.length===0&&(console.error("[search-server] Using FTS5 keyword search"),u=$.searchObservations(n,{orderBy:"relevance",limit:o==="auto"?1:l,project:p})),u.length===0)return{content:[{type:"text",text:`No observations found matching "${n}". Try a different search query.`}]};if(o==="interactive"){let m=[];m.push("# Timeline Anchor Search Results"),m.push(""),m.push(`Found ${u.length} observation(s) matching "${n}"`),m.push(""),m.push("To get timeline context around any of these observations, use the `get_context_timeline` tool with the observation ID as the anchor."),m.push(""),m.push(`**Top ${u.length} matches:**`),m.push("");for(let f=0;f<u.length;f++){let h=u[f],b=h.title||`Observation #${h.id}`,_=new Date(h.created_at_epoch).toLocaleString(),E=h.type?`[${h.type}]`:"";m.push(`${f+1}. **${E} ${b}**`),m.push(` - ID: ${h.id}`),m.push(` - Date: ${_}`),h.subtitle&&m.push(` - ${h.subtitle}`),m.push(` - Source: claude-mem://observation/${h.id}`),m.push("")}return{content:[{type:"text",text:m.join(`
|
||||
`)}]}}else{let b=function(S){return new Date(S).toLocaleString("en-US",{month:"short",day:"numeric",year:"numeric"})},_=function(S){return new Date(S).toLocaleString("en-US",{hour:"numeric",minute:"2-digit",hour12:!0})},E=function(S){return new Date(S).toLocaleString("en-US",{month:"short",day:"numeric",hour:"numeric",minute:"2-digit",hour12:!0})},x=function(S){return S?Math.ceil(S.length/4):0};var e=b,s=_,r=E,t=x;let m=u[0];console.error(`[search-server] Auto mode: Using observation #${m.id} as timeline anchor`);let f=N.getTimelineAroundObservation(m.id,m.created_at_epoch,a,d,p),h=[...f.observations.map(S=>({type:"observation",data:S,epoch:S.created_at_epoch})),...f.sessions.map(S=>({type:"session",data:S,epoch:S.created_at_epoch})),...f.prompts.map(S=>({type:"prompt",data:S,epoch:S.created_at_epoch}))];if(h.sort((S,O)=>S.epoch-O.epoch),h.length===0)return{content:[{type:"text",text:`Found observation #${m.id} matching "${n}", but no timeline context available (${a} records before, ${d} records after).`}]};let T=[];T.push(`# Timeline for query: "${n}"`),T.push(`**Anchor:** Observation #${m.id} - ${m.title||"Untitled"}`),T.push(`**Window:** ${a} records before \u2192 ${d} records after | **Items:** ${h.length} (${f.observations.length} obs, ${f.sessions.length} sessions, ${f.prompts.length} prompts)`),T.push(""),T.push("**Legend:** \u{1F3AF} session-request | \u{1F534} bugfix | \u{1F7E3} feature | \u{1F504} refactor | \u2705 change | \u{1F535} discovery | \u{1F9E0} decision"),T.push("");let g=new Map;for(let S of h){let O=b(S.epoch);g.has(O)||g.set(O,[]),g.get(O).push(S)}let I=Array.from(g.entries()).sort((S,O)=>{let C=new Date(S[0]).getTime(),v=new Date(O[0]).getTime();return C-v});for(let[S,O]of I){T.push(`### ${S}`),T.push("");let C=null,v="",F=!1;for(let y of O){let M=y.type==="observation"&&y.data.id===m.id;if(y.type==="session"){F&&(T.push(""),F=!1,C=null,v="");let R=y.data,A=R.request||"Session summary",D=`claude-mem://session-summary/${R.id}`;T.push(`**\u{1F3AF} #S${R.id}** ${A} (${E(y.epoch)}) [\u2192](${D})`),T.push("")}else if(y.type==="prompt"){F&&(T.push(""),F=!1,C=null,v="");let R=y.data,A=R.prompt.length>100?R.prompt.substring(0,100)+"...":R.prompt;T.push(`**\u{1F4AC} User Prompt #${R.prompt_number}** (${E(y.epoch)})`),T.push(`> ${A}`),T.push("")}else if(y.type==="observation"){let R=y.data,A="General";A!==C&&(F&&T.push(""),T.push(`**${A}**`),T.push("| ID | Time | T | Title | Tokens |"),T.push("|----|------|---|-------|--------|"),C=A,F=!0,v="");let D="\u2022";switch(R.type){case"bugfix":D="\u{1F534}";break;case"feature":D="\u{1F7E3}";break;case"refactor":D="\u{1F504}";break;case"change":D="\u2705";break;case"discovery":D="\u{1F535}";break;case"decision":D="\u{1F9E0}";break}let B=_(y.epoch),z=R.title||"Untitled",Y=x(R.narrative),ie=B!==v?B:"\u2033";v=B;let ae=M?" \u2190 **ANCHOR**":"";T.push(`| #${R.id} | ${ie} | ${D} | ${z}${ae} | ~${Y} |`)}}F&&T.push("")}return{content:[{type:"text",text:T.join(`
|
||||
`);return{content:[{type:"text",text:n}]}}catch(e){return{content:[{type:"text",text:`Search failed: ${e.message}`}],isError:!0}}}},{name:"get_context_timeline",description:'Get a unified timeline of context (observations, sessions, and prompts) around a specific point in time. All record types are interleaved chronologically. Useful for understanding "what was happening when X occurred". Returns depth_before records before anchor + anchor + depth_after records after (total: depth_before + 1 + depth_after mixed records).',inputSchema:i.object({anchor:i.union([i.number().describe("Observation ID to center timeline around"),i.string().describe("Session ID (format: S123) or ISO timestamp to center timeline around")]).describe('Anchor point: observation ID, session ID (e.g., "S123"), or ISO timestamp'),depth_before:i.number().min(0).max(50).default(10).describe("Number of records to retrieve before anchor, not including anchor (default: 10)"),depth_after:i.number().min(0).max(50).default(10).describe("Number of records to retrieve after anchor, not including anchor (default: 10)"),project:i.string().optional().describe("Filter by project name")}),handler:async c=>{try{let f=function(g){return new Date(g).toLocaleString("en-US",{month:"short",day:"numeric",year:"numeric"})},h=function(g){return new Date(g).toLocaleString("en-US",{hour:"numeric",minute:"2-digit",hour12:!0})},b=function(g){return new Date(g).toLocaleString("en-US",{month:"short",day:"numeric",hour:"numeric",minute:"2-digit",hour12:!0})},_=function(g){return g?Math.ceil(g.length/4):0};var e=f,s=h,r=b,t=_;let{anchor:n,depth_before:o=10,depth_after:a=10,project:d}=c,l,p=n,u;if(typeof n=="number"){let g=N.getObservationById(n);if(!g)return{content:[{type:"text",text:`Observation #${n} not found`}],isError:!0};l=g.created_at_epoch,u=N.getTimelineAroundObservation(n,l,o,a,d)}else if(typeof n=="string")if(n.startsWith("S")||n.startsWith("#S")){let g=n.replace(/^#?S/,""),I=parseInt(g,10),S=N.getSessionSummariesByIds([I]);if(S.length===0)return{content:[{type:"text",text:`Session #${I} not found`}],isError:!0};l=S[0].created_at_epoch,p=`S${I}`,u=N.getTimelineAroundTimestamp(l,o,a,d)}else{let g=new Date(n);if(isNaN(g.getTime()))return{content:[{type:"text",text:`Invalid timestamp: ${n}`}],isError:!0};l=g.getTime(),u=N.getTimelineAroundTimestamp(l,o,a,d)}else return{content:[{type:"text",text:'Invalid anchor: must be observation ID (number), session ID (e.g., "S123"), or ISO timestamp'}],isError:!0};let m=[...u.observations.map(g=>({type:"observation",data:g,epoch:g.created_at_epoch})),...u.sessions.map(g=>({type:"session",data:g,epoch:g.created_at_epoch})),...u.prompts.map(g=>({type:"prompt",data:g,epoch:g.created_at_epoch}))];if(m.sort((g,I)=>g.epoch-I.epoch),m.length===0)return{content:[{type:"text",text:`No context found around ${new Date(l).toLocaleString()} (${o} records before, ${a} records after)`}]};let E=[];E.push(`# Timeline around anchor: ${p}`),E.push(`**Window:** ${o} records before \u2192 ${a} records after | **Items:** ${m.length} (${u.observations.length} obs, ${u.sessions.length} sessions, ${u.prompts.length} prompts)`),E.push(""),E.push("**Legend:** \u{1F3AF} session-request | \u{1F534} bugfix | \u{1F7E3} feature | \u{1F504} refactor | \u2705 change | \u{1F535} discovery | \u{1F9E0} decision"),E.push("");let x=new Map;for(let g of m){let I=f(g.epoch);x.has(I)||x.set(I,[]),x.get(I).push(g)}let T=Array.from(x.entries()).sort((g,I)=>{let S=new Date(g[0]).getTime(),O=new Date(I[0]).getTime();return S-O});for(let[g,I]of T){E.push(`### ${g}`),E.push("");let S=null,O="",C=!1;for(let v of I){let F=typeof p=="number"&&v.type==="observation"&&v.data.id===p||typeof p=="string"&&p.startsWith("S")&&v.type==="session"&&`S${v.data.id}`===p;if(v.type==="session"){C&&(E.push(""),C=!1,S=null,O="");let y=v.data,U=y.request||"Session summary",R=`claude-mem://session-summary/${y.id}`,A=F?" \u2190 **ANCHOR**":"";E.push(`**\u{1F3AF} #S${y.id}** ${U} (${b(v.epoch)}) [\u2192](${R})${A}`),E.push("")}else if(v.type==="prompt"){C&&(E.push(""),C=!1,S=null,O="");let y=v.data,U=y.prompt.length>100?y.prompt.substring(0,100)+"...":y.prompt;E.push(`**\u{1F4AC} User Prompt #${y.prompt_number}** (${b(v.epoch)})`),E.push(`> ${U}`),E.push("")}else if(v.type==="observation"){let y=v.data,U="General";U!==S&&(C&&E.push(""),E.push(`**${U}**`),E.push("| ID | Time | T | Title | Tokens |"),E.push("|----|------|---|-------|--------|"),S=U,C=!0,O="");let R="\u2022";switch(y.type){case"bugfix":R="\u{1F534}";break;case"feature":R="\u{1F7E3}";break;case"refactor":R="\u{1F504}";break;case"change":R="\u2705";break;case"discovery":R="\u{1F535}";break;case"decision":R="\u{1F9E0}";break}let A=h(v.epoch),D=y.title||"Untitled",B=_(y.narrative),Y=A!==O?A:"\u2033";O=A;let Z=F?" \u2190 **ANCHOR**":"";E.push(`| #${y.id} | ${Y} | ${R} | ${D}${Z} | ~${B} |`)}}C&&E.push("")}return{content:[{type:"text",text:E.join(`
|
||||
`)}]}}catch(n){return{content:[{type:"text",text:`Timeline query failed: ${n.message}`}],isError:!0}}}},{name:"get_timeline_by_query",description:'Search for observations using natural language and get timeline context around the best match. Two modes: "auto" (default) automatically uses top result as timeline anchor; "interactive" returns top matches for you to choose from. This combines search + timeline into a single operation for faster context discovery.',inputSchema:i.object({query:i.string().describe("Natural language search query to find relevant observations"),mode:i.enum(["auto","interactive"]).default("auto").describe("auto: Automatically use top search result as timeline anchor. interactive: Show top N search results for manual anchor selection."),depth_before:i.number().min(0).max(50).default(10).describe("Number of timeline records before anchor (default: 10)"),depth_after:i.number().min(0).max(50).default(10).describe("Number of timeline records after anchor (default: 10)"),limit:i.number().min(1).max(20).default(5).describe("For interactive mode: number of top search results to display (default: 5)"),project:i.string().optional().describe("Filter by project name")}),handler:async c=>{try{let{query:n,mode:o="auto",depth_before:a=10,depth_after:d=10,limit:l=5,project:p}=c,u=[];if(k)try{console.error("[search-server] Using hybrid semantic search for timeline query");let m=await M(n,100);if(console.error(`[search-server] Chroma returned ${m.ids.length} semantic matches`),m.ids.length>0){let f=Date.now()-7776e6,h=m.ids.filter((b,_)=>{let E=m.metadatas[_];return E&&E.created_at_epoch>f});console.error(`[search-server] ${h.length} results within 90-day window`),h.length>0&&(u=N.getObservationsByIds(h,{orderBy:"date_desc",limit:o==="auto"?1:l}),console.error(`[search-server] Hydrated ${u.length} observations from SQLite`))}}catch(m){console.error("[search-server] Chroma query failed, falling back to FTS5:",m.message)}if(u.length===0&&(console.error("[search-server] Using FTS5 keyword search"),u=$.searchObservations(n,{orderBy:"relevance",limit:o==="auto"?1:l,project:p})),u.length===0)return{content:[{type:"text",text:`No observations found matching "${n}". Try a different search query.`}]};if(o==="interactive"){let m=[];m.push("# Timeline Anchor Search Results"),m.push(""),m.push(`Found ${u.length} observation(s) matching "${n}"`),m.push(""),m.push("To get timeline context around any of these observations, use the `get_context_timeline` tool with the observation ID as the anchor."),m.push(""),m.push(`**Top ${u.length} matches:**`),m.push("");for(let f=0;f<u.length;f++){let h=u[f],b=h.title||`Observation #${h.id}`,_=new Date(h.created_at_epoch).toLocaleString(),E=h.type?`[${h.type}]`:"";m.push(`${f+1}. **${E} ${b}**`),m.push(` - ID: ${h.id}`),m.push(` - Date: ${_}`),h.subtitle&&m.push(` - ${h.subtitle}`),m.push(` - Source: claude-mem://observation/${h.id}`),m.push("")}return{content:[{type:"text",text:m.join(`
|
||||
`)}]}}else{let b=function(S){return new Date(S).toLocaleString("en-US",{month:"short",day:"numeric",year:"numeric"})},_=function(S){return new Date(S).toLocaleString("en-US",{hour:"numeric",minute:"2-digit",hour12:!0})},E=function(S){return new Date(S).toLocaleString("en-US",{month:"short",day:"numeric",hour:"numeric",minute:"2-digit",hour12:!0})},x=function(S){return S?Math.ceil(S.length/4):0};var e=b,s=_,r=E,t=x;let m=u[0];console.error(`[search-server] Auto mode: Using observation #${m.id} as timeline anchor`);let f=N.getTimelineAroundObservation(m.id,m.created_at_epoch,a,d,p),h=[...f.observations.map(S=>({type:"observation",data:S,epoch:S.created_at_epoch})),...f.sessions.map(S=>({type:"session",data:S,epoch:S.created_at_epoch})),...f.prompts.map(S=>({type:"prompt",data:S,epoch:S.created_at_epoch}))];if(h.sort((S,O)=>S.epoch-O.epoch),h.length===0)return{content:[{type:"text",text:`Found observation #${m.id} matching "${n}", but no timeline context available (${a} records before, ${d} records after).`}]};let T=[];T.push(`# Timeline for query: "${n}"`),T.push(`**Anchor:** Observation #${m.id} - ${m.title||"Untitled"}`),T.push(`**Window:** ${a} records before \u2192 ${d} records after | **Items:** ${h.length} (${f.observations.length} obs, ${f.sessions.length} sessions, ${f.prompts.length} prompts)`),T.push(""),T.push("**Legend:** \u{1F3AF} session-request | \u{1F534} bugfix | \u{1F7E3} feature | \u{1F504} refactor | \u2705 change | \u{1F535} discovery | \u{1F9E0} decision"),T.push("");let g=new Map;for(let S of h){let O=b(S.epoch);g.has(O)||g.set(O,[]),g.get(O).push(S)}let I=Array.from(g.entries()).sort((S,O)=>{let C=new Date(S[0]).getTime(),v=new Date(O[0]).getTime();return C-v});for(let[S,O]of I){T.push(`### ${S}`),T.push("");let C=null,v="",F=!1;for(let y of O){let U=y.type==="observation"&&y.data.id===m.id;if(y.type==="session"){F&&(T.push(""),F=!1,C=null,v="");let R=y.data,A=R.request||"Session summary",D=`claude-mem://session-summary/${R.id}`;T.push(`**\u{1F3AF} #S${R.id}** ${A} (${E(y.epoch)}) [\u2192](${D})`),T.push("")}else if(y.type==="prompt"){F&&(T.push(""),F=!1,C=null,v="");let R=y.data,A=R.prompt.length>100?R.prompt.substring(0,100)+"...":R.prompt;T.push(`**\u{1F4AC} User Prompt #${R.prompt_number}** (${E(y.epoch)})`),T.push(`> ${A}`),T.push("")}else if(y.type==="observation"){let R=y.data,A="General";A!==C&&(F&&T.push(""),T.push(`**${A}**`),T.push("| ID | Time | T | Title | Tokens |"),T.push("|----|------|---|-------|--------|"),C=A,F=!0,v="");let D="\u2022";switch(R.type){case"bugfix":D="\u{1F534}";break;case"feature":D="\u{1F7E3}";break;case"refactor":D="\u{1F504}";break;case"change":D="\u2705";break;case"discovery":D="\u{1F535}";break;case"decision":D="\u{1F9E0}";break}let B=_(y.epoch),z=R.title||"Untitled",Y=x(R.narrative),ie=B!==v?B:"\u2033";v=B;let ae=U?" \u2190 **ANCHOR**":"";T.push(`| #${R.id} | ${ie} | ${D} | ${z}${ae} | ~${Y} |`)}}F&&T.push("")}return{content:[{type:"text",text:T.join(`
|
||||
`)}]}}}catch(n){return{content:[{type:"text",text:`Timeline query failed: ${n.message}`}],isError:!0}}}}],Q=new he({name:"claude-mem-search",version:"1.0.0"},{capabilities:{tools:{}}});Q.setRequestHandler(ge,async()=>({tools:oe.map(c=>({name:c.name,description:c.description,inputSchema:Te(c.inputSchema)}))}));Q.setRequestHandler(be,async c=>{let e=oe.find(s=>s.name===c.params.name);if(!e)throw new Error(`Unknown tool: ${c.params.name}`);try{return await e.handler(c.params.arguments||{})}catch(s){return{content:[{type:"text",text:`Tool execution failed: ${s.message}`}],isError:!0}}});async function Ie(){let c=new _e;await Q.connect(c),console.error("[search-server] Claude-mem search server started"),setTimeout(async()=>{try{console.error("[search-server] Initializing Chroma client...");let e=new Ee({command:"uvx",args:["chroma-mcp","--client-type","persistent","--data-dir",te],stderr:"ignore"}),s=new fe({name:"claude-mem-search-chroma-client",version:"1.0.0"},{capabilities:{}});await s.connect(e),k=s,console.error("[search-server] Chroma client connected successfully")}catch(e){console.error("[search-server] Failed to initialize Chroma client:",e.message),console.error("[search-server] Falling back to FTS5-only search"),k=null}},0)}Ie().catch(c=>{console.error("[search-server] Fatal error:",c),process.exit(1)});
|
||||
|
||||
Reference in New Issue
Block a user