diff --git a/plugin/package.json b/plugin/package.json new file mode 100644 index 00000000..057119b9 --- /dev/null +++ b/plugin/package.json @@ -0,0 +1,12 @@ +{ + "name": "claude-mem-plugin", + "version": "7.2.1", + "private": true, + "description": "Runtime dependencies for claude-mem bundled hooks", + "type": "module", + "dependencies": {}, + "engines": { + "node": ">=18.0.0", + "bun": ">=1.0.0" + } +} diff --git a/plugin/scripts/worker-service.cjs b/plugin/scripts/worker-service.cjs index 6c62ea05..1c1b4a94 100755 --- a/plugin/scripts/worker-service.cjs +++ b/plugin/scripts/worker-service.cjs @@ -1032,7 +1032,7 @@ Other tips: `)}formatSessionResult(e){let r=e.request||`Session ${e.sdk_session_id?.substring(0,8)||"unknown"}`,t=[];t.push(`## ${r}`),t.push(`*Source: claude-mem://session/${e.sdk_session_id}*`),t.push(""),e.completed&&(t.push(`**Completed:** ${e.completed}`),t.push("")),e.learned&&(t.push(`**Learned:** ${e.learned}`),t.push("")),e.investigated&&(t.push(`**Investigated:** ${e.investigated}`),t.push("")),e.next_steps&&(t.push(`**Next Steps:** ${e.next_steps}`),t.push("")),e.notes&&(t.push(`**Notes:** ${e.notes}`),t.push(""));let s=[];if(e.files_read||e.files_edited){let n=[];if(e.files_read)try{n.push(...JSON.parse(e.files_read))}catch{M.warn("FORMAT","Invalid JSON in session files_read field",{sessionId:e.sdk_session_id})}if(e.files_edited)try{n.push(...JSON.parse(e.files_edited))}catch{M.warn("FORMAT","Invalid JSON in session files_edited field",{sessionId:e.sdk_session_id})}n.length>0&&s.push(`Files: ${[...new Set(n)].join(", ")}`)}let i=new Date(e.created_at_epoch).toLocaleDateString();return s.push(`Date: ${i}`),s.length>0&&(t.push("---"),t.push(s.join(" | "))),t.join(` `)}formatUserPromptResult(e){let r=[];r.push(`## User Prompt #${e.prompt_number}`),r.push(`*Source: claude-mem://user-prompt/${e.id}*`),r.push(""),r.push(e.prompt_text),r.push(""),r.push("---");let t=new Date(e.created_at_epoch).toLocaleString();return r.push(`Date: ${t}`),r.join(` `)}};var Il=class{buildTimeline(e){let r=[...e.observations.map(t=>({type:"observation",data:t,epoch:t.created_at_epoch})),...e.sessions.map(t=>({type:"session",data:t,epoch:t.created_at_epoch})),...e.prompts.map(t=>({type:"prompt",data:t,epoch:t.created_at_epoch}))];return r.sort((t,s)=>t.epoch-s.epoch),r}filterByDepth(e,r,t,s,i){if(e.length===0)return e;let n=-1;if(typeof r=="number")n=e.findIndex(c=>c.type==="observation"&&c.data.id===r);else if(typeof r=="string"&&r.startsWith("S")){let c=parseInt(r.slice(1),10);n=e.findIndex(u=>u.type==="session"&&u.data.id===c)}else n=e.findIndex(c=>c.epoch>=t),n===-1&&(n=e.length-1);if(n===-1)return e;let o=Math.max(0,n-s),l=Math.min(e.length,n+i+1);return e.slice(o,l)}formatTimeline(e,r,t,s,i){if(e.length===0)return t?`Found observation matching "${t}", but no timeline context available.`:"No timeline items found";let n=[];if(t&&r){let c=e.find(p=>p.type==="observation"&&p.data.id===r),u=c?c.data.title||"Untitled":"Unknown";n.push(`# Timeline for query: "${t}"`),n.push(`**Anchor:** Observation #${r} - ${u}`)}else r?n.push(`# Timeline around anchor: ${r}`):n.push("# Timeline");s!==void 0&&i!==void 0?n.push(`**Window:** ${s} records before \u2192 ${i} records after | **Items:** ${e.length}`):n.push(`**Items:** ${e.length}`),n.push(""),n.push("**Legend:** \u{1F3AF} session-request | \u{1F534} bugfix | \u{1F7E3} feature | \u{1F504} refactor | \u2705 change | \u{1F535} discovery | \u{1F9E0} decision"),n.push("");let o=new Map;for(let c of e){let u=this.formatDate(c.epoch);o.has(u)||o.set(u,[]),o.get(u).push(c)}let l=Array.from(o.entries()).sort((c,u)=>{let p=new Date(c[0]).getTime(),f=new Date(u[0]).getTime();return p-f});for(let[c,u]of l){n.push(`### ${c}`),n.push("");let p=null,f="",d=!1;for(let v of u){let h=typeof r=="number"&&v.type==="observation"&&v.data.id===r||typeof r=="string"&&r.startsWith("S")&&v.type==="session"&&`S${v.data.id}`===r;if(v.type==="session"){d&&(n.push(""),d=!1,p=null,f="");let m=v.data,y=m.request||"Session summary",g=`claude-mem://session-summary/${m.id}`,b=h?" \u2190 **ANCHOR**":"";n.push(`**\u{1F3AF} #S${m.id}** ${y} (${this.formatDateTime(v.epoch)}) [\u2192](${g})${b}`),n.push("")}else if(v.type==="prompt"){d&&(n.push(""),d=!1,p=null,f="");let m=v.data,y=m.prompt_text.length>100?m.prompt_text.substring(0,100)+"...":m.prompt_text;n.push(`**\u{1F4AC} User Prompt #${m.prompt_number}** (${this.formatDateTime(v.epoch)})`),n.push(`> ${y}`),n.push("")}else if(v.type==="observation"){let m=v.data,y="General";y!==p&&(d&&n.push(""),n.push(`**${y}**`),n.push("| ID | Time | T | Title | Tokens |"),n.push("|----|------|---|-------|--------|"),p=y,d=!0,f="");let g=this.getTypeIcon(m.type),b=this.formatTime(v.epoch),w=m.title||"Untitled",P=this.estimateTokens(m.narrative),k=b!==f?b:"\u2033";f=b;let N=h?" \u2190 **ANCHOR**":"";n.push(`| #${m.id} | ${k} | ${g} | ${w}${N} | ~${P} |`)}}d&&n.push("")}return n.join(` -`)}getTypeIcon(e){switch(e){case"bugfix":return"\u{1F534}";case"feature":return"\u{1F7E3}";case"refactor":return"\u{1F504}";case"change":return"\u2705";case"discovery":return"\u{1F535}";case"decision":return"\u{1F9E0}";default:return"\u2022"}}formatDate(e){return new Date(e).toLocaleString("en-US",{month:"short",day:"numeric",year:"numeric"})}formatTime(e){return new Date(e).toLocaleString("en-US",{hour:"numeric",minute:"2-digit",hour12:!0})}formatDateTime(e){return new Date(e).toLocaleString("en-US",{month:"short",day:"numeric",hour:"numeric",minute:"2-digit",hour12:!0})}estimateTokens(e){return e?Math.ceil(e.length/4):0}};var Dl=class{constructor(e,r){this.sseBroadcaster=e;this.workerService=r}broadcastNewPrompt(e){this.sseBroadcaster.broadcast({type:"new_prompt",prompt:e}),this.sseBroadcaster.broadcast({type:"processing_status",isProcessing:!0}),this.workerService.broadcastProcessingStatus()}broadcastSessionStarted(e,r){this.sseBroadcaster.broadcast({type:"session_started",sessionDbId:e,project:r}),this.workerService.broadcastProcessingStatus()}broadcastObservationQueued(e){this.sseBroadcaster.broadcast({type:"observation_queued",sessionDbId:e}),this.workerService.broadcastProcessingStatus()}broadcastSessionCompleted(e){this.sseBroadcaster.broadcast({type:"session_completed",timestamp:Date.now(),sessionDbId:e}),this.workerService.broadcastProcessingStatus()}broadcastSummarizeQueued(){this.workerService.broadcastProcessingStatus()}};var kf=wt(sd(),1),Jw=wt(Yw(),1),e1=wt(require("path"),1);Ar();ft();function t1(a){let e=[];e.push(kf.default.json({limit:"50mb"})),e.push((0,Jw.default)()),e.push((s,i,n)=>{if(s.path.startsWith("/health")||s.path==="/"||s.path.includes("."))return n();let o=Date.now(),l=`${s.method}-${Date.now()}`,c=a(s.method,s.path,s.body);M.info("HTTP",`\u2192 ${s.method} ${s.path}`,{requestId:l},c);let u=i.send.bind(i);i.send=function(p){let f=Date.now()-o;return M.info("HTTP",`\u2190 ${i.statusCode} ${s.path}`,{requestId:l,duration:`${f}ms`}),u(p)},n()});let r=ca(),t=e1.default.join(r,"plugin","ui");return e.push(kf.default.static(t)),e}function r1(a,e,r){if(!r||Object.keys(r).length===0||e.includes("/init"))return"";if(e.includes("/observations")){let t=r.tool_name||"?",s=r.tool_input;return`tool=${M.formatTool(t,s)}`}return e.includes("/summarize")?"requesting summary":""}var a1=wt(require("path"),1),s1=require("fs");Ar();ft();var _r=class{wrapHandler(e){return(r,t)=>{try{let s=e(r,t);s instanceof Promise&&s.catch(i=>this.handleError(t,i))}catch(s){this.handleError(t,s)}}}parseIntParam(e,r,t){let s=parseInt(e.params[t],10);return isNaN(s)?(this.badRequest(r,`Invalid ${t}`),null):s}validateRequired(e,r,t){for(let s of t)if(e.body[s]===void 0||e.body[s]===null)return this.badRequest(r,`Missing ${s}`),!1;return!0}badRequest(e,r){e.status(400).json({error:r})}notFound(e,r){e.status(404).json({error:r})}handleError(e,r,t){M.failure("WORKER",t||"Request failed",{},r),e.status(500).json({error:r.message})}};var Nl=class extends _r{constructor(r,t,s){super();this.sseBroadcaster=r;this.dbManager=t;this.sessionManager=s}setupRoutes(r){r.get("/health",this.handleHealth.bind(this)),r.get("/",this.handleViewerUI.bind(this)),r.get("/stream",this.handleSSEStream.bind(this))}handleHealth=this.wrapHandler((r,t)=>{t.json({status:"ok",timestamp:Date.now()})});handleViewerUI=this.wrapHandler((r,t)=>{let s=ca(),i=a1.default.join(s,"plugin","ui","viewer.html"),n=(0,s1.readFileSync)(i,"utf-8");t.setHeader("Content-Type","text/html"),t.send(n)});handleSSEStream=this.wrapHandler((r,t)=>{t.setHeader("Content-Type","text/event-stream"),t.setHeader("Cache-Control","no-cache"),t.setHeader("Connection","keep-alive"),this.sseBroadcaster.addClient(t);let s=this.dbManager.getSessionStore().getAllProjects();this.sseBroadcaster.broadcast({type:"initial_load",projects:s,timestamp:Date.now()});let i=this.sessionManager.isAnySessionProcessing(),n=this.sessionManager.getTotalActiveWork();this.sseBroadcaster.broadcast({type:"processing_status",isProcessing:i,queueDepth:n})})};ft();var jl=100;function n1(a){let e=(a.match(//g)||[]).length,r=(a.match(//g)||[]).length;return e+r}function Af(a){if(typeof a!="string")return vr("[tag-stripping] received non-string for JSON context:",{type:typeof a}),"{}";let e=n1(a);return e>jl&&vr("[tag-stripping] tag count exceeds limit, truncating:",{tagCount:e,maxAllowed:jl,contentLength:a.length}),a.replace(/[\s\S]*?<\/claude-mem-context>/g,"").replace(/[\s\S]*?<\/private>/g,"").trim()}function i1(a){if(typeof a!="string")return vr("[tag-stripping] received non-string for prompt context:",{type:typeof a}),"";let e=n1(a);return e>jl&&vr("[tag-stripping] tag count exceeds limit, truncating:",{tagCount:e,maxAllowed:jl,contentLength:a.length}),a.replace(/[\s\S]*?<\/claude-mem-context>/g,"").replace(/[\s\S]*?<\/private>/g,"").trim()}var $l=class{constructor(e,r,t){this.sessionManager=e;this.dbManager=r;this.eventBroadcaster=t}async completeByDbId(e){await this.sessionManager.deleteSession(e),this.dbManager.markSessionComplete(e),this.eventBroadcaster.broadcastSessionCompleted(e)}async completeByClaudeId(e){let t=this.dbManager.getSessionStore().findActiveSDKSession(e);if(!t)return!1;let s=t.id;return await this.completeByDbId(s),!0}};ft();var ao=class{static checkUserPromptPrivacy(e,r,t,s,i,n){let o=e.getUserPrompt(r,t);return!o||o.trim()===""?(M.debug("HOOK",`Skipping ${s} - user prompt was entirely private`,{sessionId:i,promptNumber:t,...n}),null):o}};oa();Ar();var Ml=class extends _r{constructor(r,t,s,i,n){super();this.sessionManager=r;this.dbManager=t;this.sdkAgent=s;this.eventBroadcaster=i;this.workerService=n;this.completionHandler=new $l(r,t,i)}completionHandler;ensureGeneratorRunning(r,t){let s=this.sessionManager.getSession(r);s&&!s.generatorPromise&&(M.info("SESSION",`Generator auto-starting (${t})`,{sessionId:r,queueDepth:s.pendingMessages.length}),s.generatorPromise=this.sdkAgent.startSession(s,this.workerService).catch(i=>{M.failure("SDK","SDK agent error",{sessionId:r},i)}).finally(()=>{M.info("SESSION","Generator finished",{sessionId:r}),s.generatorPromise=null,this.workerService.broadcastProcessingStatus()}))}setupRoutes(r){r.post("/sessions/:sessionDbId/init",this.handleSessionInit.bind(this)),r.post("/sessions/:sessionDbId/observations",this.handleObservations.bind(this)),r.post("/sessions/:sessionDbId/summarize",this.handleSummarize.bind(this)),r.get("/sessions/:sessionDbId/status",this.handleSessionStatus.bind(this)),r.delete("/sessions/:sessionDbId",this.handleSessionDelete.bind(this)),r.post("/sessions/:sessionDbId/complete",this.handleSessionComplete.bind(this)),r.post("/api/sessions/init",this.handleSessionInitByClaudeId.bind(this)),r.post("/api/sessions/observations",this.handleObservationsByClaudeId.bind(this)),r.post("/api/sessions/summarize",this.handleSummarizeByClaudeId.bind(this)),r.post("/api/sessions/complete",this.handleSessionCompleteByClaudeId.bind(this))}handleSessionInit=this.wrapHandler((r,t)=>{let s=this.parseIntParam(r,t,"sessionDbId");if(s===null)return;let{userPrompt:i,promptNumber:n}=r.body,o=this.sessionManager.initializeSession(s,i,n),l=this.dbManager.getSessionStore().getLatestUserPrompt(o.claudeSessionId);if(l){this.eventBroadcaster.broadcastNewPrompt({id:l.id,claude_session_id:l.claude_session_id,project:l.project,prompt_number:l.prompt_number,prompt_text:l.prompt_text,created_at_epoch:l.created_at_epoch});let c=Date.now(),u=l.prompt_text;this.dbManager.getChromaSync().syncUserPrompt(l.id,l.sdk_session_id,l.project,u,l.prompt_number,l.created_at_epoch).then(()=>{let p=Date.now()-c,f=u.length>60?u.substring(0,60)+"...":u;M.debug("CHROMA","User prompt synced",{promptId:l.id,duration:`${p}ms`,prompt:f})}).catch(p=>{M.error("CHROMA","Failed to sync user_prompt",{promptId:l.id,sessionId:s},p)})}M.info("SESSION","Generator starting",{sessionId:s,project:o.project,promptNum:o.lastPromptNumber}),o.generatorPromise=this.sdkAgent.startSession(o,this.workerService).catch(c=>{M.failure("SDK","SDK agent error",{sessionId:s},c)}).finally(()=>{M.info("SESSION","Generator finished",{sessionId:s}),o.generatorPromise=null,this.workerService.broadcastProcessingStatus()}),this.eventBroadcaster.broadcastSessionStarted(s,o.project),t.json({status:"initialized",sessionDbId:s,port:Fn()})});handleObservations=this.wrapHandler((r,t)=>{let s=this.parseIntParam(r,t,"sessionDbId");if(s===null)return;let{tool_name:i,tool_input:n,tool_response:o,prompt_number:l,cwd:c}=r.body;this.sessionManager.queueObservation(s,{tool_name:i,tool_input:n,tool_response:o,prompt_number:l,cwd:c}),this.ensureGeneratorRunning(s,"observation"),this.eventBroadcaster.broadcastObservationQueued(s),t.json({status:"queued"})});handleSummarize=this.wrapHandler((r,t)=>{let s=this.parseIntParam(r,t,"sessionDbId");if(s===null)return;let{last_user_message:i,last_assistant_message:n}=r.body;this.sessionManager.queueSummarize(s,i,n),this.ensureGeneratorRunning(s,"summarize"),this.eventBroadcaster.broadcastSummarizeQueued(),t.json({status:"queued"})});handleSessionStatus=this.wrapHandler((r,t)=>{let s=this.parseIntParam(r,t,"sessionDbId");if(s===null)return;let i=this.sessionManager.getSession(s);if(!i){t.json({status:"not_found"});return}t.json({status:"active",sessionDbId:s,project:i.project,queueLength:i.pendingMessages.length,uptime:Date.now()-i.startTime})});handleSessionDelete=this.wrapHandler(async(r,t)=>{let s=this.parseIntParam(r,t,"sessionDbId");s!==null&&(await this.completionHandler.completeByDbId(s),t.json({status:"deleted"}))});handleSessionComplete=this.wrapHandler(async(r,t)=>{let s=this.parseIntParam(r,t,"sessionDbId");s!==null&&(await this.completionHandler.completeByDbId(s),t.json({success:!0}))});handleObservationsByClaudeId=this.wrapHandler((r,t)=>{let{claudeSessionId:s,tool_name:i,tool_input:n,tool_response:o,cwd:l}=r.body;if(!s)return this.badRequest(t,"Missing claudeSessionId");let c=lt.loadFromFile(Ln);if(new Set(c.CLAUDE_MEM_SKIP_TOOLS.split(",").map(g=>g.trim()).filter(Boolean)).has(i)){M.debug("SESSION","Skipping observation for tool",{tool_name:i}),t.json({status:"skipped",reason:"tool_excluded"});return}if(new Set(["Edit","Write","Read","NotebookEdit"]).has(i)&&n)try{let g=n.file_path||n.notebook_path;if(g&&g.includes("session-memory")){M.debug("SESSION","Skipping meta-observation for session-memory file",{tool_name:i,file_path:g}),t.json({status:"skipped",reason:"session_memory_meta"});return}}catch(g){M.debug("SESSION","Could not check file_path for session-memory filter",{tool_name:i},g)}let f=this.dbManager.getSessionStore(),d=f.createSDKSession(s,"",""),v=f.getPromptCounter(d);if(!ao.checkUserPromptPrivacy(f,s,v,"observation",d,{tool_name:i})){t.json({status:"skipped",reason:"private"});return}let m="{}",y="{}";try{m=n!==void 0?Af(JSON.stringify(n)):"{}"}catch(g){M.debug("SESSION","Failed to serialize tool_input",{sessionDbId:d},g),m='{"error": "Failed to serialize tool_input"}'}try{y=o!==void 0?Af(JSON.stringify(o)):"{}"}catch(g){M.debug("SESSION","Failed to serialize tool_result",{sessionDbId:d},g),y='{"error": "Failed to serialize tool_response"}'}this.sessionManager.queueObservation(d,{tool_name:i,tool_input:m,tool_response:y,prompt_number:v,cwd:l||vr("Missing cwd when queueing observation in SessionRoutes",{sessionDbId:d,tool_name:i},"")}),this.ensureGeneratorRunning(d,"observation"),this.eventBroadcaster.broadcastObservationQueued(d),t.json({status:"queued"})});handleSummarizeByClaudeId=this.wrapHandler((r,t)=>{let{claudeSessionId:s,last_user_message:i,last_assistant_message:n}=r.body;if(!s)return this.badRequest(t,"Missing claudeSessionId");let o=this.dbManager.getSessionStore(),l=o.createSDKSession(s,"",""),c=o.getPromptCounter(l);if(!ao.checkUserPromptPrivacy(o,s,c,"summarize",l)){t.json({status:"skipped",reason:"private"});return}this.sessionManager.queueSummarize(l,i||vr("Missing last_user_message when queueing summary in SessionRoutes",{sessionDbId:l},""),n),this.ensureGeneratorRunning(l,"summarize"),this.eventBroadcaster.broadcastSummarizeQueued(),t.json({status:"queued"})});handleSessionCompleteByClaudeId=this.wrapHandler(async(r,t)=>{let{claudeSessionId:s}=r.body;if(!s)return this.badRequest(t,"Missing claudeSessionId");if(!await this.completionHandler.completeByClaudeId(s)){t.json({success:!0,message:"No active session found"});return}t.json({success:!0})});handleSessionInitByClaudeId=this.wrapHandler((r,t)=>{let{claudeSessionId:s,project:i,prompt:n}=r.body;if(!this.validateRequired(r,t,["claudeSessionId","project","prompt"]))return;let o=this.dbManager.getSessionStore(),l=o.createSDKSession(s,i,n),c=o.incrementPromptCounter(l),u=i1(n);if(!u||u.trim()===""){M.debug("HOOK","Session init - prompt entirely private",{sessionId:l,promptNumber:c,originalLength:n.length}),t.json({sessionDbId:l,promptNumber:c,skipped:!0,reason:"private"});return}o.saveUserPrompt(s,c,u),M.info("SESSION","Session initialized via HTTP",{sessionId:l,promptNumber:c,project:i}),t.json({sessionDbId:l,promptNumber:c,skipped:!1})})};var If=wt(require("path"),1),Jn=require("fs"),o1=require("os");Ar();var Ll=class extends _r{constructor(r,t,s,i,n,o){super();this.paginationHelper=r;this.dbManager=t;this.sessionManager=s;this.sseBroadcaster=i;this.workerService=n;this.startTime=o}setupRoutes(r){r.get("/api/observations",this.handleGetObservations.bind(this)),r.get("/api/summaries",this.handleGetSummaries.bind(this)),r.get("/api/prompts",this.handleGetPrompts.bind(this)),r.get("/api/observation/:id",this.handleGetObservationById.bind(this)),r.get("/api/session/:id",this.handleGetSessionById.bind(this)),r.get("/api/prompt/:id",this.handleGetPromptById.bind(this)),r.get("/api/stats",this.handleGetStats.bind(this)),r.get("/api/projects",this.handleGetProjects.bind(this)),r.get("/api/processing-status",this.handleGetProcessingStatus.bind(this)),r.post("/api/processing",this.handleSetProcessing.bind(this))}handleGetObservations=this.wrapHandler((r,t)=>{let{offset:s,limit:i,project:n}=this.parsePaginationParams(r),o=this.paginationHelper.getObservations(s,i,n);t.json(o)});handleGetSummaries=this.wrapHandler((r,t)=>{let{offset:s,limit:i,project:n}=this.parsePaginationParams(r),o=this.paginationHelper.getSummaries(s,i,n);t.json(o)});handleGetPrompts=this.wrapHandler((r,t)=>{let{offset:s,limit:i,project:n}=this.parsePaginationParams(r),o=this.paginationHelper.getPrompts(s,i,n);t.json(o)});handleGetObservationById=this.wrapHandler((r,t)=>{let s=this.parseIntParam(r,t,"id");if(s===null)return;let n=this.dbManager.getSessionStore().getObservationById(s);if(!n){this.notFound(t,`Observation #${s} not found`);return}t.json(n)});handleGetSessionById=this.wrapHandler((r,t)=>{let s=this.parseIntParam(r,t,"id");if(s===null)return;let n=this.dbManager.getSessionStore().getSessionSummariesByIds([s]);if(n.length===0){this.notFound(t,`Session #${s} not found`);return}t.json(n[0])});handleGetPromptById=this.wrapHandler((r,t)=>{let s=this.parseIntParam(r,t,"id");if(s===null)return;let n=this.dbManager.getSessionStore().getUserPromptsByIds([s]);if(n.length===0){this.notFound(t,`Prompt #${s} not found`);return}t.json(n[0])});handleGetStats=this.wrapHandler((r,t)=>{let s=this.dbManager.getSessionStore().db,i=ca(),n=If.default.join(i,"package.json"),l=JSON.parse((0,Jn.readFileSync)(n,"utf-8")).version,c=s.prepare("SELECT COUNT(*) as count FROM observations").get(),u=s.prepare("SELECT COUNT(*) as count FROM sdk_sessions").get(),p=s.prepare("SELECT COUNT(*) as count FROM session_summaries").get(),f=If.default.join((0,o1.homedir)(),".claude-mem","claude-mem.db"),d=0;(0,Jn.existsSync)(f)&&(d=(0,Jn.statSync)(f).size);let v=Math.floor((Date.now()-this.startTime)/1e3),h=this.sessionManager.getActiveSessionCount(),m=this.sseBroadcaster.getClientCount();t.json({worker:{version:l,uptime:v,activeSessions:h,sseClients:m,port:Fn()},database:{path:f,size:d,observations:c.count,sessions:u.count,summaries:p.count}})});handleGetProjects=this.wrapHandler((r,t)=>{let n=this.dbManager.getSessionStore().db.prepare(` +`)}getTypeIcon(e){switch(e){case"bugfix":return"\u{1F534}";case"feature":return"\u{1F7E3}";case"refactor":return"\u{1F504}";case"change":return"\u2705";case"discovery":return"\u{1F535}";case"decision":return"\u{1F9E0}";default:return"\u2022"}}formatDate(e){return new Date(e).toLocaleString("en-US",{month:"short",day:"numeric",year:"numeric"})}formatTime(e){return new Date(e).toLocaleString("en-US",{hour:"numeric",minute:"2-digit",hour12:!0})}formatDateTime(e){return new Date(e).toLocaleString("en-US",{month:"short",day:"numeric",hour:"numeric",minute:"2-digit",hour12:!0})}estimateTokens(e){return e?Math.ceil(e.length/4):0}};var Dl=class{constructor(e,r){this.sseBroadcaster=e;this.workerService=r}broadcastNewPrompt(e){this.sseBroadcaster.broadcast({type:"new_prompt",prompt:e}),this.sseBroadcaster.broadcast({type:"processing_status",isProcessing:!0}),this.workerService.broadcastProcessingStatus()}broadcastSessionStarted(e,r){this.sseBroadcaster.broadcast({type:"session_started",sessionDbId:e,project:r}),this.workerService.broadcastProcessingStatus()}broadcastObservationQueued(e){this.sseBroadcaster.broadcast({type:"observation_queued",sessionDbId:e}),this.workerService.broadcastProcessingStatus()}broadcastSessionCompleted(e){this.sseBroadcaster.broadcast({type:"session_completed",timestamp:Date.now(),sessionDbId:e}),this.workerService.broadcastProcessingStatus()}broadcastSummarizeQueued(){this.workerService.broadcastProcessingStatus()}};var kf=wt(sd(),1),Jw=wt(Yw(),1),e1=wt(require("path"),1);Ar();ft();function t1(a){let e=[];e.push(kf.default.json({limit:"50mb"})),e.push((0,Jw.default)()),e.push((s,i,n)=>{if(s.path.startsWith("/health")||s.path==="/"||s.path.includes("."))return n();let o=Date.now(),l=`${s.method}-${Date.now()}`,c=a(s.method,s.path,s.body);M.info("HTTP",`\u2192 ${s.method} ${s.path}`,{requestId:l},c);let u=i.send.bind(i);i.send=function(p){let f=Date.now()-o;return M.info("HTTP",`\u2190 ${i.statusCode} ${s.path}`,{requestId:l,duration:`${f}ms`}),u(p)},n()});let r=ca(),t=e1.default.join(r,"plugin","ui");return e.push(kf.default.static(t)),e}function r1(a,e,r){if(!r||Object.keys(r).length===0||e.includes("/init"))return"";if(e.includes("/observations")){let t=r.tool_name||"?",s=r.tool_input;return`tool=${M.formatTool(t,s)}`}return e.includes("/summarize")?"requesting summary":""}var a1=wt(require("path"),1),s1=require("fs");Ar();ft();var _r=class{wrapHandler(e){return(r,t)=>{try{let s=e(r,t);s instanceof Promise&&s.catch(i=>this.handleError(t,i))}catch(s){this.handleError(t,s)}}}parseIntParam(e,r,t){let s=parseInt(e.params[t],10);return isNaN(s)?(this.badRequest(r,`Invalid ${t}`),null):s}validateRequired(e,r,t){for(let s of t)if(e.body[s]===void 0||e.body[s]===null)return this.badRequest(r,`Missing ${s}`),!1;return!0}badRequest(e,r){e.status(400).json({error:r})}notFound(e,r){e.status(404).json({error:r})}handleError(e,r,t){M.failure("WORKER",t||"Request failed",{},r),e.status(500).json({error:r.message})}};var Nl=class extends _r{constructor(r,t,s){super();this.sseBroadcaster=r;this.dbManager=t;this.sessionManager=s}setupRoutes(r){r.get("/health",this.handleHealth.bind(this)),r.get("/",this.handleViewerUI.bind(this)),r.get("/stream",this.handleSSEStream.bind(this))}handleHealth=this.wrapHandler((r,t)=>{t.json({status:"ok",timestamp:Date.now()})});handleViewerUI=this.wrapHandler((r,t)=>{let s=ca(),i=a1.default.join(s,"plugin","ui","viewer.html"),n=(0,s1.readFileSync)(i,"utf-8");t.setHeader("Content-Type","text/html"),t.send(n)});handleSSEStream=this.wrapHandler((r,t)=>{t.setHeader("Content-Type","text/event-stream"),t.setHeader("Cache-Control","no-cache"),t.setHeader("Connection","keep-alive"),this.sseBroadcaster.addClient(t);let s=this.dbManager.getSessionStore().getAllProjects();this.sseBroadcaster.broadcast({type:"initial_load",projects:s,timestamp:Date.now()});let i=this.sessionManager.isAnySessionProcessing(),n=this.sessionManager.getTotalActiveWork();this.sseBroadcaster.broadcast({type:"processing_status",isProcessing:i,queueDepth:n})})};ft();var jl=100;function n1(a){let e=(a.match(//g)||[]).length,r=(a.match(//g)||[]).length;return e+r}function Af(a){if(typeof a!="string")return vr("[tag-stripping] received non-string for JSON context:",{type:typeof a}),"{}";let e=n1(a);return e>jl&&vr("[tag-stripping] tag count exceeds limit, truncating:",{tagCount:e,maxAllowed:jl,contentLength:a.length}),a.replace(/[\s\S]*?<\/claude-mem-context>/g,"").replace(/[\s\S]*?<\/private>/g,"").trim()}function i1(a){if(typeof a!="string")return vr("[tag-stripping] received non-string for prompt context:",{type:typeof a}),"";let e=n1(a);return e>jl&&vr("[tag-stripping] tag count exceeds limit, truncating:",{tagCount:e,maxAllowed:jl,contentLength:a.length}),a.replace(/[\s\S]*?<\/claude-mem-context>/g,"").replace(/[\s\S]*?<\/private>/g,"").trim()}var $l=class{constructor(e,r,t){this.sessionManager=e;this.dbManager=r;this.eventBroadcaster=t}async completeByDbId(e){await this.sessionManager.deleteSession(e),this.dbManager.markSessionComplete(e),this.eventBroadcaster.broadcastSessionCompleted(e)}async completeByClaudeId(e){let t=this.dbManager.getSessionStore().findActiveSDKSession(e);if(!t)return!1;let s=t.id;return await this.completeByDbId(s),!0}};ft();var ao=class{static checkUserPromptPrivacy(e,r,t,s,i,n){let o=e.getUserPrompt(r,t);return!o||o.trim()===""?(M.debug("HOOK",`Skipping ${s} - user prompt was entirely private`,{sessionId:i,promptNumber:t,...n}),null):o}};oa();Ar();var Ml=class extends _r{constructor(r,t,s,i,n){super();this.sessionManager=r;this.dbManager=t;this.sdkAgent=s;this.eventBroadcaster=i;this.workerService=n;this.completionHandler=new $l(r,t,i)}completionHandler;ensureGeneratorRunning(r,t){let s=this.sessionManager.getSession(r);s&&!s.generatorPromise&&(M.info("SESSION",`Generator auto-starting (${t})`,{sessionId:r,queueDepth:s.pendingMessages.length}),s.generatorPromise=this.sdkAgent.startSession(s,this.workerService).catch(i=>{M.failure("SDK","SDK agent error",{sessionId:r},i)}).finally(()=>{M.info("SESSION","Generator finished",{sessionId:r}),s.generatorPromise=null,this.workerService.broadcastProcessingStatus()}))}setupRoutes(r){r.post("/sessions/:sessionDbId/init",this.handleSessionInit.bind(this)),r.post("/sessions/:sessionDbId/observations",this.handleObservations.bind(this)),r.post("/sessions/:sessionDbId/summarize",this.handleSummarize.bind(this)),r.get("/sessions/:sessionDbId/status",this.handleSessionStatus.bind(this)),r.delete("/sessions/:sessionDbId",this.handleSessionDelete.bind(this)),r.post("/sessions/:sessionDbId/complete",this.handleSessionComplete.bind(this)),r.post("/api/sessions/init",this.handleSessionInitByClaudeId.bind(this)),r.post("/api/sessions/observations",this.handleObservationsByClaudeId.bind(this)),r.post("/api/sessions/summarize",this.handleSummarizeByClaudeId.bind(this)),r.post("/api/sessions/complete",this.handleSessionCompleteByClaudeId.bind(this))}handleSessionInit=this.wrapHandler((r,t)=>{let s=this.parseIntParam(r,t,"sessionDbId");if(s===null)return;let{userPrompt:i,promptNumber:n}=r.body,o=this.sessionManager.initializeSession(s,i,n),l=this.dbManager.getSessionStore().getLatestUserPrompt(o.claudeSessionId);if(l){this.eventBroadcaster.broadcastNewPrompt({id:l.id,claude_session_id:l.claude_session_id,project:l.project,prompt_number:l.prompt_number,prompt_text:l.prompt_text,created_at_epoch:l.created_at_epoch});let c=Date.now(),u=l.prompt_text;this.dbManager.getChromaSync().syncUserPrompt(l.id,l.sdk_session_id,l.project,u,l.prompt_number,l.created_at_epoch).then(()=>{let p=Date.now()-c,f=u.length>60?u.substring(0,60)+"...":u;M.debug("CHROMA","User prompt synced",{promptId:l.id,duration:`${p}ms`,prompt:f})}).catch(p=>{M.error("CHROMA","Failed to sync user_prompt",{promptId:l.id,sessionId:s},p)})}M.info("SESSION","Generator starting",{sessionId:s,project:o.project,promptNum:o.lastPromptNumber}),o.generatorPromise=this.sdkAgent.startSession(o,this.workerService).catch(c=>{M.failure("SDK","SDK agent error",{sessionId:s},c)}).finally(()=>{M.info("SESSION","Generator finished",{sessionId:s}),o.generatorPromise=null,this.workerService.broadcastProcessingStatus()}),this.eventBroadcaster.broadcastSessionStarted(s,o.project),t.json({status:"initialized",sessionDbId:s,port:Fn()})});handleObservations=this.wrapHandler((r,t)=>{let s=this.parseIntParam(r,t,"sessionDbId");if(s===null)return;let{tool_name:i,tool_input:n,tool_response:o,prompt_number:l,cwd:c}=r.body;this.sessionManager.queueObservation(s,{tool_name:i,tool_input:n,tool_response:o,prompt_number:l,cwd:c}),this.ensureGeneratorRunning(s,"observation"),this.eventBroadcaster.broadcastObservationQueued(s),t.json({status:"queued"})});handleSummarize=this.wrapHandler((r,t)=>{let s=this.parseIntParam(r,t,"sessionDbId");if(s===null)return;let{last_user_message:i,last_assistant_message:n}=r.body;this.sessionManager.queueSummarize(s,i,n),this.ensureGeneratorRunning(s,"summarize"),this.eventBroadcaster.broadcastSummarizeQueued(),t.json({status:"queued"})});handleSessionStatus=this.wrapHandler((r,t)=>{let s=this.parseIntParam(r,t,"sessionDbId");if(s===null)return;let i=this.sessionManager.getSession(s);if(!i){t.json({status:"not_found"});return}t.json({status:"active",sessionDbId:s,project:i.project,queueLength:i.pendingMessages.length,uptime:Date.now()-i.startTime})});handleSessionDelete=this.wrapHandler(async(r,t)=>{let s=this.parseIntParam(r,t,"sessionDbId");s!==null&&(await this.completionHandler.completeByDbId(s),t.json({status:"deleted"}))});handleSessionComplete=this.wrapHandler(async(r,t)=>{let s=this.parseIntParam(r,t,"sessionDbId");s!==null&&(await this.completionHandler.completeByDbId(s),t.json({success:!0}))});handleObservationsByClaudeId=this.wrapHandler((r,t)=>{let{claudeSessionId:s,tool_name:i,tool_input:n,tool_response:o,cwd:l}=r.body;if(!s)return this.badRequest(t,"Missing claudeSessionId");let c=lt.loadFromFile(Ln);if(new Set(c.CLAUDE_MEM_SKIP_TOOLS.split(",").map(g=>g.trim()).filter(Boolean)).has(i)){M.debug("SESSION","Skipping observation for tool",{tool_name:i}),t.json({status:"skipped",reason:"tool_excluded"});return}if(new Set(["Edit","Write","Read","NotebookEdit"]).has(i)&&n)try{let g=n.file_path||n.notebook_path;if(g&&g.includes("session-memory")){M.debug("SESSION","Skipping meta-observation for session-memory file",{tool_name:i,file_path:g}),t.json({status:"skipped",reason:"session_memory_meta"});return}}catch(g){M.debug("SESSION","Could not check file_path for session-memory filter",{tool_name:i},g)}let f=this.dbManager.getSessionStore(),d=f.createSDKSession(s,"",""),v=f.getPromptCounter(d);if(!ao.checkUserPromptPrivacy(f,s,v,"observation",d,{tool_name:i})){t.json({status:"skipped",reason:"private"});return}let m="{}",y="{}";try{m=n!==void 0?Af(JSON.stringify(n)):"{}"}catch(g){M.debug("SESSION","Failed to serialize tool_input",{sessionDbId:d},g),m='{"error": "Failed to serialize tool_input"}'}try{y=o!==void 0?Af(JSON.stringify(o)):"{}"}catch(g){M.debug("SESSION","Failed to serialize tool_result",{sessionDbId:d},g),y='{"error": "Failed to serialize tool_response"}'}this.sessionManager.queueObservation(d,{tool_name:i,tool_input:m,tool_response:y,prompt_number:v,cwd:l||vr("Missing cwd when queueing observation in SessionRoutes",{sessionDbId:d,tool_name:i},"")}),this.ensureGeneratorRunning(d,"observation"),this.eventBroadcaster.broadcastObservationQueued(d),t.json({status:"queued"})});handleSummarizeByClaudeId=this.wrapHandler((r,t)=>{let{claudeSessionId:s,last_user_message:i,last_assistant_message:n}=r.body;if(!s)return this.badRequest(t,"Missing claudeSessionId");let o=this.dbManager.getSessionStore(),l=o.createSDKSession(s,"",""),c=o.getPromptCounter(l);if(!ao.checkUserPromptPrivacy(o,s,c,"summarize",l)){t.json({status:"skipped",reason:"private"});return}this.sessionManager.queueSummarize(l,i||vr("Missing last_user_message when queueing summary in SessionRoutes",{sessionDbId:l},""),n),this.ensureGeneratorRunning(l,"summarize"),this.eventBroadcaster.broadcastSummarizeQueued(),t.json({status:"queued"})});handleSessionCompleteByClaudeId=this.wrapHandler(async(r,t)=>{let{claudeSessionId:s}=r.body;if(!s)return this.badRequest(t,"Missing claudeSessionId");if(!await this.completionHandler.completeByClaudeId(s)){t.json({success:!0,message:"No active session found"});return}t.json({success:!0})});handleSessionInitByClaudeId=this.wrapHandler((r,t)=>{let{claudeSessionId:s,project:i,prompt:n}=r.body;if(!this.validateRequired(r,t,["claudeSessionId","project","prompt"]))return;let o=this.dbManager.getSessionStore(),l=o.createSDKSession(s,i,n),c=o.incrementPromptCounter(l),u=i1(n);if(!u||u.trim()===""){M.debug("HOOK","Session init - prompt entirely private",{sessionId:l,promptNumber:c,originalLength:n.length}),t.json({sessionDbId:l,promptNumber:c,skipped:!0,reason:"private"});return}o.saveUserPrompt(s,c,u),M.info("SESSION","Session initialized via HTTP",{sessionId:l,promptNumber:c,project:i}),t.json({sessionDbId:l,promptNumber:c,skipped:!1})})};var If=wt(require("path"),1),Jn=require("fs"),o1=require("os");Ar();var Ll=class extends _r{constructor(r,t,s,i,n,o){super();this.paginationHelper=r;this.dbManager=t;this.sessionManager=s;this.sseBroadcaster=i;this.workerService=n;this.startTime=o}setupRoutes(r){r.get("/api/observations",this.handleGetObservations.bind(this)),r.get("/api/summaries",this.handleGetSummaries.bind(this)),r.get("/api/prompts",this.handleGetPrompts.bind(this)),r.get("/api/observation/:id",this.handleGetObservationById.bind(this)),r.post("/api/observations/batch",this.handleGetObservationsByIds.bind(this)),r.get("/api/session/:id",this.handleGetSessionById.bind(this)),r.get("/api/prompt/:id",this.handleGetPromptById.bind(this)),r.get("/api/stats",this.handleGetStats.bind(this)),r.get("/api/projects",this.handleGetProjects.bind(this)),r.get("/api/processing-status",this.handleGetProcessingStatus.bind(this)),r.post("/api/processing",this.handleSetProcessing.bind(this))}handleGetObservations=this.wrapHandler((r,t)=>{let{offset:s,limit:i,project:n}=this.parsePaginationParams(r),o=this.paginationHelper.getObservations(s,i,n);t.json(o)});handleGetSummaries=this.wrapHandler((r,t)=>{let{offset:s,limit:i,project:n}=this.parsePaginationParams(r),o=this.paginationHelper.getSummaries(s,i,n);t.json(o)});handleGetPrompts=this.wrapHandler((r,t)=>{let{offset:s,limit:i,project:n}=this.parsePaginationParams(r),o=this.paginationHelper.getPrompts(s,i,n);t.json(o)});handleGetObservationById=this.wrapHandler((r,t)=>{let s=this.parseIntParam(r,t,"id");if(s===null)return;let n=this.dbManager.getSessionStore().getObservationById(s);if(!n){this.notFound(t,`Observation #${s} not found`);return}t.json(n)});handleGetObservationsByIds=this.wrapHandler((r,t)=>{let{ids:s,orderBy:i,limit:n,project:o}=r.body;if(!s||!Array.isArray(s)){this.badRequest(t,"ids must be an array of numbers");return}if(s.length===0){t.json([]);return}if(!s.every(u=>typeof u=="number"&&Number.isInteger(u))){this.badRequest(t,"All ids must be integers");return}let c=this.dbManager.getSessionStore().getObservationsByIds(s,{orderBy:i,limit:n,project:o});t.json(c)});handleGetSessionById=this.wrapHandler((r,t)=>{let s=this.parseIntParam(r,t,"id");if(s===null)return;let n=this.dbManager.getSessionStore().getSessionSummariesByIds([s]);if(n.length===0){this.notFound(t,`Session #${s} not found`);return}t.json(n[0])});handleGetPromptById=this.wrapHandler((r,t)=>{let s=this.parseIntParam(r,t,"id");if(s===null)return;let n=this.dbManager.getSessionStore().getUserPromptsByIds([s]);if(n.length===0){this.notFound(t,`Prompt #${s} not found`);return}t.json(n[0])});handleGetStats=this.wrapHandler((r,t)=>{let s=this.dbManager.getSessionStore().db,i=ca(),n=If.default.join(i,"package.json"),l=JSON.parse((0,Jn.readFileSync)(n,"utf-8")).version,c=s.prepare("SELECT COUNT(*) as count FROM observations").get(),u=s.prepare("SELECT COUNT(*) as count FROM sdk_sessions").get(),p=s.prepare("SELECT COUNT(*) as count FROM session_summaries").get(),f=If.default.join((0,o1.homedir)(),".claude-mem","claude-mem.db"),d=0;(0,Jn.existsSync)(f)&&(d=(0,Jn.statSync)(f).size);let v=Math.floor((Date.now()-this.startTime)/1e3),h=this.sessionManager.getActiveSessionCount(),m=this.sseBroadcaster.getClientCount();t.json({worker:{version:l,uptime:v,activeSessions:h,sseClients:m,port:Fn()},database:{path:f,size:d,observations:c.count,sessions:u.count,summaries:p.count}})});handleGetProjects=this.wrapHandler((r,t)=>{let n=this.dbManager.getSessionStore().db.prepare(` SELECT DISTINCT project FROM observations WHERE project IS NOT NULL diff --git a/plugin/skills/mem-search/SKILL.md b/plugin/skills/mem-search/SKILL.md index 732ee969..853b2c9f 100644 --- a/plugin/skills/mem-search/SKILL.md +++ b/plugin/skills/mem-search/SKILL.md @@ -19,20 +19,20 @@ Use when users ask about PREVIOUS sessions (not current conversation): **ALWAYS follow this exact flow:** 1. **Search** - Get an index of results with IDs -2. **Timeline** (optional) - Get context around top results to understand what was happening +2. **Timeline** - Get context around top results to understand what was happening 3. **Review** - Look at titles/dates/context, pick relevant IDs 4. **Fetch** - Get full details ONLY for those IDs ### Step 1: Search Everything ```bash -curl "http://localhost:37777/api/search?query=authentication&format=index&limit=5" +curl "http://localhost:37777/api/search?query=authentication&format=index&limit=40" ``` **Required parameters:** - `query` - Search term - `format=index` - ALWAYS start with index (lightweight) -- `limit=5` - Start small (3-5 results) +- `limit=40` - You can request large indexes as necessary **Returns:** ``` @@ -45,9 +45,9 @@ curl "http://localhost:37777/api/search?query=authentication&format=index&limit= ID: 10942 ``` -### Step 2: Get Timeline Context (Optional) +### Step 2: Get Timeline Context -When you need to understand "what was happening" around a result: +You MUST understand "what was happening" around a result: ```bash # Get timeline around an observation ID @@ -73,9 +73,14 @@ Review the index results (and timeline if used). Identify which IDs are actually For each relevant ID, fetch full details: ```bash -# Fetch observation +# Fetch single observation curl "http://localhost:37777/api/observation/11131" +# Fetch multiple observations in one request (more efficient) +curl -X POST "http://localhost:37777/api/observations/batch" \ + -H "Content-Type: application/json" \ + -d '{"ids": [11131, 10942, 10855]}' + # Fetch session curl "http://localhost:37777/api/session/2005" @@ -83,11 +88,24 @@ curl "http://localhost:37777/api/session/2005" curl "http://localhost:37777/api/prompt/5421" ``` +**Batch fetch options:** +```bash +# With ordering and limit +curl -X POST "http://localhost:37777/api/observations/batch" \ + -H "Content-Type: application/json" \ + -d '{"ids": [11131, 10942], "orderBy": "date_desc", "limit": 10}' +``` + **ID formats:** - Observations: Just the number (11131) - Sessions: Just the number (2005) from "S2005" - Prompts: Just the number (5421) +**When to use batch:** +- Always use batch when fetching 2+ observations +- More efficient: one request vs multiple +- Returns all observations in a single response + ## Search Parameters **Basic:** diff --git a/src/services/worker/http/routes/DataRoutes.ts b/src/services/worker/http/routes/DataRoutes.ts index 41d355ad..305cbfea 100644 --- a/src/services/worker/http/routes/DataRoutes.ts +++ b/src/services/worker/http/routes/DataRoutes.ts @@ -38,6 +38,7 @@ export class DataRoutes extends BaseRouteHandler { // Fetch by ID endpoints app.get('/api/observation/:id', this.handleGetObservationById.bind(this)); + app.post('/api/observations/batch', this.handleGetObservationsByIds.bind(this)); app.get('/api/session/:id', this.handleGetSessionById.bind(this)); app.get('/api/prompt/:id', this.handleGetPromptById.bind(this)); @@ -96,6 +97,36 @@ export class DataRoutes extends BaseRouteHandler { res.json(observation); }); + /** + * Get observations by array of IDs + * POST /api/observations/batch + * Body: { ids: number[], orderBy?: 'date_desc' | 'date_asc', limit?: number, project?: string } + */ + private handleGetObservationsByIds = this.wrapHandler((req: Request, res: Response): void => { + const { ids, orderBy, limit, project } = req.body; + + if (!ids || !Array.isArray(ids)) { + this.badRequest(res, 'ids must be an array of numbers'); + return; + } + + if (ids.length === 0) { + res.json([]); + return; + } + + // Validate all IDs are numbers + if (!ids.every(id => typeof id === 'number' && Number.isInteger(id))) { + this.badRequest(res, 'All ids must be integers'); + return; + } + + const store = this.dbManager.getSessionStore(); + const observations = store.getObservationsByIds(ids, { orderBy, limit, project }); + + res.json(observations); + }); + /** * Get session by ID * GET /api/session/:id