* Refactor context-hook: Fix anti-patterns and improve maintainability This refactor addresses all anti-patterns documented in CLAUDE.md, improving code quality without changing behavior. **Dead Code Removed:** - Eliminated unused useIndexView parameter throughout - Cleaned up entry point logic **Magic Numbers → Named Constants:** - CHARS_PER_TOKEN_ESTIMATE = 4 (token estimation) - SUMMARY_LOOKAHEAD = 1 (explains +1 in query) - Added clarifying comment for DISPLAY_SESSION_COUNT **Code Duplication Eliminated:** - Reduced 34 lines to 4 lines with renderSummaryField() helper - Replaced 4 identical summary field rendering blocks **Error Handling Added:** - parseJsonArray() now catches JSON.parse exceptions - Prevents session crashes from malformed data **Type Safety Improved:** - Added SessionSummary interface (replaced inline type cast) - Added SummaryTimelineItem for timeline items - Proper Map typing: Map<string, TimelineItem[]> **Variable Naming Clarity:** - summariesWithOffset → summariesForTimeline - isMostRecent → shouldShowLink (explains purpose) - dayTimelines → itemsByDay - nextSummary → olderSummary (correct chronology) **Better Documentation:** - Explained confusing timeline offset logic - Removed apologetic comments, added clarifying ones **Impact:** - 28 lines saved from duplication elimination - Zero behavioral changes (output identical) - Improved maintainability and type safety 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude <noreply@anthropic.com> * Fix context-hook to respect settings.json contextDepth The context-hook was ignoring the user's contextDepth setting from the web UI (stored in ~/.claude-mem/settings.json) and always using the default of 50 observations. **Problem:** - Web UI sets contextDepth in ~/.claude-mem/settings.json - Context-hook only read from process.env.CLAUDE_MEM_CONTEXT_OBSERVATIONS - User's preference of 7 observations was ignored, always showing 50 **Solution:** - Added getContextDepth() function following same pattern as getWorkerPort() - Priority: settings.json > env var > default (50) - Validates contextDepth is a positive number **Testing:** - Verified with contextDepth: 7 → shows 7 observations ✓ - Verified with contextDepth: 3 → shows 3 observations ✓ - Settings properly respected on every session start **Files Changed:** - src/hooks/context-hook.ts: Added getContextDepth() + imports - plugin/scripts/context-hook.js: Built output 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude <noreply@anthropic.com> * Fix context-hook to read from correct settings file location **Critical Bug Fix:** Previous commit read from completely wrong location with wrong field name. **What was wrong:** - Reading from: ~/.claude-mem/settings.json (doesn't exist) - Looking for: contextDepth (wrong field) - Result: Always falling back to default of 50 **What's fixed:** - Reading from: ~/.claude/settings.json (correct location) - Looking for: env.CLAUDE_MEM_CONTEXT_OBSERVATIONS (correct field) - Matches pattern used in worker-service.ts **Testing:** - With CLAUDE_MEM_CONTEXT_OBSERVATIONS: "15" → shows 15 observations ✓ - With CLAUDE_MEM_CONTEXT_OBSERVATIONS: "5" → shows 5 observations ✓ - Web UI settings now properly respected **Files Changed:** - src/hooks/context-hook.ts: Fixed path and field name in getContextDepth() - plugin/scripts/context-hook.js: Built output 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude <noreply@anthropic.com> * Fix GitHub issues #76, #74, #75 + session lifecycle improvements Bug Fixes: - Fix PM2 'Process 0 not found' error (#76): Changed pm2 restart to pm2 start (idempotent) - Fix troubleshooting skill distribution (#74, #75): Moved from .claude/skills/ to plugin/skills/ Session Lifecycle Improvements: - Added session lifecycle context to SDK agent prompt - Changed summary framing from "final report" to "progress checkpoint" - Updated summary prompts to use progressive tense ("so far", "actively working on") - Added buildContinuationPrompt() for prompt #2+ to avoid re-initialization - SessionManager now restores prompt counter from database - SDKAgent conditionally uses init vs continuation prompt based on prompt number These changes improve context-loading task handling and reduce incorrect "file not found" reports in summaries (partial fix for #73 - awaiting user feedback). 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude <noreply@anthropic.com> * Release v5.3.0: Session lifecycle improvements and bug fixes Improvements: - Session prompt counter now restored from DB on worker restart - Continuation prompts for prompt #2+ (lightweight, avoids re-init) - Summary framing changed from "final report" to "progress checkpoint" - PM2 start command (idempotent, fixes "Process 0 not found" error) - Troubleshooting skill moved to plugin/skills/ for proper distribution Technical changes: - SessionManager loads prompt_counter from DB on initialization - SDKAgent uses buildContinuationPrompt() for requests #2+ - Updated summary prompt to clarify mid-session checkpoints - Fixed worker-utils.ts to use pm2 start instead of pm2 restart - Moved .claude/skills/troubleshoot → plugin/skills/troubleshoot Fixes: - GitHub issue #76: PM2 "Process 0 not found" error - GitHub issue #74, #75: Troubleshooting skill not distributed - GitHub issue #73 (partial): Context-loading tasks reported as failed 🤖 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:
@@ -10,7 +10,7 @@
|
|||||||
"plugins": [
|
"plugins": [
|
||||||
{
|
{
|
||||||
"name": "claude-mem",
|
"name": "claude-mem",
|
||||||
"version": "5.2.3",
|
"version": "5.3.0",
|
||||||
"source": "./plugin",
|
"source": "./plugin",
|
||||||
"description": "Persistent memory system for Claude Code - context compression across sessions"
|
"description": "Persistent memory system for Claude Code - context compression across sessions"
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -6,7 +6,7 @@ Claude-mem is a Claude Code plugin providing persistent memory across sessions.
|
|||||||
|
|
||||||
**Your Role**: You are working on the plugin itself. When users interact with Claude Code with this plugin installed, your observations get captured and become their persistent memory.
|
**Your Role**: You are working on the plugin itself. When users interact with Claude Code with this plugin installed, your observations get captured and become their persistent memory.
|
||||||
|
|
||||||
**Current Version**: 5.2.3
|
**Current Version**: 5.3.0
|
||||||
|
|
||||||
## Critical Architecture Knowledge
|
## Critical Architecture Knowledge
|
||||||
|
|
||||||
|
|||||||
+1
-1
@@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"name": "claude-mem",
|
"name": "claude-mem",
|
||||||
"version": "5.2.3",
|
"version": "5.3.0",
|
||||||
"description": "Memory compression system for Claude Code - persist context across sessions",
|
"description": "Memory compression system for Claude Code - persist context across sessions",
|
||||||
"keywords": [
|
"keywords": [
|
||||||
"claude",
|
"claude",
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"name": "claude-mem",
|
"name": "claude-mem",
|
||||||
"version": "5.2.3",
|
"version": "5.3.0",
|
||||||
"description": "Persistent memory system for Claude Code - seamlessly preserve context across sessions",
|
"description": "Persistent memory system for Claude Code - seamlessly preserve context across sessions",
|
||||||
"author": {
|
"author": {
|
||||||
"name": "Alex Newman"
|
"name": "Alex Newman"
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
#!/usr/bin/env node
|
#!/usr/bin/env node
|
||||||
import X from"path";import{stdin as w}from"process";import se from"better-sqlite3";import{join as f,dirname as Q,basename as _e}from"path";import{homedir as j}from"os";import{existsSync as Ee,mkdirSync as z}from"fs";import{fileURLToPath as Z}from"url";function ee(){return typeof __dirname<"u"?__dirname:Q(Z(import.meta.url))}var ge=ee(),I=process.env.CLAUDE_MEM_DATA_DIR||f(j(),".claude-mem"),$=process.env.CLAUDE_CONFIG_DIR||f(j(),".claude"),he=f(I,"archives"),be=f(I,"logs"),Se=f(I,"trash"),fe=f(I,"backups"),Re=f(I,"settings.json"),P=f(I,"claude-mem.db"),Ne=f(I,"vector-db"),Oe=f($,"settings.json"),Ie=f($,"commands"),Le=f($,"CLAUDE.md");function H(p){z(p,{recursive:!0})}var U=(i=>(i[i.DEBUG=0]="DEBUG",i[i.INFO=1]="INFO",i[i.WARN=2]="WARN",i[i.ERROR=3]="ERROR",i[i.SILENT=4]="SILENT",i))(U||{}),M=class{level;useColor;constructor(){let e=process.env.CLAUDE_MEM_LOG_LEVEL?.toUpperCase()||"INFO";this.level=U[e]??1,this.useColor=process.stdout.isTTY??!1}correlationId(e,s){return`obs-${e}-${s}`}sessionId(e){return`session-${e}`}formatData(e){if(e==null)return"";if(typeof e=="string")return e;if(typeof e=="number"||typeof e=="boolean")return e.toString();if(typeof e=="object"){if(e instanceof Error)return this.level===0?`${e.message}
|
import x from"path";import{homedir as se}from"os";import{existsSync as te,readFileSync as re}from"fs";import{stdin as F}from"process";import ee from"better-sqlite3";import{join as b,dirname as J,basename as Te}from"path";import{homedir as j}from"os";import{existsSync as Se,mkdirSync as Q}from"fs";import{fileURLToPath as z}from"url";function Z(){return typeof __dirname<"u"?__dirname:J(z(import.meta.url))}var fe=Z(),N=process.env.CLAUDE_MEM_DATA_DIR||b(j(),".claude-mem"),$=process.env.CLAUDE_CONFIG_DIR||b(j(),".claude"),Ne=b(N,"archives"),Oe=b(N,"logs"),Ie=b(N,"trash"),Le=b(N,"backups"),ye=b(N,"settings.json"),P=b(N,"claude-mem.db"),Ae=b(N,"vector-db"),ve=b($,"settings.json"),Ce=b($,"commands"),De=b($,"CLAUDE.md");function H(c){Q(c,{recursive:!0})}var M=(i=>(i[i.DEBUG=0]="DEBUG",i[i.INFO=1]="INFO",i[i.WARN=2]="WARN",i[i.ERROR=3]="ERROR",i[i.SILENT=4]="SILENT",i))(M||{}),w=class{level;useColor;constructor(){let e=process.env.CLAUDE_MEM_LOG_LEVEL?.toUpperCase()||"INFO";this.level=M[e]??1,this.useColor=process.stdout.isTTY??!1}correlationId(e,s){return`obs-${e}-${s}`}sessionId(e){return`session-${e}`}formatData(e){if(e==null)return"";if(typeof e=="string")return e;if(typeof e=="number"||typeof e=="boolean")return e.toString();if(typeof e=="object"){if(e instanceof Error)return this.level===0?`${e.message}
|
||||||
${e.stack}`:e.message;if(Array.isArray(e))return`[${e.length} items]`;let s=Object.keys(e);return s.length===0?"{}":s.length<=3?JSON.stringify(e):`{${s.length} keys: ${s.slice(0,3).join(", ")}...}`}return String(e)}formatTool(e,s){if(!s)return e;try{let t=typeof s=="string"?JSON.parse(s):s;if(e==="Bash"&&t.command){let r=t.command.length>50?t.command.substring(0,50)+"...":t.command;return`${e}(${r})`}if(e==="Read"&&t.file_path){let r=t.file_path.split("/").pop()||t.file_path;return`${e}(${r})`}if(e==="Edit"&&t.file_path){let r=t.file_path.split("/").pop()||t.file_path;return`${e}(${r})`}if(e==="Write"&&t.file_path){let r=t.file_path.split("/").pop()||t.file_path;return`${e}(${r})`}return e}catch{return e}}log(e,s,t,r,i){if(e<this.level)return;let d=new Date().toISOString().replace("T"," ").substring(0,23),a=U[e].padEnd(5),_=s.padEnd(6),T="";r?.correlationId?T=`[${r.correlationId}] `:r?.sessionId&&(T=`[session-${r.sessionId}] `);let S="";i!=null&&(this.level===0&&typeof i=="object"?S=`
|
${e.stack}`:e.message;if(Array.isArray(e))return`[${e.length} items]`;let s=Object.keys(e);return s.length===0?"{}":s.length<=3?JSON.stringify(e):`{${s.length} keys: ${s.slice(0,3).join(", ")}...}`}return String(e)}formatTool(e,s){if(!s)return e;try{let t=typeof s=="string"?JSON.parse(s):s;if(e==="Bash"&&t.command){let r=t.command.length>50?t.command.substring(0,50)+"...":t.command;return`${e}(${r})`}if(e==="Read"&&t.file_path){let r=t.file_path.split("/").pop()||t.file_path;return`${e}(${r})`}if(e==="Edit"&&t.file_path){let r=t.file_path.split("/").pop()||t.file_path;return`${e}(${r})`}if(e==="Write"&&t.file_path){let r=t.file_path.split("/").pop()||t.file_path;return`${e}(${r})`}return e}catch{return e}}log(e,s,t,r,i){if(e<this.level)return;let d=new Date().toISOString().replace("T"," ").substring(0,23),p=M[e].padEnd(5),_=s.padEnd(6),E="";r?.correlationId?E=`[${r.correlationId}] `:r?.sessionId&&(E=`[session-${r.sessionId}] `);let n="";i!=null&&(this.level===0&&typeof i=="object"?n=`
|
||||||
`+JSON.stringify(i,null,2):S=" "+this.formatData(i));let n="";if(r){let{sessionId:R,sdkSessionId:N,correlationId:l,...c}=r;Object.keys(c).length>0&&(n=` {${Object.entries(c).map(([u,g])=>`${u}=${g}`).join(", ")}}`)}let v=`[${d}] [${a}] [${_}] ${T}${t}${n}${S}`;e===3?console.error(v):console.log(v)}debug(e,s,t,r){this.log(0,e,s,t,r)}info(e,s,t,r){this.log(1,e,s,t,r)}warn(e,s,t,r){this.log(2,e,s,t,r)}error(e,s,t,r){this.log(3,e,s,t,r)}dataIn(e,s,t,r){this.info(e,`\u2192 ${s}`,t,r)}dataOut(e,s,t,r){this.info(e,`\u2190 ${s}`,t,r)}success(e,s,t,r){this.info(e,`\u2713 ${s}`,t,r)}failure(e,s,t,r){this.error(e,`\u2717 ${s}`,t,r)}timing(e,s,t,r){this.info(e,`\u23F1 ${s}`,r,{duration:`${t}ms`})}},G=new M;var D=class{db;constructor(){H(I),this.db=new se(P),this.db.pragma("journal_mode = WAL"),this.db.pragma("synchronous = NORMAL"),this.db.pragma("foreign_keys = ON"),this.initializeSchema(),this.ensureWorkerPortColumn(),this.ensurePromptTrackingColumns(),this.removeSessionSummariesUniqueConstraint(),this.addObservationHierarchicalFields(),this.makeObservationsTextNullable(),this.createUserPromptsTable()}initializeSchema(){try{this.db.exec(`
|
`+JSON.stringify(i,null,2):n=" "+this.formatData(i));let O="";if(r){let{sessionId:S,sdkSessionId:R,correlationId:l,...a}=r;Object.keys(a).length>0&&(O=` {${Object.entries(a).map(([T,m])=>`${T}=${m}`).join(", ")}}`)}let L=`[${d}] [${p}] [${_}] ${E}${t}${O}${n}`;e===3?console.error(L):console.log(L)}debug(e,s,t,r){this.log(0,e,s,t,r)}info(e,s,t,r){this.log(1,e,s,t,r)}warn(e,s,t,r){this.log(2,e,s,t,r)}error(e,s,t,r){this.log(3,e,s,t,r)}dataIn(e,s,t,r){this.info(e,`\u2192 ${s}`,t,r)}dataOut(e,s,t,r){this.info(e,`\u2190 ${s}`,t,r)}success(e,s,t,r){this.info(e,`\u2713 ${s}`,t,r)}failure(e,s,t,r){this.error(e,`\u2717 ${s}`,t,r)}timing(e,s,t,r){this.info(e,`\u23F1 ${s}`,r,{duration:`${t}ms`})}},G=new w;var C=class{db;constructor(){H(N),this.db=new ee(P),this.db.pragma("journal_mode = WAL"),this.db.pragma("synchronous = NORMAL"),this.db.pragma("foreign_keys = ON"),this.initializeSchema(),this.ensureWorkerPortColumn(),this.ensurePromptTrackingColumns(),this.removeSessionSummariesUniqueConstraint(),this.addObservationHierarchicalFields(),this.makeObservationsTextNullable(),this.createUserPromptsTable()}initializeSchema(){try{this.db.exec(`
|
||||||
CREATE TABLE IF NOT EXISTS schema_versions (
|
CREATE TABLE IF NOT EXISTS schema_versions (
|
||||||
id INTEGER PRIMARY KEY,
|
id INTEGER PRIMARY KEY,
|
||||||
version INTEGER UNIQUE NOT NULL,
|
version INTEGER UNIQUE NOT NULL,
|
||||||
@@ -244,10 +244,10 @@ ${e.stack}`:e.message;if(Array.isArray(e))return`[${e.length} items]`;let s=Obje
|
|||||||
SELECT *
|
SELECT *
|
||||||
FROM observations
|
FROM observations
|
||||||
WHERE id = ?
|
WHERE id = ?
|
||||||
`).get(e)||null}getObservationsByIds(e,s={}){if(e.length===0)return[];let{orderBy:t="date_desc",limit:r}=s,i=t==="date_asc"?"ASC":"DESC",d=r?`LIMIT ${r}`:"",a=e.map(()=>"?").join(",");return this.db.prepare(`
|
`).get(e)||null}getObservationsByIds(e,s={}){if(e.length===0)return[];let{orderBy:t="date_desc",limit:r}=s,i=t==="date_asc"?"ASC":"DESC",d=r?`LIMIT ${r}`:"",p=e.map(()=>"?").join(",");return this.db.prepare(`
|
||||||
SELECT *
|
SELECT *
|
||||||
FROM observations
|
FROM observations
|
||||||
WHERE id IN (${a})
|
WHERE id IN (${p})
|
||||||
ORDER BY created_at_epoch ${i}
|
ORDER BY created_at_epoch ${i}
|
||||||
${d}
|
${d}
|
||||||
`).all(...e)}getSummaryForSession(e){return this.db.prepare(`
|
`).all(...e)}getSummaryForSession(e){return this.db.prepare(`
|
||||||
@@ -262,7 +262,7 @@ ${e.stack}`:e.message;if(Array.isArray(e))return`[${e.length} items]`;let s=Obje
|
|||||||
SELECT files_read, files_modified
|
SELECT files_read, files_modified
|
||||||
FROM observations
|
FROM observations
|
||||||
WHERE sdk_session_id = ?
|
WHERE sdk_session_id = ?
|
||||||
`).all(e),r=new Set,i=new Set;for(let d of t){if(d.files_read)try{let a=JSON.parse(d.files_read);Array.isArray(a)&&a.forEach(_=>r.add(_))}catch{}if(d.files_modified)try{let a=JSON.parse(d.files_modified);Array.isArray(a)&&a.forEach(_=>i.add(_))}catch{}}return{filesRead:Array.from(r),filesModified:Array.from(i)}}getSessionById(e){return this.db.prepare(`
|
`).all(e),r=new Set,i=new Set;for(let d of t){if(d.files_read)try{let p=JSON.parse(d.files_read);Array.isArray(p)&&p.forEach(_=>r.add(_))}catch{}if(d.files_modified)try{let p=JSON.parse(d.files_modified);Array.isArray(p)&&p.forEach(_=>i.add(_))}catch{}}return{filesRead:Array.from(r),filesModified:Array.from(i)}}getSessionById(e){return this.db.prepare(`
|
||||||
SELECT id, claude_session_id, sdk_session_id, project, user_prompt
|
SELECT id, claude_session_id, sdk_session_id, project, user_prompt
|
||||||
FROM sdk_sessions
|
FROM sdk_sessions
|
||||||
WHERE id = ?
|
WHERE id = ?
|
||||||
@@ -289,13 +289,13 @@ ${e.stack}`:e.message;if(Array.isArray(e))return`[${e.length} items]`;let s=Obje
|
|||||||
SELECT prompt_counter FROM sdk_sessions WHERE id = ?
|
SELECT prompt_counter FROM sdk_sessions WHERE id = ?
|
||||||
`).get(e)?.prompt_counter||1}getPromptCounter(e){return this.db.prepare(`
|
`).get(e)?.prompt_counter||1}getPromptCounter(e){return this.db.prepare(`
|
||||||
SELECT prompt_counter FROM sdk_sessions WHERE id = ?
|
SELECT prompt_counter FROM sdk_sessions WHERE id = ?
|
||||||
`).get(e)?.prompt_counter||0}createSDKSession(e,s,t){let r=new Date,i=r.getTime(),a=this.db.prepare(`
|
`).get(e)?.prompt_counter||0}createSDKSession(e,s,t){let r=new Date,i=r.getTime(),p=this.db.prepare(`
|
||||||
INSERT OR IGNORE INTO sdk_sessions
|
INSERT OR IGNORE INTO sdk_sessions
|
||||||
(claude_session_id, sdk_session_id, project, user_prompt, started_at, started_at_epoch, status)
|
(claude_session_id, sdk_session_id, project, user_prompt, started_at, started_at_epoch, status)
|
||||||
VALUES (?, ?, ?, ?, ?, ?, 'active')
|
VALUES (?, ?, ?, ?, ?, ?, 'active')
|
||||||
`).run(e,e,s,t,r.toISOString(),i);return a.lastInsertRowid===0||a.changes===0?this.db.prepare(`
|
`).run(e,e,s,t,r.toISOString(),i);return p.lastInsertRowid===0||p.changes===0?this.db.prepare(`
|
||||||
SELECT id FROM sdk_sessions WHERE claude_session_id = ? LIMIT 1
|
SELECT id FROM sdk_sessions WHERE claude_session_id = ? LIMIT 1
|
||||||
`).get(e).id:a.lastInsertRowid}updateSDKSessionId(e,s){return this.db.prepare(`
|
`).get(e).id:p.lastInsertRowid}updateSDKSessionId(e,s){return this.db.prepare(`
|
||||||
UPDATE sdk_sessions
|
UPDATE sdk_sessions
|
||||||
SET sdk_session_id = ?
|
SET sdk_session_id = ?
|
||||||
WHERE id = ? AND sdk_session_id IS NULL
|
WHERE id = ? AND sdk_session_id IS NULL
|
||||||
@@ -318,23 +318,23 @@ ${e.stack}`:e.message;if(Array.isArray(e))return`[${e.length} items]`;let s=Obje
|
|||||||
INSERT INTO sdk_sessions
|
INSERT INTO sdk_sessions
|
||||||
(claude_session_id, sdk_session_id, project, started_at, started_at_epoch, status)
|
(claude_session_id, sdk_session_id, project, started_at, started_at_epoch, status)
|
||||||
VALUES (?, ?, ?, ?, ?, 'active')
|
VALUES (?, ?, ?, ?, ?, 'active')
|
||||||
`).run(e,e,s,i.toISOString(),d),console.error(`[SessionStore] Auto-created session record for session_id: ${e}`));let S=this.db.prepare(`
|
`).run(e,e,s,i.toISOString(),d),console.error(`[SessionStore] Auto-created session record for session_id: ${e}`));let n=this.db.prepare(`
|
||||||
INSERT INTO observations
|
INSERT INTO observations
|
||||||
(sdk_session_id, project, type, title, subtitle, facts, narrative, concepts,
|
(sdk_session_id, project, type, title, subtitle, facts, narrative, concepts,
|
||||||
files_read, files_modified, prompt_number, created_at, created_at_epoch)
|
files_read, files_modified, prompt_number, created_at, created_at_epoch)
|
||||||
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
|
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
|
||||||
`).run(e,s,t.type,t.title,t.subtitle,JSON.stringify(t.facts),t.narrative,JSON.stringify(t.concepts),JSON.stringify(t.files_read),JSON.stringify(t.files_modified),r||null,i.toISOString(),d);return{id:Number(S.lastInsertRowid),createdAtEpoch:d}}storeSummary(e,s,t,r){let i=new Date,d=i.getTime();this.db.prepare(`
|
`).run(e,s,t.type,t.title,t.subtitle,JSON.stringify(t.facts),t.narrative,JSON.stringify(t.concepts),JSON.stringify(t.files_read),JSON.stringify(t.files_modified),r||null,i.toISOString(),d);return{id:Number(n.lastInsertRowid),createdAtEpoch:d}}storeSummary(e,s,t,r){let i=new Date,d=i.getTime();this.db.prepare(`
|
||||||
SELECT id FROM sdk_sessions WHERE sdk_session_id = ?
|
SELECT id FROM sdk_sessions WHERE sdk_session_id = ?
|
||||||
`).get(e)||(this.db.prepare(`
|
`).get(e)||(this.db.prepare(`
|
||||||
INSERT INTO sdk_sessions
|
INSERT INTO sdk_sessions
|
||||||
(claude_session_id, sdk_session_id, project, started_at, started_at_epoch, status)
|
(claude_session_id, sdk_session_id, project, started_at, started_at_epoch, status)
|
||||||
VALUES (?, ?, ?, ?, ?, 'active')
|
VALUES (?, ?, ?, ?, ?, 'active')
|
||||||
`).run(e,e,s,i.toISOString(),d),console.error(`[SessionStore] Auto-created session record for session_id: ${e}`));let S=this.db.prepare(`
|
`).run(e,e,s,i.toISOString(),d),console.error(`[SessionStore] Auto-created session record for session_id: ${e}`));let n=this.db.prepare(`
|
||||||
INSERT INTO session_summaries
|
INSERT INTO session_summaries
|
||||||
(sdk_session_id, project, request, investigated, learned, completed,
|
(sdk_session_id, project, request, investigated, learned, completed,
|
||||||
next_steps, notes, prompt_number, created_at, created_at_epoch)
|
next_steps, notes, prompt_number, created_at, created_at_epoch)
|
||||||
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
|
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
|
||||||
`).run(e,s,t.request,t.investigated,t.learned,t.completed,t.next_steps,t.notes,r||null,i.toISOString(),d);return{id:Number(S.lastInsertRowid),createdAtEpoch:d}}markSessionCompleted(e){let s=new Date,t=s.getTime();this.db.prepare(`
|
`).run(e,s,t.request,t.investigated,t.learned,t.completed,t.next_steps,t.notes,r||null,i.toISOString(),d);return{id:Number(n.lastInsertRowid),createdAtEpoch:d}}markSessionCompleted(e){let s=new Date,t=s.getTime();this.db.prepare(`
|
||||||
UPDATE sdk_sessions
|
UPDATE sdk_sessions
|
||||||
SET status = 'completed', completed_at = ?, completed_at_epoch = ?
|
SET status = 'completed', completed_at = ?, completed_at_epoch = ?
|
||||||
WHERE id = ?
|
WHERE id = ?
|
||||||
@@ -342,62 +342,62 @@ ${e.stack}`:e.message;if(Array.isArray(e))return`[${e.length} items]`;let s=Obje
|
|||||||
UPDATE sdk_sessions
|
UPDATE sdk_sessions
|
||||||
SET status = 'failed', completed_at = ?, completed_at_epoch = ?
|
SET status = 'failed', completed_at = ?, completed_at_epoch = ?
|
||||||
WHERE id = ?
|
WHERE id = ?
|
||||||
`).run(s.toISOString(),t,e)}getSessionSummariesByIds(e,s={}){if(e.length===0)return[];let{orderBy:t="date_desc",limit:r}=s,i=t==="date_asc"?"ASC":"DESC",d=r?`LIMIT ${r}`:"",a=e.map(()=>"?").join(",");return this.db.prepare(`
|
`).run(s.toISOString(),t,e)}getSessionSummariesByIds(e,s={}){if(e.length===0)return[];let{orderBy:t="date_desc",limit:r}=s,i=t==="date_asc"?"ASC":"DESC",d=r?`LIMIT ${r}`:"",p=e.map(()=>"?").join(",");return this.db.prepare(`
|
||||||
SELECT * FROM session_summaries
|
SELECT * FROM session_summaries
|
||||||
WHERE id IN (${a})
|
WHERE id IN (${p})
|
||||||
ORDER BY created_at_epoch ${i}
|
ORDER BY created_at_epoch ${i}
|
||||||
${d}
|
${d}
|
||||||
`).all(...e)}getUserPromptsByIds(e,s={}){if(e.length===0)return[];let{orderBy:t="date_desc",limit:r}=s,i=t==="date_asc"?"ASC":"DESC",d=r?`LIMIT ${r}`:"",a=e.map(()=>"?").join(",");return this.db.prepare(`
|
`).all(...e)}getUserPromptsByIds(e,s={}){if(e.length===0)return[];let{orderBy:t="date_desc",limit:r}=s,i=t==="date_asc"?"ASC":"DESC",d=r?`LIMIT ${r}`:"",p=e.map(()=>"?").join(",");return this.db.prepare(`
|
||||||
SELECT
|
SELECT
|
||||||
up.*,
|
up.*,
|
||||||
s.project,
|
s.project,
|
||||||
s.sdk_session_id
|
s.sdk_session_id
|
||||||
FROM user_prompts up
|
FROM user_prompts up
|
||||||
JOIN sdk_sessions s ON up.claude_session_id = s.claude_session_id
|
JOIN sdk_sessions s ON up.claude_session_id = s.claude_session_id
|
||||||
WHERE up.id IN (${a})
|
WHERE up.id IN (${p})
|
||||||
ORDER BY up.created_at_epoch ${i}
|
ORDER BY up.created_at_epoch ${i}
|
||||||
${d}
|
${d}
|
||||||
`).all(...e)}getTimelineAroundTimestamp(e,s=10,t=10,r){return this.getTimelineAroundObservation(null,e,s,t,r)}getTimelineAroundObservation(e,s,t=10,r=10,i){let d=i?"AND project = ?":"",a=i?[i]:[],_,T;if(e!==null){let R=`
|
`).all(...e)}getTimelineAroundTimestamp(e,s=10,t=10,r){return this.getTimelineAroundObservation(null,e,s,t,r)}getTimelineAroundObservation(e,s,t=10,r=10,i){let d=i?"AND project = ?":"",p=i?[i]:[],_,E;if(e!==null){let S=`
|
||||||
SELECT id, created_at_epoch
|
SELECT id, created_at_epoch
|
||||||
FROM observations
|
FROM observations
|
||||||
WHERE id <= ? ${d}
|
WHERE id <= ? ${d}
|
||||||
ORDER BY id DESC
|
ORDER BY id DESC
|
||||||
LIMIT ?
|
LIMIT ?
|
||||||
`,N=`
|
`,R=`
|
||||||
SELECT id, created_at_epoch
|
SELECT id, created_at_epoch
|
||||||
FROM observations
|
FROM observations
|
||||||
WHERE id >= ? ${d}
|
WHERE id >= ? ${d}
|
||||||
ORDER BY id ASC
|
ORDER BY id ASC
|
||||||
LIMIT ?
|
LIMIT ?
|
||||||
`;try{let l=this.db.prepare(R).all(e,...a,t+1),c=this.db.prepare(N).all(e,...a,r+1);if(l.length===0&&c.length===0)return{observations:[],sessions:[],prompts:[]};_=l.length>0?l[l.length-1].created_at_epoch:s,T=c.length>0?c[c.length-1].created_at_epoch:s}catch(l){return console.error("[SessionStore] Error getting boundary observations:",l.message),{observations:[],sessions:[],prompts:[]}}}else{let R=`
|
`;try{let l=this.db.prepare(S).all(e,...p,t+1),a=this.db.prepare(R).all(e,...p,r+1);if(l.length===0&&a.length===0)return{observations:[],sessions:[],prompts:[]};_=l.length>0?l[l.length-1].created_at_epoch:s,E=a.length>0?a[a.length-1].created_at_epoch:s}catch(l){return console.error("[SessionStore] Error getting boundary observations:",l.message),{observations:[],sessions:[],prompts:[]}}}else{let S=`
|
||||||
SELECT created_at_epoch
|
SELECT created_at_epoch
|
||||||
FROM observations
|
FROM observations
|
||||||
WHERE created_at_epoch <= ? ${d}
|
WHERE created_at_epoch <= ? ${d}
|
||||||
ORDER BY created_at_epoch DESC
|
ORDER BY created_at_epoch DESC
|
||||||
LIMIT ?
|
LIMIT ?
|
||||||
`,N=`
|
`,R=`
|
||||||
SELECT created_at_epoch
|
SELECT created_at_epoch
|
||||||
FROM observations
|
FROM observations
|
||||||
WHERE created_at_epoch >= ? ${d}
|
WHERE created_at_epoch >= ? ${d}
|
||||||
ORDER BY created_at_epoch ASC
|
ORDER BY created_at_epoch ASC
|
||||||
LIMIT ?
|
LIMIT ?
|
||||||
`;try{let l=this.db.prepare(R).all(s,...a,t),c=this.db.prepare(N).all(s,...a,r+1);if(l.length===0&&c.length===0)return{observations:[],sessions:[],prompts:[]};_=l.length>0?l[l.length-1].created_at_epoch:s,T=c.length>0?c[c.length-1].created_at_epoch:s}catch(l){return console.error("[SessionStore] Error getting boundary timestamps:",l.message),{observations:[],sessions:[],prompts:[]}}}let S=`
|
`;try{let l=this.db.prepare(S).all(s,...p,t),a=this.db.prepare(R).all(s,...p,r+1);if(l.length===0&&a.length===0)return{observations:[],sessions:[],prompts:[]};_=l.length>0?l[l.length-1].created_at_epoch:s,E=a.length>0?a[a.length-1].created_at_epoch:s}catch(l){return console.error("[SessionStore] Error getting boundary timestamps:",l.message),{observations:[],sessions:[],prompts:[]}}}let n=`
|
||||||
SELECT *
|
SELECT *
|
||||||
FROM observations
|
FROM observations
|
||||||
WHERE created_at_epoch >= ? AND created_at_epoch <= ? ${d}
|
WHERE created_at_epoch >= ? AND created_at_epoch <= ? ${d}
|
||||||
ORDER BY created_at_epoch ASC
|
ORDER BY created_at_epoch ASC
|
||||||
`,n=`
|
`,O=`
|
||||||
SELECT *
|
SELECT *
|
||||||
FROM session_summaries
|
FROM session_summaries
|
||||||
WHERE created_at_epoch >= ? AND created_at_epoch <= ? ${d}
|
WHERE created_at_epoch >= ? AND created_at_epoch <= ? ${d}
|
||||||
ORDER BY created_at_epoch ASC
|
ORDER BY created_at_epoch ASC
|
||||||
`,v=`
|
`,L=`
|
||||||
SELECT up.*, s.project, s.sdk_session_id
|
SELECT up.*, s.project, s.sdk_session_id
|
||||||
FROM user_prompts up
|
FROM user_prompts up
|
||||||
JOIN sdk_sessions s ON up.claude_session_id = s.claude_session_id
|
JOIN sdk_sessions s ON up.claude_session_id = s.claude_session_id
|
||||||
WHERE up.created_at_epoch >= ? AND up.created_at_epoch <= ? ${d.replace("project","s.project")}
|
WHERE up.created_at_epoch >= ? AND up.created_at_epoch <= ? ${d.replace("project","s.project")}
|
||||||
ORDER BY up.created_at_epoch ASC
|
ORDER BY up.created_at_epoch ASC
|
||||||
`;try{let R=this.db.prepare(S).all(_,T,...a),N=this.db.prepare(n).all(_,T,...a),l=this.db.prepare(v).all(_,T,...a);return{observations:R,sessions:N.map(c=>({id:c.id,sdk_session_id:c.sdk_session_id,project:c.project,request:c.request,completed:c.completed,next_steps:c.next_steps,created_at:c.created_at,created_at_epoch:c.created_at_epoch})),prompts:l.map(c=>({id:c.id,claude_session_id:c.claude_session_id,project:c.project,prompt:c.prompt_text,created_at:c.created_at,created_at_epoch:c.created_at_epoch}))}}catch(R){return console.error("[SessionStore] Error querying timeline records:",R.message),{observations:[],sessions:[],prompts:[]}}}close(){this.db.close()}};var te=parseInt(process.env.CLAUDE_MEM_CONTEXT_OBSERVATIONS||"50",10),W=10,o={reset:"\x1B[0m",bright:"\x1B[1m",dim:"\x1B[2m",cyan:"\x1B[36m",green:"\x1B[32m",yellow:"\x1B[33m",blue:"\x1B[34m",magenta:"\x1B[35m",gray:"\x1B[90m",red:"\x1B[31m"};function re(p){if(!p)return[];let e=JSON.parse(p);return Array.isArray(e)?e:[]}function ne(p){return new Date(p).toLocaleString("en-US",{month:"short",day:"numeric",hour:"numeric",minute:"2-digit",hour12:!0})}function ie(p){return new Date(p).toLocaleString("en-US",{hour:"numeric",minute:"2-digit",hour12:!0})}function oe(p){return new Date(p).toLocaleString("en-US",{month:"short",day:"numeric",year:"numeric"})}function ae(p){return p?Math.ceil(p.length/4):0}function de(p,e){return X.isAbsolute(p)?X.relative(e,p):p}async function Y(p,e=!1,s=!1){let t=p?.cwd??process.cwd(),r=t?X.basename(t):"unknown-project",i=new D,d=i.db.prepare(`
|
`;try{let S=this.db.prepare(n).all(_,E,...p),R=this.db.prepare(O).all(_,E,...p),l=this.db.prepare(L).all(_,E,...p);return{observations:S,sessions:R.map(a=>({id:a.id,sdk_session_id:a.sdk_session_id,project:a.project,request:a.request,completed:a.completed,next_steps:a.next_steps,created_at:a.created_at,created_at_epoch:a.created_at_epoch})),prompts:l.map(a=>({id:a.id,claude_session_id:a.claude_session_id,project:a.project,prompt:a.prompt_text,created_at:a.created_at,created_at_epoch:a.created_at_epoch}))}}catch(S){return console.error("[SessionStore] Error querying timeline records:",S.message),{observations:[],sessions:[],prompts:[]}}}close(){this.db.close()}};function ne(){try{let c=x.join(se(),".claude","settings.json");if(te(c)){let e=JSON.parse(re(c,"utf-8"));if(e.env?.CLAUDE_MEM_CONTEXT_OBSERVATIONS){let s=parseInt(e.env.CLAUDE_MEM_CONTEXT_OBSERVATIONS,10);if(!isNaN(s)&&s>0)return s}}}catch{}return parseInt(process.env.CLAUDE_MEM_CONTEXT_OBSERVATIONS||"50",10)}var ie=ne(),W=10,oe=4,ae=1,o={reset:"\x1B[0m",bright:"\x1B[1m",dim:"\x1B[2m",cyan:"\x1B[36m",green:"\x1B[32m",yellow:"\x1B[33m",blue:"\x1B[34m",magenta:"\x1B[35m",gray:"\x1B[90m",red:"\x1B[31m"};function de(c){if(!c)return[];try{let e=JSON.parse(c);return Array.isArray(e)?e:[]}catch{return[]}}function ce(c){return new Date(c).toLocaleString("en-US",{month:"short",day:"numeric",hour:"numeric",minute:"2-digit",hour12:!0})}function pe(c){return new Date(c).toLocaleString("en-US",{hour:"numeric",minute:"2-digit",hour12:!0})}function _e(c){return new Date(c).toLocaleString("en-US",{month:"short",day:"numeric",year:"numeric"})}function ue(c){return c?Math.ceil(c.length/oe):0}function le(c,e){return x.isAbsolute(c)?x.relative(e,c):c}function D(c,e,s,t){return e?t?[`${s}${c}:${o.reset} ${e}`,""]:[`**${c}**: ${e}`,""]:[]}async function Y(c,e=!1){let s=c?.cwd??process.cwd(),t=s?x.basename(s):"unknown-project",r=new C,i=r.db.prepare(`
|
||||||
SELECT
|
SELECT
|
||||||
id, sdk_session_id, type, title, subtitle, narrative,
|
id, sdk_session_id, type, title, subtitle, narrative,
|
||||||
facts, concepts, files_read, files_modified,
|
facts, concepts, files_read, files_modified,
|
||||||
@@ -406,18 +406,18 @@ ${e.stack}`:e.message;if(Array.isArray(e))return`[${e.length} items]`;let s=Obje
|
|||||||
WHERE project = ?
|
WHERE project = ?
|
||||||
ORDER BY created_at_epoch DESC
|
ORDER BY created_at_epoch DESC
|
||||||
LIMIT ?
|
LIMIT ?
|
||||||
`).all(r,te),a=i.db.prepare(`
|
`).all(t,ie),d=r.db.prepare(`
|
||||||
SELECT id, sdk_session_id, request, investigated, learned, completed, next_steps, created_at, created_at_epoch
|
SELECT id, sdk_session_id, request, investigated, learned, completed, next_steps, created_at, created_at_epoch
|
||||||
FROM session_summaries
|
FROM session_summaries
|
||||||
WHERE project = ?
|
WHERE project = ?
|
||||||
ORDER BY created_at_epoch DESC
|
ORDER BY created_at_epoch DESC
|
||||||
LIMIT ?
|
LIMIT ?
|
||||||
`).all(r,W+1);if(d.length===0&&a.length===0)return i.close(),e?`
|
`).all(t,W+ae);if(i.length===0&&d.length===0)return r.close(),e?`
|
||||||
${o.bright}${o.cyan}\u{1F4DD} [${r}] recent context${o.reset}
|
${o.bright}${o.cyan}\u{1F4DD} [${t}] recent context${o.reset}
|
||||||
${o.gray}${"\u2500".repeat(60)}${o.reset}
|
${o.gray}${"\u2500".repeat(60)}${o.reset}
|
||||||
|
|
||||||
${o.dim}No previous sessions found for this project yet.${o.reset}
|
${o.dim}No previous sessions found for this project yet.${o.reset}
|
||||||
`:`# [${r}] recent context
|
`:`# [${t}] recent context
|
||||||
|
|
||||||
No previous sessions found for this project yet.`;let _=d,T=a.slice(0,W),S=_,n=[];if(e?(n.push(""),n.push(`${o.bright}${o.cyan}\u{1F4DD} [${r}] recent context${o.reset}`),n.push(`${o.gray}${"\u2500".repeat(60)}${o.reset}`),n.push("")):(n.push(`# [${r}] recent context`),n.push("")),S.length>0){e?(n.push(`${o.dim}Legend: \u{1F3AF} session-request | \u{1F534} bugfix | \u{1F7E3} feature | \u{1F504} refactor | \u2705 change | \u{1F535} discovery | \u{1F9E0} decision${o.reset}`),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("")),e?(n.push(`${o.dim}\u{1F4A1} Progressive Disclosure: This index shows WHAT exists (titles) and retrieval COST (token counts).${o.reset}`),n.push(`${o.dim} \u2192 Use MCP search tools to fetch full observation details on-demand (Layer 2)${o.reset}`),n.push(`${o.dim} \u2192 Prefer searching observations over re-reading code for past decisions and learnings${o.reset}`),n.push(`${o.dim} \u2192 Critical types (\u{1F534} bugfix, \u{1F9E0} decision) often worth fetching immediately${o.reset}`),n.push("")):(n.push("\u{1F4A1} **Progressive Disclosure:** This index shows WHAT exists (titles) and retrieval COST (token counts)."),n.push("- Use MCP search tools to fetch full observation details on-demand (Layer 2)"),n.push("- Prefer searching observations over re-reading code for past decisions and learnings"),n.push("- Critical types (\u{1F534} bugfix, \u{1F9E0} decision) often worth fetching immediately"),n.push(""));let v=a[0]?.id,R=T.map((u,g)=>{let E=g===0?null:a[g+1];return{...u,displayEpoch:E?E.created_at_epoch:u.created_at_epoch,displayTime:E?E.created_at:u.created_at,isMostRecent:u.id===v}}),N=[...S.map(u=>({type:"observation",data:u})),...R.map(u=>({type:"summary",data:u}))];N.sort((u,g)=>{let E=u.type==="observation"?u.data.created_at_epoch:u.data.displayEpoch,L=g.type==="observation"?g.data.created_at_epoch:g.data.displayEpoch;return E-L});let l=new Map;for(let u of N){let g=u.type==="observation"?u.data.created_at:u.data.displayTime,E=oe(g);l.has(E)||l.set(E,[]),l.get(E).push(u)}let c=Array.from(l.entries()).sort((u,g)=>{let E=new Date(u[0]).getTime(),L=new Date(g[0]).getTime();return E-L});for(let[u,g]of c){e?(n.push(`${o.bright}${o.cyan}${u}${o.reset}`),n.push("")):(n.push(`### ${u}`),n.push(""));let E=null,L="",A=!1;for(let x of g)if(x.type==="summary"){A&&(n.push(""),A=!1,E=null,L="");let h=x.data,y=`${h.request||"Session started"} (${ne(h.displayTime)})`,O=h.isMostRecent?"":`claude-mem://session-summary/${h.id}`;if(e){let b=O?`${o.dim}[${O}]${o.reset}`:"";n.push(`\u{1F3AF} ${o.yellow}#S${h.id}${o.reset} ${y} ${b}`)}else{let b=O?` [\u2192](${O})`:"";n.push(`**\u{1F3AF} #S${h.id}** ${y}${b}`)}n.push("")}else{let h=x.data,y=re(h.files_modified),O=y.length>0?de(y[0],t):"General";O!==E&&(A&&n.push(""),e?n.push(`${o.dim}${O}${o.reset}`):n.push(`**${O}**`),e||(n.push("| ID | Time | T | Title | Tokens |"),n.push("|----|------|---|-------|--------|")),E=O,A=!0,L="");let b="\u2022";switch(h.type){case"bugfix":b="\u{1F534}";break;case"feature":b="\u{1F7E3}";break;case"refactor":b="\u{1F504}";break;case"change":b="\u2705";break;case"discovery":b="\u{1F535}";break;case"decision":b="\u{1F9E0}";break;default:b="\u2022"}let C=ie(h.created_at),F=h.title||"Untitled",k=ae(h.narrative),B=C!==L,q=B?C:"";if(L=C,e){let K=B?`${o.dim}${C}${o.reset}`:" ".repeat(C.length),J=k>0?`${o.dim}(~${k}t)${o.reset}`:"";n.push(` ${o.dim}#${h.id}${o.reset} ${K} ${b} ${F} ${J}`)}else n.push(`| #${h.id} | ${q||"\u2033"} | ${b} | ${F} | ~${k} |`)}A&&n.push("")}let m=a[0];m&&(m.investigated||m.learned||m.completed||m.next_steps)&&(m.investigated&&(e?n.push(`${o.blue}Investigated:${o.reset} ${m.investigated}`):n.push(`**Investigated**: ${m.investigated}`),n.push("")),m.learned&&(e?n.push(`${o.yellow}Learned:${o.reset} ${m.learned}`):n.push(`**Learned**: ${m.learned}`),n.push("")),m.completed&&(e?n.push(`${o.green}Completed:${o.reset} ${m.completed}`):n.push(`**Completed**: ${m.completed}`),n.push("")),m.next_steps&&(e?n.push(`${o.magenta}Next Steps:${o.reset} ${m.next_steps}`):n.push(`**Next Steps**: ${m.next_steps}`),n.push(""))),e?n.push(`${o.dim}Use claude-mem MCP search to access records with the given ID${o.reset}`):n.push("*Use claude-mem MCP search to access records with the given ID*")}return i.close(),n.join(`
|
No previous sessions found for this project yet.`;let p=i,_=d.slice(0,W),E=p,n=[];if(e?(n.push(""),n.push(`${o.bright}${o.cyan}\u{1F4DD} [${t}] recent context${o.reset}`),n.push(`${o.gray}${"\u2500".repeat(60)}${o.reset}`),n.push("")):(n.push(`# [${t}] recent context`),n.push("")),E.length>0){e?(n.push(`${o.dim}Legend: \u{1F3AF} session-request | \u{1F534} bugfix | \u{1F7E3} feature | \u{1F504} refactor | \u2705 change | \u{1F535} discovery | \u{1F9E0} decision${o.reset}`),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("")),e?(n.push(`${o.dim}\u{1F4A1} Progressive Disclosure: This index shows WHAT exists (titles) and retrieval COST (token counts).${o.reset}`),n.push(`${o.dim} \u2192 Use MCP search tools to fetch full observation details on-demand (Layer 2)${o.reset}`),n.push(`${o.dim} \u2192 Prefer searching observations over re-reading code for past decisions and learnings${o.reset}`),n.push(`${o.dim} \u2192 Critical types (\u{1F534} bugfix, \u{1F9E0} decision) often worth fetching immediately${o.reset}`),n.push("")):(n.push("\u{1F4A1} **Progressive Disclosure:** This index shows WHAT exists (titles) and retrieval COST (token counts)."),n.push("- Use MCP search tools to fetch full observation details on-demand (Layer 2)"),n.push("- Prefer searching observations over re-reading code for past decisions and learnings"),n.push("- Critical types (\u{1F534} bugfix, \u{1F9E0} decision) often worth fetching immediately"),n.push(""));let O=d[0]?.id,L=_.map((u,T)=>{let m=T===0?null:d[T+1];return{...u,displayEpoch:m?m.created_at_epoch:u.created_at_epoch,displayTime:m?m.created_at:u.created_at,shouldShowLink:u.id!==O}}),S=[...E.map(u=>({type:"observation",data:u})),...L.map(u=>({type:"summary",data:u}))];S.sort((u,T)=>{let m=u.type==="observation"?u.data.created_at_epoch:u.data.displayEpoch,I=T.type==="observation"?T.data.created_at_epoch:T.data.displayEpoch;return m-I});let R=new Map;for(let u of S){let T=u.type==="observation"?u.data.created_at:u.data.displayTime,m=_e(T);R.has(m)||R.set(m,[]),R.get(m).push(u)}let l=Array.from(R.entries()).sort((u,T)=>{let m=new Date(u[0]).getTime(),I=new Date(T[0]).getTime();return m-I});for(let[u,T]of l){e?(n.push(`${o.bright}${o.cyan}${u}${o.reset}`),n.push("")):(n.push(`### ${u}`),n.push(""));let m=null,I="",y=!1;for(let k of T)if(k.type==="summary"){y&&(n.push(""),y=!1,m=null,I="");let g=k.data,A=`${g.request||"Session started"} (${ce(g.displayTime)})`,f=g.shouldShowLink?`claude-mem://session-summary/${g.id}`:"";if(e){let h=f?`${o.dim}[${f}]${o.reset}`:"";n.push(`\u{1F3AF} ${o.yellow}#S${g.id}${o.reset} ${A} ${h}`)}else{let h=f?` [\u2192](${f})`:"";n.push(`**\u{1F3AF} #S${g.id}** ${A}${h}`)}n.push("")}else{let g=k.data,A=de(g.files_modified),f=A.length>0?le(A[0],s):"General";f!==m&&(y&&n.push(""),e?n.push(`${o.dim}${f}${o.reset}`):n.push(`**${f}**`),e||(n.push("| ID | Time | T | Title | Tokens |"),n.push("|----|------|---|-------|--------|")),m=f,y=!0,I="");let h="\u2022";switch(g.type){case"bugfix":h="\u{1F534}";break;case"feature":h="\u{1F7E3}";break;case"refactor":h="\u{1F504}";break;case"change":h="\u2705";break;case"discovery":h="\u{1F535}";break;case"decision":h="\u{1F9E0}";break;default:h="\u2022"}let v=pe(g.created_at),X=g.title||"Untitled",U=ue(g.narrative),B=v!==I,V=B?v:"";if(I=v,e){let q=B?`${o.dim}${v}${o.reset}`:" ".repeat(v.length),K=U>0?`${o.dim}(~${U}t)${o.reset}`:"";n.push(` ${o.dim}#${g.id}${o.reset} ${q} ${h} ${X} ${K}`)}else n.push(`| #${g.id} | ${V||"\u2033"} | ${h} | ${X} | ~${U} |`)}y&&n.push("")}let a=d[0];a&&(a.investigated||a.learned||a.completed||a.next_steps)&&(n.push(...D("Investigated",a.investigated,o.blue,e)),n.push(...D("Learned",a.learned,o.yellow,e)),n.push(...D("Completed",a.completed,o.green,e)),n.push(...D("Next Steps",a.next_steps,o.magenta,e))),e?n.push(`${o.dim}Use claude-mem MCP search to access records with the given ID${o.reset}`):n.push("*Use claude-mem MCP search to access records with the given ID*")}return r.close(),n.join(`
|
||||||
`).trimEnd()}var V=process.argv.includes("--index"),ce=process.argv.includes("--colors");if(w.isTTY||ce)Y(void 0,!0,V).then(p=>{console.log(p),process.exit(0)});else{let p="";w.on("data",e=>p+=e),w.on("end",async()=>{let e=p.trim()?JSON.parse(p):void 0,t={hookSpecificOutput:{hookEventName:"SessionStart",additionalContext:await Y(e,!1,V)}};console.log(JSON.stringify(t)),process.exit(0)})}
|
`).trimEnd()}var me=process.argv.includes("--colors");if(F.isTTY||me)Y(void 0,!0).then(c=>{console.log(c),process.exit(0)});else{let c="";F.on("data",e=>c+=e),F.on("end",async()=>{let e=c.trim()?JSON.parse(c):void 0,t={hookSpecificOutput:{hookEventName:"SessionStart",additionalContext:await Y(e,!1)}};console.log(JSON.stringify(t)),process.exit(0)})}
|
||||||
|
|||||||
@@ -397,4 +397,4 @@ ${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
|
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")}
|
WHERE up.created_at_epoch >= ? AND up.created_at_epoch <= ? ${o.replace("project","s.project")}
|
||||||
ORDER BY up.created_at_epoch ASC
|
ORDER BY up.created_at_epoch ASC
|
||||||
`;try{let l=this.db.prepare(E).all(d,c,...i),b=this.db.prepare(T).all(d,c,...i),_=this.db.prepare(S).all(d,c,...i);return{observations:l,sessions:b.map(p=>({id:p.id,sdk_session_id:p.sdk_session_id,project:p.project,request:p.request,completed:p.completed,next_steps:p.next_steps,created_at:p.created_at,created_at_epoch:p.created_at_epoch})),prompts:_.map(p=>({id:p.id,claude_session_id:p.claude_session_id,project:p.project,prompt:p.prompt_text,created_at:p.created_at,created_at_epoch:p.created_at_epoch}))}}catch(l){return console.error("[SessionStore] Error querying timeline records:",l.message),{observations:[],sessions:[],prompts:[]}}}close(){this.db.close()}};function $(a,e,s){return a==="PreCompact"?e?{continue:!0,suppressOutput:!0}:{continue:!1,stopReason:s.reason||"Pre-compact operation failed",suppressOutput:!0}:a==="SessionStart"?e&&s.context?{continue:!0,suppressOutput:!0,hookSpecificOutput:{hookEventName:"SessionStart",additionalContext:s.context}}:{continue:!0,suppressOutput:!0}:a==="UserPromptSubmit"||a==="PostToolUse"?{continue:!0,suppressOutput:!0}:a==="Stop"?{continue:!0,suppressOutput:!0}:{continue:e,suppressOutput:!0,...s.reason&&!e?{stopReason:s.reason}:{}}}function D(a,e,s={}){let t=$(a,e,s);return JSON.stringify(t)}import O from"path";import{homedir as W}from"os";import{existsSync as G,readFileSync as Y}from"fs";import{execSync as K}from"child_process";var V=100,q=100,J=1e4;function f(){try{let a=O.join(W(),".claude-mem","settings.json");if(G(a)){let e=JSON.parse(Y(a,"utf-8")),s=parseInt(e.env?.CLAUDE_MEM_WORKER_PORT,10);if(!isNaN(s))return s}}catch{}return parseInt(process.env.CLAUDE_MEM_WORKER_PORT||"37777",10)}async function k(){try{let a=f();return(await fetch(`http://127.0.0.1:${a}/health`,{signal:AbortSignal.timeout(V)})).ok}catch{return!1}}async function Q(){let a=Date.now();for(;Date.now()-a<J;){if(await k())return!0;await new Promise(e=>setTimeout(e,q))}return!1}async function x(){if(await k())return;let a=v(),e=O.join(a,"node_modules",".bin","pm2"),s=O.join(a,"ecosystem.config.cjs");if(K(`"${e}" restart "${s}"`,{cwd:a,stdio:"pipe"}),!await Q())throw new Error("Worker failed to become healthy after restart")}async function Z(a){if(!a)throw new Error("newHook requires input");let{session_id:e,cwd:s,prompt:t}=a,r=z.basename(s);await x();let n=new g,o=n.createSDKSession(e,r,t),i=n.incrementPromptCounter(o);n.saveUserPrompt(e,i,t),console.error(`[new-hook] Session ${o}, prompt #${i}`),n.close();let d=f();try{let c=await fetch(`http://127.0.0.1:${d}/sessions/${o}/init`,{method:"POST",headers:{"Content-Type":"application/json"},body:JSON.stringify({project:r,userPrompt:t}),signal:AbortSignal.timeout(5e3)});if(!c.ok){let E=await c.text();throw new Error(`Failed to initialize session: ${c.status} ${E}`)}}catch(c){throw c.cause?.code==="ECONNREFUSED"||c.name==="TimeoutError"||c.message.includes("fetch failed")?new Error("There's a problem with the worker. If you just updated, type `pm2 restart claude-mem-worker` in your terminal to continue"):c}console.log(D("UserPromptSubmit",!0))}var I="";U.on("data",a=>I+=a);U.on("end",async()=>{let a=I?JSON.parse(I):void 0;await Z(a)});
|
`;try{let l=this.db.prepare(E).all(d,c,...i),b=this.db.prepare(T).all(d,c,...i),_=this.db.prepare(S).all(d,c,...i);return{observations:l,sessions:b.map(p=>({id:p.id,sdk_session_id:p.sdk_session_id,project:p.project,request:p.request,completed:p.completed,next_steps:p.next_steps,created_at:p.created_at,created_at_epoch:p.created_at_epoch})),prompts:_.map(p=>({id:p.id,claude_session_id:p.claude_session_id,project:p.project,prompt:p.prompt_text,created_at:p.created_at,created_at_epoch:p.created_at_epoch}))}}catch(l){return console.error("[SessionStore] Error querying timeline records:",l.message),{observations:[],sessions:[],prompts:[]}}}close(){this.db.close()}};function $(a,e,s){return a==="PreCompact"?e?{continue:!0,suppressOutput:!0}:{continue:!1,stopReason:s.reason||"Pre-compact operation failed",suppressOutput:!0}:a==="SessionStart"?e&&s.context?{continue:!0,suppressOutput:!0,hookSpecificOutput:{hookEventName:"SessionStart",additionalContext:s.context}}:{continue:!0,suppressOutput:!0}:a==="UserPromptSubmit"||a==="PostToolUse"?{continue:!0,suppressOutput:!0}:a==="Stop"?{continue:!0,suppressOutput:!0}:{continue:e,suppressOutput:!0,...s.reason&&!e?{stopReason:s.reason}:{}}}function D(a,e,s={}){let t=$(a,e,s);return JSON.stringify(t)}import O from"path";import{homedir as W}from"os";import{existsSync as G,readFileSync as Y}from"fs";import{execSync as K}from"child_process";var V=100,q=100,J=1e4;function f(){try{let a=O.join(W(),".claude-mem","settings.json");if(G(a)){let e=JSON.parse(Y(a,"utf-8")),s=parseInt(e.env?.CLAUDE_MEM_WORKER_PORT,10);if(!isNaN(s))return s}}catch{}return parseInt(process.env.CLAUDE_MEM_WORKER_PORT||"37777",10)}async function k(){try{let a=f();return(await fetch(`http://127.0.0.1:${a}/health`,{signal:AbortSignal.timeout(V)})).ok}catch{return!1}}async function Q(){let a=Date.now();for(;Date.now()-a<J;){if(await k())return!0;await new Promise(e=>setTimeout(e,q))}return!1}async function x(){if(await k())return;let a=v(),e=O.join(a,"node_modules",".bin","pm2"),s=O.join(a,"ecosystem.config.cjs");if(K(`"${e}" start "${s}"`,{cwd:a,stdio:"pipe"}),!await Q())throw new Error("Worker failed to become healthy after restart")}async function Z(a){if(!a)throw new Error("newHook requires input");let{session_id:e,cwd:s,prompt:t}=a,r=z.basename(s);await x();let n=new g,o=n.createSDKSession(e,r,t),i=n.incrementPromptCounter(o);n.saveUserPrompt(e,i,t),console.error(`[new-hook] Session ${o}, prompt #${i}`),n.close();let d=f();try{let c=await fetch(`http://127.0.0.1:${d}/sessions/${o}/init`,{method:"POST",headers:{"Content-Type":"application/json"},body:JSON.stringify({project:r,userPrompt:t}),signal:AbortSignal.timeout(5e3)});if(!c.ok){let E=await c.text();throw new Error(`Failed to initialize session: ${c.status} ${E}`)}}catch(c){throw c.cause?.code==="ECONNREFUSED"||c.name==="TimeoutError"||c.message.includes("fetch failed")?new Error("There's a problem with the worker. If you just updated, type `pm2 restart claude-mem-worker` in your terminal to continue"):c}console.log(D("UserPromptSubmit",!0))}var I="";U.on("data",a=>I+=a);U.on("end",async()=>{let a=I?JSON.parse(I):void 0;await Z(a)});
|
||||||
|
|||||||
@@ -397,4 +397,4 @@ ${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
|
JOIN sdk_sessions s ON up.claude_session_id = s.claude_session_id
|
||||||
WHERE up.created_at_epoch >= ? AND up.created_at_epoch <= ? ${n.replace("project","s.project")}
|
WHERE up.created_at_epoch >= ? AND up.created_at_epoch <= ? ${n.replace("project","s.project")}
|
||||||
ORDER BY up.created_at_epoch ASC
|
ORDER BY up.created_at_epoch ASC
|
||||||
`;try{let T=this.db.prepare(c).all(p,u,...i),S=this.db.prepare(m).all(p,u,...i),_=this.db.prepare(g).all(p,u,...i);return{observations:T,sessions:S.map(d=>({id:d.id,sdk_session_id:d.sdk_session_id,project:d.project,request:d.request,completed:d.completed,next_steps:d.next_steps,created_at:d.created_at,created_at_epoch:d.created_at_epoch})),prompts:_.map(d=>({id:d.id,claude_session_id:d.claude_session_id,project:d.project,prompt:d.prompt_text,created_at:d.created_at,created_at_epoch:d.created_at_epoch}))}}catch(T){return console.error("[SessionStore] Error querying timeline records:",T.message),{observations:[],sessions:[],prompts:[]}}}close(){this.db.close()}};function $(a,e,s){return a==="PreCompact"?e?{continue:!0,suppressOutput:!0}:{continue:!1,stopReason:s.reason||"Pre-compact operation failed",suppressOutput:!0}:a==="SessionStart"?e&&s.context?{continue:!0,suppressOutput:!0,hookSpecificOutput:{hookEventName:"SessionStart",additionalContext:s.context}}:{continue:!0,suppressOutput:!0}:a==="UserPromptSubmit"||a==="PostToolUse"?{continue:!0,suppressOutput:!0}:a==="Stop"?{continue:!0,suppressOutput:!0}:{continue:e,suppressOutput:!0,...s.reason&&!e?{stopReason:s.reason}:{}}}function f(a,e,s={}){let t=$(a,e,s);return JSON.stringify(t)}import I from"path";import{homedir as W}from"os";import{existsSync as G,readFileSync as Y}from"fs";import{execSync as K}from"child_process";var V=100,q=100,J=1e4;function L(){try{let a=I.join(W(),".claude-mem","settings.json");if(G(a)){let e=JSON.parse(Y(a,"utf-8")),s=parseInt(e.env?.CLAUDE_MEM_WORKER_PORT,10);if(!isNaN(s))return s}}catch{}return parseInt(process.env.CLAUDE_MEM_WORKER_PORT||"37777",10)}async function k(){try{let a=L();return(await fetch(`http://127.0.0.1:${a}/health`,{signal:AbortSignal.timeout(V)})).ok}catch{return!1}}async function Q(){let a=Date.now();for(;Date.now()-a<J;){if(await k())return!0;await new Promise(e=>setTimeout(e,q))}return!1}async function x(){if(await k())return;let a=D(),e=I.join(a,"node_modules",".bin","pm2"),s=I.join(a,"ecosystem.config.cjs");if(K(`"${e}" restart "${s}"`,{cwd:a,stdio:"pipe"}),!await Q())throw new Error("Worker failed to become healthy after restart")}var z=new Set(["ListMcpResourcesTool"]);async function Z(a){if(!a)throw new Error("saveHook requires input");let{session_id:e,tool_name:s,tool_input:t,tool_response:r}=a;if(z.has(s)){console.log(f("PostToolUse",!0));return}await x();let o=new R,n=o.createSDKSession(e,"",""),i=o.getPromptCounter(n);o.close();let p=b.formatTool(s,t),u=L();b.dataIn("HOOK",`PostToolUse: ${p}`,{sessionId:n,workerPort:u});try{let c=await fetch(`http://127.0.0.1:${u}/sessions/${n}/observations`,{method:"POST",headers:{"Content-Type":"application/json"},body:JSON.stringify({tool_name:s,tool_input:t!==void 0?JSON.stringify(t):"{}",tool_response:r!==void 0?JSON.stringify(r):"{}",prompt_number:i}),signal:AbortSignal.timeout(2e3)});if(!c.ok){let m=await c.text();throw b.failure("HOOK","Failed to send observation",{sessionId:n,status:c.status},m),new Error(`Failed to send observation to worker: ${c.status} ${m}`)}b.debug("HOOK","Observation sent successfully",{sessionId:n,toolName:s})}catch(c){throw c.cause?.code==="ECONNREFUSED"||c.name==="TimeoutError"||c.message.includes("fetch failed")?new Error("There's a problem with the worker. If you just updated, type `pm2 restart claude-mem-worker` in your terminal to continue"):c}console.log(f("PostToolUse",!0))}var A="";U.on("data",a=>A+=a);U.on("end",async()=>{let a=A?JSON.parse(A):void 0;await Z(a)});
|
`;try{let T=this.db.prepare(c).all(p,u,...i),S=this.db.prepare(m).all(p,u,...i),_=this.db.prepare(g).all(p,u,...i);return{observations:T,sessions:S.map(d=>({id:d.id,sdk_session_id:d.sdk_session_id,project:d.project,request:d.request,completed:d.completed,next_steps:d.next_steps,created_at:d.created_at,created_at_epoch:d.created_at_epoch})),prompts:_.map(d=>({id:d.id,claude_session_id:d.claude_session_id,project:d.project,prompt:d.prompt_text,created_at:d.created_at,created_at_epoch:d.created_at_epoch}))}}catch(T){return console.error("[SessionStore] Error querying timeline records:",T.message),{observations:[],sessions:[],prompts:[]}}}close(){this.db.close()}};function $(a,e,s){return a==="PreCompact"?e?{continue:!0,suppressOutput:!0}:{continue:!1,stopReason:s.reason||"Pre-compact operation failed",suppressOutput:!0}:a==="SessionStart"?e&&s.context?{continue:!0,suppressOutput:!0,hookSpecificOutput:{hookEventName:"SessionStart",additionalContext:s.context}}:{continue:!0,suppressOutput:!0}:a==="UserPromptSubmit"||a==="PostToolUse"?{continue:!0,suppressOutput:!0}:a==="Stop"?{continue:!0,suppressOutput:!0}:{continue:e,suppressOutput:!0,...s.reason&&!e?{stopReason:s.reason}:{}}}function f(a,e,s={}){let t=$(a,e,s);return JSON.stringify(t)}import I from"path";import{homedir as W}from"os";import{existsSync as G,readFileSync as Y}from"fs";import{execSync as K}from"child_process";var V=100,q=100,J=1e4;function L(){try{let a=I.join(W(),".claude-mem","settings.json");if(G(a)){let e=JSON.parse(Y(a,"utf-8")),s=parseInt(e.env?.CLAUDE_MEM_WORKER_PORT,10);if(!isNaN(s))return s}}catch{}return parseInt(process.env.CLAUDE_MEM_WORKER_PORT||"37777",10)}async function k(){try{let a=L();return(await fetch(`http://127.0.0.1:${a}/health`,{signal:AbortSignal.timeout(V)})).ok}catch{return!1}}async function Q(){let a=Date.now();for(;Date.now()-a<J;){if(await k())return!0;await new Promise(e=>setTimeout(e,q))}return!1}async function x(){if(await k())return;let a=D(),e=I.join(a,"node_modules",".bin","pm2"),s=I.join(a,"ecosystem.config.cjs");if(K(`"${e}" start "${s}"`,{cwd:a,stdio:"pipe"}),!await Q())throw new Error("Worker failed to become healthy after restart")}var z=new Set(["ListMcpResourcesTool"]);async function Z(a){if(!a)throw new Error("saveHook requires input");let{session_id:e,tool_name:s,tool_input:t,tool_response:r}=a;if(z.has(s)){console.log(f("PostToolUse",!0));return}await x();let o=new R,n=o.createSDKSession(e,"",""),i=o.getPromptCounter(n);o.close();let p=b.formatTool(s,t),u=L();b.dataIn("HOOK",`PostToolUse: ${p}`,{sessionId:n,workerPort:u});try{let c=await fetch(`http://127.0.0.1:${u}/sessions/${n}/observations`,{method:"POST",headers:{"Content-Type":"application/json"},body:JSON.stringify({tool_name:s,tool_input:t!==void 0?JSON.stringify(t):"{}",tool_response:r!==void 0?JSON.stringify(r):"{}",prompt_number:i}),signal:AbortSignal.timeout(2e3)});if(!c.ok){let m=await c.text();throw b.failure("HOOK","Failed to send observation",{sessionId:n,status:c.status},m),new Error(`Failed to send observation to worker: ${c.status} ${m}`)}b.debug("HOOK","Observation sent successfully",{sessionId:n,toolName:s})}catch(c){throw c.cause?.code==="ECONNREFUSED"||c.name==="TimeoutError"||c.message.includes("fetch failed")?new Error("There's a problem with the worker. If you just updated, type `pm2 restart claude-mem-worker` in your terminal to continue"):c}console.log(f("PostToolUse",!0))}var A="";U.on("data",a=>A+=a);U.on("end",async()=>{let a=A?JSON.parse(A):void 0;await Z(a)});
|
||||||
|
|||||||
@@ -397,4 +397,4 @@ ${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
|
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")}
|
WHERE up.created_at_epoch >= ? AND up.created_at_epoch <= ? ${o.replace("project","s.project")}
|
||||||
ORDER BY up.created_at_epoch ASC
|
ORDER BY up.created_at_epoch ASC
|
||||||
`;try{let l=this.db.prepare(m).all(p,u,...i),b=this.db.prepare(T).all(p,u,...i),c=this.db.prepare(g).all(p,u,...i);return{observations:l,sessions:b.map(d=>({id:d.id,sdk_session_id:d.sdk_session_id,project:d.project,request:d.request,completed:d.completed,next_steps:d.next_steps,created_at:d.created_at,created_at_epoch:d.created_at_epoch})),prompts:c.map(d=>({id:d.id,claude_session_id:d.claude_session_id,project:d.project,prompt:d.prompt_text,created_at:d.created_at,created_at_epoch:d.created_at_epoch}))}}catch(l){return console.error("[SessionStore] Error querying timeline records:",l.message),{observations:[],sessions:[],prompts:[]}}}close(){this.db.close()}};function $(a,e,s){return a==="PreCompact"?e?{continue:!0,suppressOutput:!0}:{continue:!1,stopReason:s.reason||"Pre-compact operation failed",suppressOutput:!0}:a==="SessionStart"?e&&s.context?{continue:!0,suppressOutput:!0,hookSpecificOutput:{hookEventName:"SessionStart",additionalContext:s.context}}:{continue:!0,suppressOutput:!0}:a==="UserPromptSubmit"||a==="PostToolUse"?{continue:!0,suppressOutput:!0}:a==="Stop"?{continue:!0,suppressOutput:!0}:{continue:e,suppressOutput:!0,...s.reason&&!e?{stopReason:s.reason}:{}}}function D(a,e,s={}){let t=$(a,e,s);return JSON.stringify(t)}import f from"path";import{homedir as W}from"os";import{existsSync as G,readFileSync as Y}from"fs";import{execSync as K}from"child_process";var q=100,V=100,J=1e4;function I(){try{let a=f.join(W(),".claude-mem","settings.json");if(G(a)){let e=JSON.parse(Y(a,"utf-8")),s=parseInt(e.env?.CLAUDE_MEM_WORKER_PORT,10);if(!isNaN(s))return s}}catch{}return parseInt(process.env.CLAUDE_MEM_WORKER_PORT||"37777",10)}async function k(){try{let a=I();return(await fetch(`http://127.0.0.1:${a}/health`,{signal:AbortSignal.timeout(q)})).ok}catch{return!1}}async function Q(){let a=Date.now();for(;Date.now()-a<J;){if(await k())return!0;await new Promise(e=>setTimeout(e,V))}return!1}async function x(){if(await k())return;let a=v(),e=f.join(a,"node_modules",".bin","pm2"),s=f.join(a,"ecosystem.config.cjs");if(K(`"${e}" restart "${s}"`,{cwd:a,stdio:"pipe"}),!await Q())throw new Error("Worker failed to become healthy after restart")}async function z(a){if(!a)throw new Error("summaryHook requires input");let{session_id:e}=a;await x();let s=new R,t=s.createSDKSession(e,"",""),r=s.getPromptCounter(t);s.close();let n=I();S.dataIn("HOOK","Stop: Requesting summary",{sessionId:t,workerPort:n,promptNumber:r});try{let o=await fetch(`http://127.0.0.1:${n}/sessions/${t}/summarize`,{method:"POST",headers:{"Content-Type":"application/json"},body:JSON.stringify({prompt_number:r}),signal:AbortSignal.timeout(2e3)});if(!o.ok){let i=await o.text();throw S.failure("HOOK","Failed to generate summary",{sessionId:t,status:o.status},i),new Error(`Failed to request summary from worker: ${o.status} ${i}`)}S.debug("HOOK","Summary request sent successfully",{sessionId:t})}catch(o){throw o.cause?.code==="ECONNREFUSED"||o.name==="TimeoutError"||o.message.includes("fetch failed")?new Error("There's a problem with the worker. If you just updated, type `pm2 restart claude-mem-worker` in your terminal to continue"):o}finally{await fetch(`http://127.0.0.1:${n}/api/processing`,{method:"POST",headers:{"Content-Type":"application/json"},body:JSON.stringify({isProcessing:!1})})}console.log(D("Stop",!0))}var L="";U.on("data",a=>L+=a);U.on("end",async()=>{let a=L?JSON.parse(L):void 0;await z(a)});
|
`;try{let l=this.db.prepare(m).all(p,u,...i),b=this.db.prepare(T).all(p,u,...i),c=this.db.prepare(g).all(p,u,...i);return{observations:l,sessions:b.map(d=>({id:d.id,sdk_session_id:d.sdk_session_id,project:d.project,request:d.request,completed:d.completed,next_steps:d.next_steps,created_at:d.created_at,created_at_epoch:d.created_at_epoch})),prompts:c.map(d=>({id:d.id,claude_session_id:d.claude_session_id,project:d.project,prompt:d.prompt_text,created_at:d.created_at,created_at_epoch:d.created_at_epoch}))}}catch(l){return console.error("[SessionStore] Error querying timeline records:",l.message),{observations:[],sessions:[],prompts:[]}}}close(){this.db.close()}};function $(a,e,s){return a==="PreCompact"?e?{continue:!0,suppressOutput:!0}:{continue:!1,stopReason:s.reason||"Pre-compact operation failed",suppressOutput:!0}:a==="SessionStart"?e&&s.context?{continue:!0,suppressOutput:!0,hookSpecificOutput:{hookEventName:"SessionStart",additionalContext:s.context}}:{continue:!0,suppressOutput:!0}:a==="UserPromptSubmit"||a==="PostToolUse"?{continue:!0,suppressOutput:!0}:a==="Stop"?{continue:!0,suppressOutput:!0}:{continue:e,suppressOutput:!0,...s.reason&&!e?{stopReason:s.reason}:{}}}function D(a,e,s={}){let t=$(a,e,s);return JSON.stringify(t)}import f from"path";import{homedir as W}from"os";import{existsSync as G,readFileSync as Y}from"fs";import{execSync as K}from"child_process";var q=100,V=100,J=1e4;function I(){try{let a=f.join(W(),".claude-mem","settings.json");if(G(a)){let e=JSON.parse(Y(a,"utf-8")),s=parseInt(e.env?.CLAUDE_MEM_WORKER_PORT,10);if(!isNaN(s))return s}}catch{}return parseInt(process.env.CLAUDE_MEM_WORKER_PORT||"37777",10)}async function k(){try{let a=I();return(await fetch(`http://127.0.0.1:${a}/health`,{signal:AbortSignal.timeout(q)})).ok}catch{return!1}}async function Q(){let a=Date.now();for(;Date.now()-a<J;){if(await k())return!0;await new Promise(e=>setTimeout(e,V))}return!1}async function x(){if(await k())return;let a=v(),e=f.join(a,"node_modules",".bin","pm2"),s=f.join(a,"ecosystem.config.cjs");if(K(`"${e}" start "${s}"`,{cwd:a,stdio:"pipe"}),!await Q())throw new Error("Worker failed to become healthy after restart")}async function z(a){if(!a)throw new Error("summaryHook requires input");let{session_id:e}=a;await x();let s=new R,t=s.createSDKSession(e,"",""),r=s.getPromptCounter(t);s.close();let n=I();S.dataIn("HOOK","Stop: Requesting summary",{sessionId:t,workerPort:n,promptNumber:r});try{let o=await fetch(`http://127.0.0.1:${n}/sessions/${t}/summarize`,{method:"POST",headers:{"Content-Type":"application/json"},body:JSON.stringify({prompt_number:r}),signal:AbortSignal.timeout(2e3)});if(!o.ok){let i=await o.text();throw S.failure("HOOK","Failed to generate summary",{sessionId:t,status:o.status},i),new Error(`Failed to request summary from worker: ${o.status} ${i}`)}S.debug("HOOK","Summary request sent successfully",{sessionId:t})}catch(o){throw o.cause?.code==="ECONNREFUSED"||o.name==="TimeoutError"||o.message.includes("fetch failed")?new Error("There's a problem with the worker. If you just updated, type `pm2 restart claude-mem-worker` in your terminal to continue"):o}finally{await fetch(`http://127.0.0.1:${n}/api/processing`,{method:"POST",headers:{"Content-Type":"application/json"},body:JSON.stringify({isProcessing:!1})})}console.log(D("Stop",!0))}var L="";U.on("data",a=>L+=a);U.on("end",async()=>{let a=L?JSON.parse(L):void 0;await z(a)});
|
||||||
|
|||||||
File diff suppressed because one or more lines are too long
+94
-66
@@ -4,13 +4,38 @@
|
|||||||
*/
|
*/
|
||||||
|
|
||||||
import path from 'path';
|
import path from 'path';
|
||||||
|
import { homedir } from 'os';
|
||||||
|
import { existsSync, readFileSync } from 'fs';
|
||||||
import { stdin } from 'process';
|
import { stdin } from 'process';
|
||||||
import { SessionStore } from '../services/sqlite/SessionStore.js';
|
import { SessionStore } from '../services/sqlite/SessionStore.js';
|
||||||
|
|
||||||
// Configuration: Read from environment or use defaults
|
/**
|
||||||
const DISPLAY_OBSERVATION_COUNT = parseInt(process.env.CLAUDE_MEM_CONTEXT_OBSERVATIONS || '50', 10);
|
* Get context depth from settings
|
||||||
// Summaries are supplementary - show last 10 for context but not configurable
|
* Priority: ~/.claude/settings.json > env var > default
|
||||||
const DISPLAY_SESSION_COUNT = 10;
|
*/
|
||||||
|
function getContextDepth(): number {
|
||||||
|
try {
|
||||||
|
const settingsPath = path.join(homedir(), '.claude', 'settings.json');
|
||||||
|
if (existsSync(settingsPath)) {
|
||||||
|
const settings = JSON.parse(readFileSync(settingsPath, 'utf-8'));
|
||||||
|
if (settings.env?.CLAUDE_MEM_CONTEXT_OBSERVATIONS) {
|
||||||
|
const count = parseInt(settings.env.CLAUDE_MEM_CONTEXT_OBSERVATIONS, 10);
|
||||||
|
if (!isNaN(count) && count > 0) {
|
||||||
|
return count;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} catch {
|
||||||
|
// Fall through to env var or default
|
||||||
|
}
|
||||||
|
return parseInt(process.env.CLAUDE_MEM_CONTEXT_OBSERVATIONS || '50', 10);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Configuration: Read from settings.json or environment
|
||||||
|
const DISPLAY_OBSERVATION_COUNT = getContextDepth();
|
||||||
|
const DISPLAY_SESSION_COUNT = 10; // Recent sessions for timeline context
|
||||||
|
const CHARS_PER_TOKEN_ESTIMATE = 4; // Rough estimate for token counting
|
||||||
|
const SUMMARY_LOOKAHEAD = 1; // Fetch one extra summary for offset calculation
|
||||||
|
|
||||||
export interface SessionStartInput {
|
export interface SessionStartInput {
|
||||||
session_id?: string;
|
session_id?: string;
|
||||||
@@ -50,11 +75,27 @@ interface Observation {
|
|||||||
created_at_epoch: number;
|
created_at_epoch: number;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
interface SessionSummary {
|
||||||
|
id: number;
|
||||||
|
sdk_session_id: string;
|
||||||
|
request: string | null;
|
||||||
|
investigated: string | null;
|
||||||
|
learned: string | null;
|
||||||
|
completed: string | null;
|
||||||
|
next_steps: string | null;
|
||||||
|
created_at: string;
|
||||||
|
created_at_epoch: number;
|
||||||
|
}
|
||||||
|
|
||||||
// Helper: Parse JSON array safely
|
// Helper: Parse JSON array safely
|
||||||
function parseJsonArray(json: string | null): string[] {
|
function parseJsonArray(json: string | null): string[] {
|
||||||
if (!json) return [];
|
if (!json) return [];
|
||||||
const parsed = JSON.parse(json);
|
try {
|
||||||
return Array.isArray(parsed) ? parsed : [];
|
const parsed = JSON.parse(json);
|
||||||
|
return Array.isArray(parsed) ? parsed : [];
|
||||||
|
} catch (err) {
|
||||||
|
return [];
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Helper: Format date with time
|
// Helper: Format date with time
|
||||||
@@ -92,8 +133,7 @@ function formatDate(dateStr: string): string {
|
|||||||
// Helper: Estimate token count for text
|
// Helper: Estimate token count for text
|
||||||
function estimateTokens(text: string | null): number {
|
function estimateTokens(text: string | null): number {
|
||||||
if (!text) return 0;
|
if (!text) return 0;
|
||||||
// Rough estimate: ~4 characters per token
|
return Math.ceil(text.length / CHARS_PER_TOKEN_ESTIMATE);
|
||||||
return Math.ceil(text.length / 4);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// Helper: Convert absolute paths to relative paths
|
// Helper: Convert absolute paths to relative paths
|
||||||
@@ -104,6 +144,16 @@ function toRelativePath(filePath: string, cwd: string): string {
|
|||||||
return filePath;
|
return filePath;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Helper: Render a summary field (investigated, learned, etc.)
|
||||||
|
function renderSummaryField(label: string, value: string | null, color: string, useColors: boolean): string[] {
|
||||||
|
if (!value) return [];
|
||||||
|
|
||||||
|
if (useColors) {
|
||||||
|
return [`${color}${label}:${colors.reset} ${value}`, ''];
|
||||||
|
}
|
||||||
|
return [`**${label}**: ${value}`, ''];
|
||||||
|
}
|
||||||
|
|
||||||
// Helper: Get all observations for given sessions
|
// Helper: Get all observations for given sessions
|
||||||
function getObservations(db: SessionStore, sessionIds: string[]): Observation[] {
|
function getObservations(db: SessionStore, sessionIds: string[]): Observation[] {
|
||||||
if (sessionIds.length === 0) return [];
|
if (sessionIds.length === 0) return [];
|
||||||
@@ -125,7 +175,7 @@ function getObservations(db: SessionStore, sessionIds: string[]): Observation[]
|
|||||||
/**
|
/**
|
||||||
* Context Hook Main Logic
|
* Context Hook Main Logic
|
||||||
*/
|
*/
|
||||||
async function contextHook(input?: SessionStartInput, useColors: boolean = false, useIndexView: boolean = false): Promise<string> {
|
async function contextHook(input?: SessionStartInput, useColors: boolean = false): Promise<string> {
|
||||||
const cwd = input?.cwd ?? process.cwd();
|
const cwd = input?.cwd ?? process.cwd();
|
||||||
const project = cwd ? path.basename(cwd) : 'unknown-project';
|
const project = cwd ? path.basename(cwd) : 'unknown-project';
|
||||||
|
|
||||||
@@ -146,13 +196,14 @@ async function contextHook(input?: SessionStartInput, useColors: boolean = false
|
|||||||
`).all(project, DISPLAY_OBSERVATION_COUNT) as Observation[];
|
`).all(project, DISPLAY_OBSERVATION_COUNT) as Observation[];
|
||||||
|
|
||||||
// Get recent summaries (optional - may not exist for recent sessions)
|
// Get recent summaries (optional - may not exist for recent sessions)
|
||||||
|
// Fetch one extra for offset calculation
|
||||||
const recentSummaries = db.db.prepare(`
|
const recentSummaries = db.db.prepare(`
|
||||||
SELECT id, sdk_session_id, request, investigated, learned, completed, next_steps, created_at, created_at_epoch
|
SELECT id, sdk_session_id, request, investigated, learned, completed, next_steps, created_at, created_at_epoch
|
||||||
FROM session_summaries
|
FROM session_summaries
|
||||||
WHERE project = ?
|
WHERE project = ?
|
||||||
ORDER BY created_at_epoch DESC
|
ORDER BY created_at_epoch DESC
|
||||||
LIMIT ?
|
LIMIT ?
|
||||||
`).all(project, DISPLAY_SESSION_COUNT + 1) as Array<{ id: number; sdk_session_id: string; request: string | null; investigated: string | null; learned: string | null; completed: string | null; next_steps: string | null; created_at: string; created_at_epoch: number }>;
|
`).all(project, DISPLAY_SESSION_COUNT + SUMMARY_LOOKAHEAD) as SessionSummary[];
|
||||||
|
|
||||||
// If we have neither observations nor summaries, show empty state
|
// If we have neither observations nor summaries, show empty state
|
||||||
if (allObservations.length === 0 && recentSummaries.length === 0) {
|
if (allObservations.length === 0 && recentSummaries.length === 0) {
|
||||||
@@ -210,28 +261,37 @@ async function contextHook(input?: SessionStartInput, useColors: boolean = false
|
|||||||
output.push('');
|
output.push('');
|
||||||
}
|
}
|
||||||
|
|
||||||
// Create unified timeline with both observations and summaries
|
// Prepare summaries for timeline display
|
||||||
|
// The most recent summary shows full details (investigated, learned, etc.)
|
||||||
|
// Older summaries only show as timeline markers (no link needed)
|
||||||
const mostRecentSummaryId = recentSummaries[0]?.id;
|
const mostRecentSummaryId = recentSummaries[0]?.id;
|
||||||
|
|
||||||
// Create offset summaries
|
interface SummaryTimelineItem extends SessionSummary {
|
||||||
const summariesWithOffset = displaySummaries.map((summary, i) => {
|
displayEpoch: number;
|
||||||
// Most recent keeps its own time, others offset to next summary's time
|
displayTime: string;
|
||||||
const nextSummary = i === 0 ? null : recentSummaries[i + 1];
|
shouldShowLink: boolean;
|
||||||
|
}
|
||||||
|
|
||||||
|
const summariesForTimeline: SummaryTimelineItem[] = displaySummaries.map((summary, i) => {
|
||||||
|
// For visual grouping, display each summary at the time range it covers
|
||||||
|
// Most recent: shows at its own time (current session)
|
||||||
|
// Older: shows at the previous (older) summary's time to mark the session range
|
||||||
|
const olderSummary = i === 0 ? null : recentSummaries[i + 1];
|
||||||
return {
|
return {
|
||||||
...summary,
|
...summary,
|
||||||
displayEpoch: nextSummary ? nextSummary.created_at_epoch : summary.created_at_epoch,
|
displayEpoch: olderSummary ? olderSummary.created_at_epoch : summary.created_at_epoch,
|
||||||
displayTime: nextSummary ? nextSummary.created_at : summary.created_at,
|
displayTime: olderSummary ? olderSummary.created_at : summary.created_at,
|
||||||
isMostRecent: summary.id === mostRecentSummaryId
|
shouldShowLink: summary.id !== mostRecentSummaryId
|
||||||
};
|
};
|
||||||
});
|
});
|
||||||
|
|
||||||
type TimelineItem =
|
type TimelineItem =
|
||||||
| { type: 'observation'; data: Observation }
|
| { type: 'observation'; data: Observation }
|
||||||
| { type: 'summary'; data: typeof summariesWithOffset[0] };
|
| { type: 'summary'; data: SummaryTimelineItem };
|
||||||
|
|
||||||
const timeline: TimelineItem[] = [
|
const timeline: TimelineItem[] = [
|
||||||
...timelineObs.map(obs => ({ type: 'observation' as const, data: obs })),
|
...timelineObs.map(obs => ({ type: 'observation' as const, data: obs })),
|
||||||
...summariesWithOffset.map(summary => ({ type: 'summary' as const, data: summary }))
|
...summariesForTimeline.map(summary => ({ type: 'summary' as const, data: summary }))
|
||||||
];
|
];
|
||||||
|
|
||||||
// Sort chronologically
|
// Sort chronologically
|
||||||
@@ -242,18 +302,18 @@ async function contextHook(input?: SessionStartInput, useColors: boolean = false
|
|||||||
});
|
});
|
||||||
|
|
||||||
// Group by day for rendering
|
// Group by day for rendering
|
||||||
const dayTimelines = new Map<string, typeof timeline>();
|
const itemsByDay = new Map<string, TimelineItem[]>();
|
||||||
for (const item of timeline) {
|
for (const item of timeline) {
|
||||||
const itemDate = item.type === 'observation' ? item.data.created_at : item.data.displayTime;
|
const itemDate = item.type === 'observation' ? item.data.created_at : item.data.displayTime;
|
||||||
const day = formatDate(itemDate);
|
const day = formatDate(itemDate);
|
||||||
if (!dayTimelines.has(day)) {
|
if (!itemsByDay.has(day)) {
|
||||||
dayTimelines.set(day, []);
|
itemsByDay.set(day, []);
|
||||||
}
|
}
|
||||||
dayTimelines.get(day)!.push(item);
|
itemsByDay.get(day)!.push(item);
|
||||||
}
|
}
|
||||||
|
|
||||||
// Sort days chronologically
|
// Sort days chronologically
|
||||||
const sortedDays = Array.from(dayTimelines.entries()).sort((a, b) => {
|
const sortedDays = Array.from(itemsByDay.entries()).sort((a, b) => {
|
||||||
const aDate = new Date(a[0]).getTime();
|
const aDate = new Date(a[0]).getTime();
|
||||||
const bDate = new Date(b[0]).getTime();
|
const bDate = new Date(b[0]).getTime();
|
||||||
return aDate - bDate;
|
return aDate - bDate;
|
||||||
@@ -288,7 +348,7 @@ async function contextHook(input?: SessionStartInput, useColors: boolean = false
|
|||||||
// Render summary
|
// Render summary
|
||||||
const summary = item.data;
|
const summary = item.data;
|
||||||
const summaryTitle = `${summary.request || 'Session started'} (${formatDateTime(summary.displayTime)})`;
|
const summaryTitle = `${summary.request || 'Session started'} (${formatDateTime(summary.displayTime)})`;
|
||||||
const link = summary.isMostRecent ? '' : `claude-mem://session-summary/${summary.id}`;
|
const link = summary.shouldShowLink ? `claude-mem://session-summary/${summary.id}` : '';
|
||||||
|
|
||||||
if (useColors) {
|
if (useColors) {
|
||||||
const linkPart = link ? `${colors.dim}[${link}]${colors.reset}` : '';
|
const linkPart = link ? `${colors.dim}[${link}]${colors.reset}` : '';
|
||||||
@@ -383,41 +443,10 @@ async function contextHook(input?: SessionStartInput, useColors: boolean = false
|
|||||||
// Add full summary details for most recent session
|
// Add full summary details for most recent session
|
||||||
const mostRecentSummary = recentSummaries[0];
|
const mostRecentSummary = recentSummaries[0];
|
||||||
if (mostRecentSummary && (mostRecentSummary.investigated || mostRecentSummary.learned || mostRecentSummary.completed || mostRecentSummary.next_steps)) {
|
if (mostRecentSummary && (mostRecentSummary.investigated || mostRecentSummary.learned || mostRecentSummary.completed || mostRecentSummary.next_steps)) {
|
||||||
if (mostRecentSummary.investigated) {
|
output.push(...renderSummaryField('Investigated', mostRecentSummary.investigated, colors.blue, useColors));
|
||||||
if (useColors) {
|
output.push(...renderSummaryField('Learned', mostRecentSummary.learned, colors.yellow, useColors));
|
||||||
output.push(`${colors.blue}Investigated:${colors.reset} ${mostRecentSummary.investigated}`);
|
output.push(...renderSummaryField('Completed', mostRecentSummary.completed, colors.green, useColors));
|
||||||
} else {
|
output.push(...renderSummaryField('Next Steps', mostRecentSummary.next_steps, colors.magenta, useColors));
|
||||||
output.push(`**Investigated**: ${mostRecentSummary.investigated}`);
|
|
||||||
}
|
|
||||||
output.push('');
|
|
||||||
}
|
|
||||||
|
|
||||||
if (mostRecentSummary.learned) {
|
|
||||||
if (useColors) {
|
|
||||||
output.push(`${colors.yellow}Learned:${colors.reset} ${mostRecentSummary.learned}`);
|
|
||||||
} else {
|
|
||||||
output.push(`**Learned**: ${mostRecentSummary.learned}`);
|
|
||||||
}
|
|
||||||
output.push('');
|
|
||||||
}
|
|
||||||
|
|
||||||
if (mostRecentSummary.completed) {
|
|
||||||
if (useColors) {
|
|
||||||
output.push(`${colors.green}Completed:${colors.reset} ${mostRecentSummary.completed}`);
|
|
||||||
} else {
|
|
||||||
output.push(`**Completed**: ${mostRecentSummary.completed}`);
|
|
||||||
}
|
|
||||||
output.push('');
|
|
||||||
}
|
|
||||||
|
|
||||||
if (mostRecentSummary.next_steps) {
|
|
||||||
if (useColors) {
|
|
||||||
output.push(`${colors.magenta}Next Steps:${colors.reset} ${mostRecentSummary.next_steps}`);
|
|
||||||
} else {
|
|
||||||
output.push(`**Next Steps**: ${mostRecentSummary.next_steps}`);
|
|
||||||
}
|
|
||||||
output.push('');
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// Footer with MCP search instructions
|
// Footer with MCP search instructions
|
||||||
@@ -433,12 +462,11 @@ async function contextHook(input?: SessionStartInput, useColors: boolean = false
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Entry Point - handle stdin/stdout
|
// Entry Point - handle stdin/stdout
|
||||||
const useIndexView = process.argv.includes('--index');
|
const forceColors = process.argv.includes('--colors');
|
||||||
const forceColors = process.argv.includes('--colors'); // Add this line
|
|
||||||
|
|
||||||
if (stdin.isTTY || forceColors) { // Modify this line to include forceColors
|
if (stdin.isTTY || forceColors) {
|
||||||
// Running manually from terminal - print formatted output with colors
|
// Running manually from terminal - print formatted output with colors
|
||||||
contextHook(undefined, true, useIndexView).then(contextOutput => {
|
contextHook(undefined, true).then(contextOutput => {
|
||||||
console.log(contextOutput);
|
console.log(contextOutput);
|
||||||
process.exit(0);
|
process.exit(0);
|
||||||
});
|
});
|
||||||
@@ -448,7 +476,7 @@ if (stdin.isTTY || forceColors) { // Modify this line to include forceColors
|
|||||||
stdin.on('data', (chunk) => input += chunk);
|
stdin.on('data', (chunk) => input += chunk);
|
||||||
stdin.on('end', async () => {
|
stdin.on('end', async () => {
|
||||||
const parsed = input.trim() ? JSON.parse(input) : undefined;
|
const parsed = input.trim() ? JSON.parse(input) : undefined;
|
||||||
const contextOutput = await contextHook(parsed, false, useIndexView);
|
const contextOutput = await contextHook(parsed, false);
|
||||||
const result = {
|
const result = {
|
||||||
hookSpecificOutput: {
|
hookSpecificOutput: {
|
||||||
hookEventName: "SessionStart",
|
hookEventName: "SessionStart",
|
||||||
|
|||||||
+20
-13
@@ -29,6 +29,8 @@ CRITICAL: Record what was BUILT/FIXED/DEPLOYED/CONFIGURED, not what you (the obs
|
|||||||
User's Goal: ${userPrompt}
|
User's Goal: ${userPrompt}
|
||||||
Date: ${new Date().toISOString().split('T')[0]}
|
Date: ${new Date().toISOString().split('T')[0]}
|
||||||
|
|
||||||
|
SESSION LIFECYCLE: You will observe tool executions, create observations, generate progress summaries when requested, and receive continuation prompts as the session progresses.
|
||||||
|
|
||||||
WHAT TO RECORD
|
WHAT TO RECORD
|
||||||
--------------
|
--------------
|
||||||
Focus on deliverables and capabilities:
|
Focus on deliverables and capabilities:
|
||||||
@@ -154,26 +156,31 @@ export function buildObservationPrompt(obs: Observation): string {
|
|||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Build prompt to generate request summary
|
* Build prompt to generate progress summary
|
||||||
*/
|
*/
|
||||||
export function buildSummaryPrompt(session: SDKSession): string {
|
export function buildSummaryPrompt(session: SDKSession): string {
|
||||||
return `THIS REQUEST'S SUMMARY
|
return `PROGRESS SUMMARY CHECKPOINT
|
||||||
===============
|
===========================
|
||||||
Think about the last request, and write a summary of what was done, what was learned, and what's next.
|
Write progress notes of what was done, what was learned, and what's next. This is a checkpoint to capture progress so far.
|
||||||
|
|
||||||
IMPORTANT! DO NOT summarize the observation process itself - you are summarizing a DIFFERENT claude code session, not this one.
|
IMPORTANT! DO NOT summarize the observation process itself - you are summarizing a DIFFERENT claude code session, not this one.
|
||||||
|
|
||||||
User's Original Request: ${session.user_prompt}
|
|
||||||
|
|
||||||
Respond in this XML format:
|
Respond in this XML format:
|
||||||
<summary>
|
<summary>
|
||||||
<request>[What did the user request? Form a title that reflects the actual request: ${session.user_prompt}]</request>
|
<request>[Short title related to the most recent prompt]</request>
|
||||||
<investigated>[Was anything explored? What was it?]</investigated>
|
<investigated>[What has been explored so far? What was examined?]</investigated>
|
||||||
<learned>[Did you learn anything? What was learned about how things work?]</learned>
|
<learned>[What have you learned about how things work?]</learned>
|
||||||
<completed>[Did you do any work? What shipped? What does the system now do?]</completed>
|
<completed>[What work has been completed so far? What has shipped or changed?]</completed>
|
||||||
<next_steps>[What are the next steps?]</next_steps>
|
<next_steps>[What are you actively working on or planning to work on next in this session?]</next_steps>
|
||||||
<notes>[Additional insights]</notes>
|
<notes>[Additional insights or observations about the current progress]</notes>
|
||||||
</summary>
|
</summary>
|
||||||
|
|
||||||
IMPORTANT: This is not the end of the session. You will receive more requests to process, and more tool usages to observe and record. The summary helps keep track of progress. Always write at least a minimal summary explaining where we are at currently, even if you didn't learn anything new or complete any work.`;
|
FRAMING: This is a mid-session progress checkpoint. The session is ongoing - you may receive more requests and tool executions after this summary. Write "next_steps" as the current trajectory of work (what's actively being worked on or coming up next), not as post-session future work. Always write at least a minimal summary explaining current progress, even if work is still in early stages.`;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Build prompt for continuation of existing session
|
||||||
|
*/
|
||||||
|
export function buildContinuationPrompt(userPrompt: string, promptNumber: number): string {
|
||||||
|
return `User's request #${promptNumber}: ${userPrompt}`;
|
||||||
}
|
}
|
||||||
@@ -16,7 +16,7 @@ import { DatabaseManager } from './DatabaseManager.js';
|
|||||||
import { SessionManager } from './SessionManager.js';
|
import { SessionManager } from './SessionManager.js';
|
||||||
import { logger } from '../../utils/logger.js';
|
import { logger } from '../../utils/logger.js';
|
||||||
import { parseObservations, parseSummary } from '../../sdk/parser.js';
|
import { parseObservations, parseSummary } from '../../sdk/parser.js';
|
||||||
import { buildInitPrompt, buildObservationPrompt, buildSummaryPrompt } from '../../sdk/prompts.js';
|
import { buildInitPrompt, buildObservationPrompt, buildSummaryPrompt, buildContinuationPrompt } from '../../sdk/prompts.js';
|
||||||
import type { ActiveSession, SDKUserMessage, PendingMessage } from '../worker-types.js';
|
import type { ActiveSession, SDKUserMessage, PendingMessage } from '../worker-types.js';
|
||||||
|
|
||||||
// Import Agent SDK (assumes it's installed)
|
// Import Agent SDK (assumes it's installed)
|
||||||
@@ -110,12 +110,14 @@ export class SDKAgent {
|
|||||||
* Create event-driven message generator (yields messages from SessionManager)
|
* Create event-driven message generator (yields messages from SessionManager)
|
||||||
*/
|
*/
|
||||||
private async *createMessageGenerator(session: ActiveSession): AsyncIterableIterator<SDKUserMessage> {
|
private async *createMessageGenerator(session: ActiveSession): AsyncIterableIterator<SDKUserMessage> {
|
||||||
// Yield initial user prompt with context
|
// Yield initial user prompt with context (or continuation if prompt #2+)
|
||||||
yield {
|
yield {
|
||||||
type: 'user',
|
type: 'user',
|
||||||
message: {
|
message: {
|
||||||
role: 'user',
|
role: 'user',
|
||||||
content: buildInitPrompt(session.project, session.claudeSessionId, session.userPrompt)
|
content: session.lastPromptNumber === 1
|
||||||
|
? buildInitPrompt(session.project, session.claudeSessionId, session.userPrompt)
|
||||||
|
: buildContinuationPrompt(session.userPrompt, session.lastPromptNumber)
|
||||||
},
|
},
|
||||||
session_id: session.claudeSessionId,
|
session_id: session.claudeSessionId,
|
||||||
parent_tool_use_id: null,
|
parent_tool_use_id: null,
|
||||||
|
|||||||
@@ -45,7 +45,7 @@ export class SessionManager {
|
|||||||
pendingMessages: [],
|
pendingMessages: [],
|
||||||
abortController: new AbortController(),
|
abortController: new AbortController(),
|
||||||
generatorPromise: null,
|
generatorPromise: null,
|
||||||
lastPromptNumber: 0,
|
lastPromptNumber: this.dbManager.getSessionStore().getPromptCounter(sessionDbId),
|
||||||
startTime: Date.now()
|
startTime: Date.now()
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|||||||
@@ -70,7 +70,7 @@ export async function ensureWorkerRunning(): Promise<void> {
|
|||||||
const pm2Path = path.join(packageRoot, "node_modules", ".bin", "pm2");
|
const pm2Path = path.join(packageRoot, "node_modules", ".bin", "pm2");
|
||||||
const ecosystemPath = path.join(packageRoot, "ecosystem.config.cjs");
|
const ecosystemPath = path.join(packageRoot, "ecosystem.config.cjs");
|
||||||
|
|
||||||
execSync(`"${pm2Path}" restart "${ecosystemPath}"`, {
|
execSync(`"${pm2Path}" start "${ecosystemPath}"`, {
|
||||||
cwd: packageRoot,
|
cwd: packageRoot,
|
||||||
stdio: 'pipe'
|
stdio: 'pipe'
|
||||||
});
|
});
|
||||||
|
|||||||
Reference in New Issue
Block a user