From 225424a19e337ecdd048e443efbd4d3910388bde Mon Sep 17 00:00:00 2001 From: Copilot <198982749+Copilot@users.noreply.github.com> Date: Sat, 6 Dec 2025 17:29:20 -0500 Subject: [PATCH] Fix Python 3.14 incompatibility: Pin uvx to Python 3.13 for chroma-mcp (#171) * Initial plan * Initial exploration - no changes yet Co-authored-by: thedotmack <683968+thedotmack@users.noreply.github.com> * Add CLAUDE_MEM_PYTHON_VERSION env var defaulting to 3.13 for uvx/chroma-mcp to avoid onnxruntime Python 3.14 compatibility issues Co-authored-by: thedotmack <683968+thedotmack@users.noreply.github.com> * Fix code review comment: update placeholder issue URL to #170 Co-authored-by: thedotmack <683968+thedotmack@users.noreply.github.com> --------- Co-authored-by: copilot-swe-agent[bot] <198982749+Copilot@users.noreply.github.com> Co-authored-by: thedotmack <683968+thedotmack@users.noreply.github.com> --- CLAUDE.md | 1 + README.md | 1 + package-lock.json | 4 ++-- plugin/scripts/search-server.cjs | 2 +- plugin/scripts/worker-service.cjs | 2 +- src/servers/search-server.ts | 4 +++- src/services/sync/ChromaSync.ts | 4 ++++ 7 files changed, 13 insertions(+), 5 deletions(-) diff --git a/CLAUDE.md b/CLAUDE.md index a3ac4fa2..ff9d7e67 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -47,6 +47,7 @@ Claude-mem is a Claude Code plugin providing persistent memory across sessions. - `CLAUDE_MEM_MODEL` - Model for observations/summaries (default: claude-haiku-4-5) - `CLAUDE_MEM_CONTEXT_OBSERVATIONS` - Observations injected at SessionStart (default: 50) - `CLAUDE_MEM_WORKER_PORT` - Worker service port (default: 37777) +- `CLAUDE_MEM_PYTHON_VERSION` - Python version for uvx/chroma-mcp (default: 3.13, avoids onnxruntime compatibility issues with Python 3.14+) ## File Locations diff --git a/README.md b/README.md index 8e73e968..edb42c5e 100644 --- a/README.md +++ b/README.md @@ -317,6 +317,7 @@ See [CHANGELOG.md](CHANGELOG.md) for complete version history. - `CLAUDE_MEM_MODEL` - AI model for processing (default: claude-haiku-4-5) - `CLAUDE_MEM_WORKER_PORT` - Worker port (default: 37777) - `CLAUDE_MEM_DATA_DIR` - Data directory override (dev only) +- `CLAUDE_MEM_PYTHON_VERSION` - Python version for uvx/chroma-mcp (default: 3.13) See [Configuration Guide](https://docs.claude-mem.ai/configuration) for details. diff --git a/package-lock.json b/package-lock.json index ce7be241..bcb3bb35 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,12 +1,12 @@ { "name": "claude-mem", - "version": "6.5.0", + "version": "6.5.2", "lockfileVersion": 3, "requires": true, "packages": { "": { "name": "claude-mem", - "version": "6.5.0", + "version": "6.5.2", "license": "AGPL-3.0", "dependencies": { "@anthropic-ai/claude-agent-sdk": "^0.1.27", diff --git a/plugin/scripts/search-server.cjs b/plugin/scripts/search-server.cjs index a8697488..9e1c0749 100755 --- a/plugin/scripts/search-server.cjs +++ b/plugin/scripts/search-server.cjs @@ -650,7 +650,7 @@ No previous sessions found for project "${e}".`}]};let t=[];t.push("# Recent Ses `);return{content:[{type:"text",text:i}]}}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:c.object({anchor:c.union([c.number().describe("Observation ID to center timeline around"),c.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:c.number().min(0).max(50).default(10).describe("Number of records to retrieve before anchor, not including anchor (default: 10)"),depth_after:c.number().min(0).max(50).default(10).describe("Number of records to retrieve after anchor, not including anchor (default: 10)"),project:c.string().optional().describe("Filter by project name")}),handler:async s=>{try{let g=function(T){return new Date(T).toLocaleString("en-US",{month:"short",day:"numeric",year:"numeric"})},y=function(T){return new Date(T).toLocaleString("en-US",{hour:"numeric",minute:"2-digit",hour12:!0})},v=function(T){return new Date(T).toLocaleString("en-US",{month:"short",day:"numeric",hour:"numeric",minute:"2-digit",hour12:!0})},x=function(T){return T?Math.ceil(T.length/4):0};var e=g,r=y,a=v,t=x;let{anchor:n,depth_before:i=10,depth_after:o=10,project:l}=s,u,d=n,f;if(typeof n=="number"){let T=ne.getObservationById(n);if(!T)return{content:[{type:"text",text:`Observation #${n} not found`}],isError:!0};u=T.created_at_epoch,f=ne.getTimelineAroundObservation(n,u,i,o,l)}else if(typeof n=="string")if(n.startsWith("S")||n.startsWith("#S")){let T=n.replace(/^#?S/,""),S=parseInt(T,10),I=ne.getSessionSummariesByIds([S]);if(I.length===0)return{content:[{type:"text",text:`Session #${S} not found`}],isError:!0};u=I[0].created_at_epoch,d=`S${S}`,f=ne.getTimelineAroundTimestamp(u,i,o,l)}else{let T=new Date(n);if(isNaN(T.getTime()))return{content:[{type:"text",text:`Invalid timestamp: ${n}`}],isError:!0};u=T.getTime(),f=ne.getTimelineAroundTimestamp(u,i,o,l)}else return{content:[{type:"text",text:'Invalid anchor: must be observation ID (number), session ID (e.g., "S123"), or ISO timestamp'}],isError:!0};let h=[...f.observations.map(T=>({type:"observation",data:T,epoch:T.created_at_epoch})),...f.sessions.map(T=>({type:"session",data:T,epoch:T.created_at_epoch})),...f.prompts.map(T=>({type:"prompt",data:T,epoch:T.created_at_epoch}))];h.sort((T,S)=>T.epoch-S.epoch);let p=cn(h,d,u,i,o);if(p.length===0)return{content:[{type:"text",text:`No context found around ${new Date(u).toLocaleString()} (${i} records before, ${o} records after)`}]};let P=[];P.push(`# Timeline around anchor: ${d}`),P.push(`**Window:** ${i} records before \u2192 ${o} records after | **Items:** ${p.length}`),P.push(""),P.push("**Legend:** \u{1F3AF} session-request | \u{1F534} bugfix | \u{1F7E3} feature | \u{1F504} refactor | \u2705 change | \u{1F535} discovery | \u{1F9E0} decision"),P.push("");let E=new Map;for(let T of p){let S=g(T.epoch);E.has(S)||E.set(S,[]),E.get(S).push(T)}let R=Array.from(E.entries()).sort((T,S)=>{let I=new Date(T[0]).getTime(),D=new Date(S[0]).getTime();return I-D});for(let[T,S]of R){P.push(`### ${T}`),P.push("");let I=null,D="",$=!1;for(let L of S){let C=typeof d=="number"&&L.type==="observation"&&L.data.id===d||typeof d=="string"&&d.startsWith("S")&&L.type==="session"&&`S${L.data.id}`===d;if(L.type==="session"){$&&(P.push(""),$=!1,I=null,D="");let k=L.data,N=k.request||"Session summary",A=`claude-mem://session-summary/${k.id}`,M=C?" \u2190 **ANCHOR**":"";P.push(`**\u{1F3AF} #S${k.id}** ${N} (${v(L.epoch)}) [\u2192](${A})${M}`),P.push("")}else if(L.type==="prompt"){$&&(P.push(""),$=!1,I=null,D="");let k=L.data,N=k.prompt.length>100?k.prompt.substring(0,100)+"...":k.prompt;P.push(`**\u{1F4AC} User Prompt #${k.prompt_number}** (${v(L.epoch)})`),P.push(`> ${N}`),P.push("")}else if(L.type==="observation"){let k=L.data,N="General";N!==I&&($&&P.push(""),P.push(`**${N}**`),P.push("| ID | Time | T | Title | Tokens |"),P.push("|----|------|---|-------|--------|"),I=N,$=!0,D="");let A="\u2022";switch(k.type){case"bugfix":A="\u{1F534}";break;case"feature":A="\u{1F7E3}";break;case"refactor":A="\u{1F504}";break;case"change":A="\u2705";break;case"discovery":A="\u{1F535}";break;case"decision":A="\u{1F9E0}";break}let M=y(L.epoch),X=k.title||"Untitled",W=x(k.narrative),Q=M!==D?M:"\u2033";D=M;let z=C?" \u2190 **ANCHOR**":"";P.push(`| #${k.id} | ${Q} | ${A} | ${X}${z} | ~${W} |`)}}$&&P.push("")}return{content:[{type:"text",text:P.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:c.object({query:c.string().describe("Natural language search query to find relevant observations"),mode:c.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:c.number().min(0).max(50).default(10).describe("Number of timeline records before anchor (default: 10)"),depth_after:c.number().min(0).max(50).default(10).describe("Number of timeline records after anchor (default: 10)"),limit:c.number().min(1).max(20).default(5).describe("For interactive mode: number of top search results to display (default: 5)"),project:c.string().optional().describe("Filter by project name")}),handler:async s=>{try{let{query:n,mode:i="auto",depth_before:o=10,depth_after:l=10,limit:u=5,project:d}=s,f=[];if(we)try{console.error("[search-server] Using hybrid semantic search for timeline query");let h=await ze(n,100);if(console.error(`[search-server] Chroma returned ${h.ids.length} semantic matches`),h.ids.length>0){let p=Date.now()-7776e6,g=h.ids.filter((y,v)=>{let x=h.metadatas[v];return x&&x.created_at_epoch>p});console.error(`[search-server] ${g.length} results within 90-day window`),g.length>0&&(f=ne.getObservationsByIds(g,{orderBy:"date_desc",limit:i==="auto"?1:u}),console.error(`[search-server] Hydrated ${f.length} observations from SQLite`))}}catch(h){console.error("[search-server] Chroma query failed - no results (FTS5 fallback removed):",h.message)}if(f.length===0)return{content:[{type:"text",text:`No observations found matching "${n}". Try a different search query.`}]};if(i==="interactive"){let h=[];h.push("# Timeline Anchor Search Results"),h.push(""),h.push(`Found ${f.length} observation(s) matching "${n}"`),h.push(""),h.push("To get timeline context around any of these observations, use the `get_context_timeline` tool with the observation ID as the anchor."),h.push(""),h.push(`**Top ${f.length} matches:**`),h.push("");for(let p=0;p({type:"observation",data:I,epoch:I.created_at_epoch})),...p.sessions.map(I=>({type:"session",data:I,epoch:I.created_at_epoch})),...p.prompts.map(I=>({type:"prompt",data:I,epoch:I.created_at_epoch}))];g.sort((I,D)=>I.epoch-D.epoch);let y=cn(g,h.id,0,o,l);if(y.length===0)return{content:[{type:"text",text:`Found observation #${h.id} matching "${n}", but no timeline context available (${o} records before, ${l} records after).`}]};let R=[];R.push(`# Timeline for query: "${n}"`),R.push(`**Anchor:** Observation #${h.id} - ${h.title||"Untitled"}`),R.push(`**Window:** ${o} records before \u2192 ${l} records after | **Items:** ${y.length}`),R.push(""),R.push("**Legend:** \u{1F3AF} session-request | \u{1F534} bugfix | \u{1F7E3} feature | \u{1F504} refactor | \u2705 change | \u{1F535} discovery | \u{1F9E0} decision"),R.push("");let T=new Map;for(let I of y){let D=v(I.epoch);T.has(D)||T.set(D,[]),T.get(D).push(I)}let S=Array.from(T.entries()).sort((I,D)=>{let $=new Date(I[0]).getTime(),L=new Date(D[0]).getTime();return $-L});for(let[I,D]of S){R.push(`### ${I}`),R.push("");let $=null,L="",C=!1;for(let k of D){let N=k.type==="observation"&&k.data.id===h.id;if(k.type==="session"){C&&(R.push(""),C=!1,$=null,L="");let A=k.data,M=A.request||"Session summary",X=`claude-mem://session-summary/${A.id}`;R.push(`**\u{1F3AF} #S${A.id}** ${M} (${P(k.epoch)}) [\u2192](${X})`),R.push("")}else if(k.type==="prompt"){C&&(R.push(""),C=!1,$=null,L="");let A=k.data,M=A.prompt.length>100?A.prompt.substring(0,100)+"...":A.prompt;R.push(`**\u{1F4AC} User Prompt #${A.prompt_number}** (${P(k.epoch)})`),R.push(`> ${M}`),R.push("")}else if(k.type==="observation"){let A=k.data,M="General";M!==$&&(C&&R.push(""),R.push(`**${M}**`),R.push("| ID | Time | T | Title | Tokens |"),R.push("|----|------|---|-------|--------|"),$=M,C=!0,L="");let X="\u2022";switch(A.type){case"bugfix":X="\u{1F534}";break;case"feature":X="\u{1F7E3}";break;case"refactor":X="\u{1F504}";break;case"change":X="\u2705";break;case"discovery":X="\u{1F535}";break;case"decision":X="\u{1F9E0}";break}let W=x(k.epoch),ee=A.title||"Untitled",Q=E(A.narrative),pe=W!==L?W:"\u2033";L=W;let Re=N?" \u2190 **ANCHOR**":"";R.push(`| #${A.id} | ${pe} | ${X} | ${ee}${Re} | ~${Q} |`)}}C&&R.push("")}return{content:[{type:"text",text:R.join(` -`)}]}}}catch(n){return{content:[{type:"text",text:`Timeline query failed: ${n.message}`}],isError:!0}}}}],dn=new Os({name:"claude-mem-search",version:"1.0.0"},{capabilities:{tools:{}}});dn.setRequestHandler(ca,async()=>({tools:Dl.map(s=>({name:s.name,description:s.description,inputSchema:tn(s.inputSchema)}))}));dn.setRequestHandler(ua,async s=>{let e=Dl.find(r=>r.name===s.params.name);if(!e)throw new Error(`Unknown tool: ${s.params.name}`);try{return await e.handler(s.params.arguments||{})}catch(r){return{content:[{type:"text",text:`Tool execution failed: ${r.message}`}],isError:!0}}});async function Cl(){if(console.error("[search-server] Shutting down..."),we)try{await we.close(),console.error("[search-server] Chroma client closed")}catch(s){console.error("[search-server] Error closing Chroma client:",s.message)}if(_e)try{_e.close(),console.error("[search-server] SessionSearch closed")}catch(s){console.error("[search-server] Error closing SessionSearch:",s.message)}if(ne)try{ne.close(),console.error("[search-server] SessionStore closed")}catch(s){console.error("[search-server] Error closing SessionStore:",s.message)}console.error("[search-server] Shutdown complete"),process.exit(0)}process.on("SIGTERM",Cl);process.on("SIGINT",Cl);async function Oh(){let s=new Is;await dn.connect(s),console.error("[search-server] Claude-mem search server started"),setTimeout(async()=>{try{console.error("[search-server] Initializing Chroma client...");let e=new $s({command:"uvx",args:["chroma-mcp","--client-type","persistent","--data-dir",xl],stderr:"ignore"}),r=new Ns({name:"claude-mem-search-chroma-client",version:"1.0.0"},{capabilities:{}});await r.connect(e),we=r,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] Vector search unavailable - text queries will return empty results (FTS5 fallback removed)"),console.error("[search-server] Install UVX/Python to enable vector search: https://docs.astral.sh/uv/getting-started/installation/"),we=null}},0)}Oh().catch(s=>{console.error("[search-server] Fatal error:",s),process.exit(1)}); +`)}]}}}catch(n){return{content:[{type:"text",text:`Timeline query failed: ${n.message}`}],isError:!0}}}}],dn=new Os({name:"claude-mem-search",version:"1.0.0"},{capabilities:{tools:{}}});dn.setRequestHandler(ca,async()=>({tools:Dl.map(s=>({name:s.name,description:s.description,inputSchema:tn(s.inputSchema)}))}));dn.setRequestHandler(ua,async s=>{let e=Dl.find(r=>r.name===s.params.name);if(!e)throw new Error(`Unknown tool: ${s.params.name}`);try{return await e.handler(s.params.arguments||{})}catch(r){return{content:[{type:"text",text:`Tool execution failed: ${r.message}`}],isError:!0}}});async function Cl(){if(console.error("[search-server] Shutting down..."),we)try{await we.close(),console.error("[search-server] Chroma client closed")}catch(s){console.error("[search-server] Error closing Chroma client:",s.message)}if(_e)try{_e.close(),console.error("[search-server] SessionSearch closed")}catch(s){console.error("[search-server] Error closing SessionSearch:",s.message)}if(ne)try{ne.close(),console.error("[search-server] SessionStore closed")}catch(s){console.error("[search-server] Error closing SessionStore:",s.message)}console.error("[search-server] Shutdown complete"),process.exit(0)}process.on("SIGTERM",Cl);process.on("SIGINT",Cl);async function Oh(){let s=new Is;await dn.connect(s),console.error("[search-server] Claude-mem search server started"),setTimeout(async()=>{try{console.error("[search-server] Initializing Chroma client...");let e=process.env.CLAUDE_MEM_PYTHON_VERSION||"3.13",r=new $s({command:"uvx",args:["--python",e,"chroma-mcp","--client-type","persistent","--data-dir",xl],stderr:"ignore"}),a=new Ns({name:"claude-mem-search-chroma-client",version:"1.0.0"},{capabilities:{}});await a.connect(r),we=a,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] Vector search unavailable - text queries will return empty results (FTS5 fallback removed)"),console.error("[search-server] Install UVX/Python to enable vector search: https://docs.astral.sh/uv/getting-started/installation/"),we=null}},0)}Oh().catch(s=>{console.error("[search-server] Fatal error:",s),process.exit(1)}); /*! Bundled license information: uri-js/dist/es5/uri.all.js: diff --git a/plugin/scripts/worker-service.cjs b/plugin/scripts/worker-service.cjs index 5ee7b335..8a7888dd 100755 --- a/plugin/scripts/worker-service.cjs +++ b/plugin/scripts/worker-service.cjs @@ -583,7 +583,7 @@ ${e.stack}`:e.message;if(Array.isArray(e))return`[${e.length} items]`;let r=Obje FROM user_prompts WHERE claude_session_id = ? ORDER BY prompt_number ASC - `).all(e)}close(){this.db.close()}};var $E=Nt(require("path"),1),ME=Nt(require("os"),1),Fc=class{client=null;connected=!1;project;collectionName;VECTOR_DB_DIR;BATCH_SIZE=100;constructor(e){this.project=e,this.collectionName=`cm__${e}`,this.VECTOR_DB_DIR=$E.default.join(ME.default.homedir(),".claude-mem","vector-db")}async ensureConnection(){if(!(this.connected&&this.client)){H.info("CHROMA_SYNC","Connecting to Chroma MCP server...",{project:this.project});try{let e=new En({command:"uvx",args:["chroma-mcp","--client-type","persistent","--data-dir",this.VECTOR_DB_DIR],stderr:"ignore"});this.client=new bn({name:"claude-mem-chroma-sync",version:"1.0.0"},{capabilities:{}}),await this.client.connect(e),this.connected=!0,H.info("CHROMA_SYNC","Connected to Chroma MCP server",{project:this.project})}catch(e){throw H.error("CHROMA_SYNC","Failed to connect to Chroma MCP server",{project:this.project},e),new Error(`Chroma connection failed: ${e instanceof Error?e.message:String(e)}`)}}}async ensureCollection(){if(await this.ensureConnection(),!this.client)throw new Error("Chroma client not initialized");try{await this.client.callTool({name:"chroma_get_collection_info",arguments:{collection_name:this.collectionName}}),H.debug("CHROMA_SYNC","Collection exists",{collection:this.collectionName})}catch{H.info("CHROMA_SYNC","Creating collection",{collection:this.collectionName});try{await this.client.callTool({name:"chroma_create_collection",arguments:{collection_name:this.collectionName,embedding_function_name:"default"}}),H.info("CHROMA_SYNC","Collection created",{collection:this.collectionName})}catch(r){throw H.error("CHROMA_SYNC","Failed to create collection",{collection:this.collectionName},r),new Error(`Collection creation failed: ${r instanceof Error?r.message:String(r)}`)}}}formatObservationDocs(e){let r=[],t=e.facts?JSON.parse(e.facts):[],s=e.concepts?JSON.parse(e.concepts):[],i=e.files_read?JSON.parse(e.files_read):[],n=e.files_modified?JSON.parse(e.files_modified):[],o={sqlite_id:e.id,doc_type:"observation",sdk_session_id:e.sdk_session_id,project:e.project,created_at_epoch:e.created_at_epoch,type:e.type||"discovery",title:e.title||"Untitled"};return e.subtitle&&(o.subtitle=e.subtitle),s.length>0&&(o.concepts=s.join(",")),i.length>0&&(o.files_read=i.join(",")),n.length>0&&(o.files_modified=n.join(",")),e.narrative&&r.push({id:`obs_${e.id}_narrative`,document:e.narrative,metadata:{...o,field_type:"narrative"}}),e.text&&r.push({id:`obs_${e.id}_text`,document:e.text,metadata:{...o,field_type:"text"}}),t.forEach((l,c)=>{r.push({id:`obs_${e.id}_fact_${c}`,document:l,metadata:{...o,field_type:"fact",fact_index:c}})}),r}formatSummaryDocs(e){let r=[],t={sqlite_id:e.id,doc_type:"session_summary",sdk_session_id:e.sdk_session_id,project:e.project,created_at_epoch:e.created_at_epoch,prompt_number:e.prompt_number||0};return e.request&&r.push({id:`summary_${e.id}_request`,document:e.request,metadata:{...t,field_type:"request"}}),e.investigated&&r.push({id:`summary_${e.id}_investigated`,document:e.investigated,metadata:{...t,field_type:"investigated"}}),e.learned&&r.push({id:`summary_${e.id}_learned`,document:e.learned,metadata:{...t,field_type:"learned"}}),e.completed&&r.push({id:`summary_${e.id}_completed`,document:e.completed,metadata:{...t,field_type:"completed"}}),e.next_steps&&r.push({id:`summary_${e.id}_next_steps`,document:e.next_steps,metadata:{...t,field_type:"next_steps"}}),e.notes&&r.push({id:`summary_${e.id}_notes`,document:e.notes,metadata:{...t,field_type:"notes"}}),r}async addDocuments(e){if(e.length!==0){if(await this.ensureCollection(),!this.client)throw new Error("Chroma client not initialized");try{await this.client.callTool({name:"chroma_add_documents",arguments:{collection_name:this.collectionName,documents:e.map(r=>r.document),ids:e.map(r=>r.id),metadatas:e.map(r=>r.metadata)}}),H.debug("CHROMA_SYNC","Documents added",{collection:this.collectionName,count:e.length})}catch(r){throw H.error("CHROMA_SYNC","Failed to add documents",{collection:this.collectionName,count:e.length},r),new Error(`Document add failed: ${r instanceof Error?r.message:String(r)}`)}}}async syncObservation(e,r,t,s,i,n,o=0){let l={id:e,sdk_session_id:r,project:t,text:null,type:s.type,title:s.title,subtitle:s.subtitle,facts:JSON.stringify(s.facts),narrative:s.narrative,concepts:JSON.stringify(s.concepts),files_read:JSON.stringify(s.files_read),files_modified:JSON.stringify(s.files_modified),prompt_number:i,discovery_tokens:o,created_at:new Date(n*1e3).toISOString(),created_at_epoch:n},c=this.formatObservationDocs(l);H.info("CHROMA_SYNC","Syncing observation",{observationId:e,documentCount:c.length,project:t}),await this.addDocuments(c)}async syncSummary(e,r,t,s,i,n,o=0){let l={id:e,sdk_session_id:r,project:t,request:s.request,investigated:s.investigated,learned:s.learned,completed:s.completed,next_steps:s.next_steps,notes:s.notes,prompt_number:i,discovery_tokens:o,created_at:new Date(n*1e3).toISOString(),created_at_epoch:n},c=this.formatSummaryDocs(l);H.info("CHROMA_SYNC","Syncing summary",{summaryId:e,documentCount:c.length,project:t}),await this.addDocuments(c)}formatUserPromptDoc(e){return{id:`prompt_${e.id}`,document:e.prompt_text,metadata:{sqlite_id:e.id,doc_type:"user_prompt",sdk_session_id:e.sdk_session_id,project:e.project,created_at_epoch:e.created_at_epoch,prompt_number:e.prompt_number}}}async syncUserPrompt(e,r,t,s,i,n){let o={id:e,claude_session_id:"",prompt_number:i,prompt_text:s,created_at:new Date(n*1e3).toISOString(),created_at_epoch:n,sdk_session_id:r,project:t},l=this.formatUserPromptDoc(o);H.info("CHROMA_SYNC","Syncing user prompt",{promptId:e,project:t}),await this.addDocuments([l])}async getExistingChromaIds(){if(await this.ensureConnection(),!this.client)throw new Error("Chroma client not initialized");let e=new Set,r=new Set,t=new Set,s=0,i=1e3;for(H.info("CHROMA_SYNC","Fetching existing Chroma document IDs...",{project:this.project});;)try{let o=(await this.client.callTool({name:"chroma_get_documents",arguments:{collection_name:this.collectionName,limit:i,offset:s,where:{project:this.project},include:["metadatas"]}})).content[0];if(o.type!=="text")throw new Error("Unexpected response type from chroma_get_documents");let c=JSON.parse(o.text).metadatas||[];if(c.length===0)break;for(let u of c)u.sqlite_id&&(u.doc_type==="observation"?e.add(u.sqlite_id):u.doc_type==="session_summary"?r.add(u.sqlite_id):u.doc_type==="user_prompt"&&t.add(u.sqlite_id));s+=i,H.debug("CHROMA_SYNC","Fetched batch of existing IDs",{project:this.project,offset:s,batchSize:c.length})}catch(n){throw H.error("CHROMA_SYNC","Failed to fetch existing IDs",{project:this.project},n),n}return H.info("CHROMA_SYNC","Existing IDs fetched",{project:this.project,observations:e.size,summaries:r.size,prompts:t.size}),{observations:e,summaries:r,prompts:t}}async ensureBackfilled(){H.info("CHROMA_SYNC","Starting smart backfill",{project:this.project}),await this.ensureCollection();let e=await this.getExistingChromaIds(),r=new wn;try{let t=Array.from(e.observations),s=t.length>0?`AND id NOT IN (${t.join(",")})`:"",i=r.db.prepare(` + `).all(e)}close(){this.db.close()}};var $E=Nt(require("path"),1),ME=Nt(require("os"),1),Fc=class{client=null;connected=!1;project;collectionName;VECTOR_DB_DIR;BATCH_SIZE=100;constructor(e){this.project=e,this.collectionName=`cm__${e}`,this.VECTOR_DB_DIR=$E.default.join(ME.default.homedir(),".claude-mem","vector-db")}async ensureConnection(){if(!(this.connected&&this.client)){H.info("CHROMA_SYNC","Connecting to Chroma MCP server...",{project:this.project});try{let e=process.env.CLAUDE_MEM_PYTHON_VERSION||"3.13",r=new En({command:"uvx",args:["--python",e,"chroma-mcp","--client-type","persistent","--data-dir",this.VECTOR_DB_DIR],stderr:"ignore"});this.client=new bn({name:"claude-mem-chroma-sync",version:"1.0.0"},{capabilities:{}}),await this.client.connect(r),this.connected=!0,H.info("CHROMA_SYNC","Connected to Chroma MCP server",{project:this.project})}catch(e){throw H.error("CHROMA_SYNC","Failed to connect to Chroma MCP server",{project:this.project},e),new Error(`Chroma connection failed: ${e instanceof Error?e.message:String(e)}`)}}}async ensureCollection(){if(await this.ensureConnection(),!this.client)throw new Error("Chroma client not initialized");try{await this.client.callTool({name:"chroma_get_collection_info",arguments:{collection_name:this.collectionName}}),H.debug("CHROMA_SYNC","Collection exists",{collection:this.collectionName})}catch{H.info("CHROMA_SYNC","Creating collection",{collection:this.collectionName});try{await this.client.callTool({name:"chroma_create_collection",arguments:{collection_name:this.collectionName,embedding_function_name:"default"}}),H.info("CHROMA_SYNC","Collection created",{collection:this.collectionName})}catch(r){throw H.error("CHROMA_SYNC","Failed to create collection",{collection:this.collectionName},r),new Error(`Collection creation failed: ${r instanceof Error?r.message:String(r)}`)}}}formatObservationDocs(e){let r=[],t=e.facts?JSON.parse(e.facts):[],s=e.concepts?JSON.parse(e.concepts):[],i=e.files_read?JSON.parse(e.files_read):[],n=e.files_modified?JSON.parse(e.files_modified):[],o={sqlite_id:e.id,doc_type:"observation",sdk_session_id:e.sdk_session_id,project:e.project,created_at_epoch:e.created_at_epoch,type:e.type||"discovery",title:e.title||"Untitled"};return e.subtitle&&(o.subtitle=e.subtitle),s.length>0&&(o.concepts=s.join(",")),i.length>0&&(o.files_read=i.join(",")),n.length>0&&(o.files_modified=n.join(",")),e.narrative&&r.push({id:`obs_${e.id}_narrative`,document:e.narrative,metadata:{...o,field_type:"narrative"}}),e.text&&r.push({id:`obs_${e.id}_text`,document:e.text,metadata:{...o,field_type:"text"}}),t.forEach((l,c)=>{r.push({id:`obs_${e.id}_fact_${c}`,document:l,metadata:{...o,field_type:"fact",fact_index:c}})}),r}formatSummaryDocs(e){let r=[],t={sqlite_id:e.id,doc_type:"session_summary",sdk_session_id:e.sdk_session_id,project:e.project,created_at_epoch:e.created_at_epoch,prompt_number:e.prompt_number||0};return e.request&&r.push({id:`summary_${e.id}_request`,document:e.request,metadata:{...t,field_type:"request"}}),e.investigated&&r.push({id:`summary_${e.id}_investigated`,document:e.investigated,metadata:{...t,field_type:"investigated"}}),e.learned&&r.push({id:`summary_${e.id}_learned`,document:e.learned,metadata:{...t,field_type:"learned"}}),e.completed&&r.push({id:`summary_${e.id}_completed`,document:e.completed,metadata:{...t,field_type:"completed"}}),e.next_steps&&r.push({id:`summary_${e.id}_next_steps`,document:e.next_steps,metadata:{...t,field_type:"next_steps"}}),e.notes&&r.push({id:`summary_${e.id}_notes`,document:e.notes,metadata:{...t,field_type:"notes"}}),r}async addDocuments(e){if(e.length!==0){if(await this.ensureCollection(),!this.client)throw new Error("Chroma client not initialized");try{await this.client.callTool({name:"chroma_add_documents",arguments:{collection_name:this.collectionName,documents:e.map(r=>r.document),ids:e.map(r=>r.id),metadatas:e.map(r=>r.metadata)}}),H.debug("CHROMA_SYNC","Documents added",{collection:this.collectionName,count:e.length})}catch(r){throw H.error("CHROMA_SYNC","Failed to add documents",{collection:this.collectionName,count:e.length},r),new Error(`Document add failed: ${r instanceof Error?r.message:String(r)}`)}}}async syncObservation(e,r,t,s,i,n,o=0){let l={id:e,sdk_session_id:r,project:t,text:null,type:s.type,title:s.title,subtitle:s.subtitle,facts:JSON.stringify(s.facts),narrative:s.narrative,concepts:JSON.stringify(s.concepts),files_read:JSON.stringify(s.files_read),files_modified:JSON.stringify(s.files_modified),prompt_number:i,discovery_tokens:o,created_at:new Date(n*1e3).toISOString(),created_at_epoch:n},c=this.formatObservationDocs(l);H.info("CHROMA_SYNC","Syncing observation",{observationId:e,documentCount:c.length,project:t}),await this.addDocuments(c)}async syncSummary(e,r,t,s,i,n,o=0){let l={id:e,sdk_session_id:r,project:t,request:s.request,investigated:s.investigated,learned:s.learned,completed:s.completed,next_steps:s.next_steps,notes:s.notes,prompt_number:i,discovery_tokens:o,created_at:new Date(n*1e3).toISOString(),created_at_epoch:n},c=this.formatSummaryDocs(l);H.info("CHROMA_SYNC","Syncing summary",{summaryId:e,documentCount:c.length,project:t}),await this.addDocuments(c)}formatUserPromptDoc(e){return{id:`prompt_${e.id}`,document:e.prompt_text,metadata:{sqlite_id:e.id,doc_type:"user_prompt",sdk_session_id:e.sdk_session_id,project:e.project,created_at_epoch:e.created_at_epoch,prompt_number:e.prompt_number}}}async syncUserPrompt(e,r,t,s,i,n){let o={id:e,claude_session_id:"",prompt_number:i,prompt_text:s,created_at:new Date(n*1e3).toISOString(),created_at_epoch:n,sdk_session_id:r,project:t},l=this.formatUserPromptDoc(o);H.info("CHROMA_SYNC","Syncing user prompt",{promptId:e,project:t}),await this.addDocuments([l])}async getExistingChromaIds(){if(await this.ensureConnection(),!this.client)throw new Error("Chroma client not initialized");let e=new Set,r=new Set,t=new Set,s=0,i=1e3;for(H.info("CHROMA_SYNC","Fetching existing Chroma document IDs...",{project:this.project});;)try{let o=(await this.client.callTool({name:"chroma_get_documents",arguments:{collection_name:this.collectionName,limit:i,offset:s,where:{project:this.project},include:["metadatas"]}})).content[0];if(o.type!=="text")throw new Error("Unexpected response type from chroma_get_documents");let c=JSON.parse(o.text).metadatas||[];if(c.length===0)break;for(let u of c)u.sqlite_id&&(u.doc_type==="observation"?e.add(u.sqlite_id):u.doc_type==="session_summary"?r.add(u.sqlite_id):u.doc_type==="user_prompt"&&t.add(u.sqlite_id));s+=i,H.debug("CHROMA_SYNC","Fetched batch of existing IDs",{project:this.project,offset:s,batchSize:c.length})}catch(n){throw H.error("CHROMA_SYNC","Failed to fetch existing IDs",{project:this.project},n),n}return H.info("CHROMA_SYNC","Existing IDs fetched",{project:this.project,observations:e.size,summaries:r.size,prompts:t.size}),{observations:e,summaries:r,prompts:t}}async ensureBackfilled(){H.info("CHROMA_SYNC","Starting smart backfill",{project:this.project}),await this.ensureCollection();let e=await this.getExistingChromaIds(),r=new wn;try{let t=Array.from(e.observations),s=t.length>0?`AND id NOT IN (${t.join(",")})`:"",i=r.db.prepare(` SELECT * FROM observations WHERE project = ? ${s} ORDER BY id ASC diff --git a/src/servers/search-server.ts b/src/servers/search-server.ts index c00345c8..63c00849 100644 --- a/src/servers/search-server.ts +++ b/src/servers/search-server.ts @@ -2687,9 +2687,11 @@ async function main() { setTimeout(async () => { try { console.error('[search-server] Initializing Chroma client...'); + // Use Python 3.13 by default to avoid onnxruntime compatibility issues with Python 3.14+ + const pythonVersion = process.env.CLAUDE_MEM_PYTHON_VERSION || '3.13'; const chromaTransport = new StdioClientTransport({ command: 'uvx', - args: ['chroma-mcp', '--client-type', 'persistent', '--data-dir', VECTOR_DB_DIR], + args: ['--python', pythonVersion, 'chroma-mcp', '--client-type', 'persistent', '--data-dir', VECTOR_DB_DIR], stderr: 'ignore' }); diff --git a/src/services/sync/ChromaSync.ts b/src/services/sync/ChromaSync.ts index a80dc50d..c1e4b876 100644 --- a/src/services/sync/ChromaSync.ts +++ b/src/services/sync/ChromaSync.ts @@ -94,9 +94,13 @@ export class ChromaSync { logger.info('CHROMA_SYNC', 'Connecting to Chroma MCP server...', { project: this.project }); try { + // Use Python 3.13 by default to avoid onnxruntime compatibility issues with Python 3.14+ + // See: https://github.com/thedotmack/claude-mem/issues/170 (Python 3.14 incompatibility) + const pythonVersion = process.env.CLAUDE_MEM_PYTHON_VERSION || '3.13'; const transport = new StdioClientTransport({ command: 'uvx', args: [ + '--python', pythonVersion, 'chroma-mcp', '--client-type', 'persistent', '--data-dir', this.VECTOR_DB_DIR