diff --git a/plugin/scripts/context-hook.js b/plugin/scripts/context-hook.js index d3a4732a..e0a84d65 100755 --- a/plugin/scripts/context-hook.js +++ b/plugin/scripts/context-hook.js @@ -1,7 +1,7 @@ #!/usr/bin/env node -import C from"path";import q from"better-sqlite3";import{join as _,dirname as W,basename as z}from"path";import{homedir as U}from"os";import{existsSync as te,mkdirSync as j}from"fs";import{fileURLToPath as H}from"url";function B(){return typeof __dirname<"u"?__dirname:W(H(import.meta.url))}var Y=B(),m=process.env.CLAUDE_MEM_DATA_DIR||_(U(),".claude-mem"),O=process.env.CLAUDE_CONFIG_DIR||_(U(),".claude"),ne=_(m,"archives"),ie=_(m,"logs"),oe=_(m,"trash"),ae=_(m,"backups"),de=_(m,"settings.json"),w=_(m,"claude-mem.db"),pe=_(O,"settings.json"),ce=_(O,"commands"),_e=_(O,"CLAUDE.md");function $(p){j(p,{recursive:!0})}function M(){return _(Y,"..","..")}var L=(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))(L||{}),A=class{level;useColor;constructor(){let e=process.env.CLAUDE_MEM_LOG_LEVEL?.toUpperCase()||"INFO";this.level=L[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(e0&&(l=` {${Object.entries(T).map(([g,b])=>`${g}=${b}`).join(", ")}}`)}let f=`[${d}] [${n}] [${c}] ${E}${t}${l}${a}`;e===3?console.error(f):console.log(f)}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`})}},X=new A;var N=class{db;constructor(){$(m),this.db=new q(w),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(` +import X from"path";import ae from"better-sqlite3";import{join as _,dirname as te,basename as Te}from"path";import{homedir as H}from"os";import{existsSync as Se,mkdirSync as re}from"fs";import{fileURLToPath as ne}from"url";function ie(){return typeof __dirname<"u"?__dirname:te(ne(import.meta.url))}var oe=ie(),L=process.env.CLAUDE_MEM_DATA_DIR||_(H(),".claude-mem"),C=process.env.CLAUDE_CONFIG_DIR||_(H(),".claude"),Re=_(L,"archives"),Ne=_(L,"logs"),Ie=_(L,"trash"),Oe=_(L,"backups"),Le=_(L,"settings.json"),B=_(L,"claude-mem.db"),Ae=_(C,"settings.json"),ve=_(C,"commands"),ye=_(C,"CLAUDE.md");function j(a){re(a,{recursive:!0})}function Y(){return _(oe,"..","..")}var x=(o=>(o[o.DEBUG=0]="DEBUG",o[o.INFO=1]="INFO",o[o.WARN=2]="WARN",o[o.ERROR=3]="ERROR",o[o.SILENT=4]="SILENT",o))(x||{}),$=class{level;useColor;constructor(){let e=process.env.CLAUDE_MEM_LOG_LEVEL?.toUpperCase()||"INFO";this.level=x[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,o){if(e0&&(n=` {${Object.entries(g).map(([O,f])=>`${O}=${f}`).join(", ")}}`)}let b=`[${d}] [${u}] [${p}] ${c}${t}${n}${R}`;e===3?console.error(b):console.log(b)}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`})}},q=new $;var D=class{db;constructor(){j(L),this.db=new ae(B),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 ( id INTEGER PRIMARY KEY, version INTEGER UNIQUE NOT NULL, @@ -63,7 +63,7 @@ ${e.stack}`:e.message;if(Array.isArray(e))return`[${e.length} items]`;let s=Obje CREATE INDEX IF NOT EXISTS idx_session_summaries_sdk_session ON session_summaries(sdk_session_id); CREATE INDEX IF NOT EXISTS idx_session_summaries_project ON session_summaries(project); CREATE INDEX IF NOT EXISTS idx_session_summaries_created ON session_summaries(created_at_epoch DESC); - `),this.db.prepare("INSERT INTO schema_versions (version, applied_at) VALUES (?, ?)").run(4,new Date().toISOString()),console.error("[SessionStore] Migration004 applied successfully"))}catch(e){throw console.error("[SessionStore] Schema initialization error:",e.message),e}}ensureWorkerPortColumn(){try{if(this.db.prepare("SELECT version FROM schema_versions WHERE version = ?").get(5))return;this.db.pragma("table_info(sdk_sessions)").some(r=>r.name==="worker_port")||(this.db.exec("ALTER TABLE sdk_sessions ADD COLUMN worker_port INTEGER"),console.error("[SessionStore] Added worker_port column to sdk_sessions table")),this.db.prepare("INSERT OR IGNORE INTO schema_versions (version, applied_at) VALUES (?, ?)").run(5,new Date().toISOString())}catch(e){console.error("[SessionStore] Migration error:",e.message)}}ensurePromptTrackingColumns(){try{if(this.db.prepare("SELECT version FROM schema_versions WHERE version = ?").get(6))return;this.db.pragma("table_info(sdk_sessions)").some(c=>c.name==="prompt_counter")||(this.db.exec("ALTER TABLE sdk_sessions ADD COLUMN prompt_counter INTEGER DEFAULT 0"),console.error("[SessionStore] Added prompt_counter column to sdk_sessions table")),this.db.pragma("table_info(observations)").some(c=>c.name==="prompt_number")||(this.db.exec("ALTER TABLE observations ADD COLUMN prompt_number INTEGER"),console.error("[SessionStore] Added prompt_number column to observations table")),this.db.pragma("table_info(session_summaries)").some(c=>c.name==="prompt_number")||(this.db.exec("ALTER TABLE session_summaries ADD COLUMN prompt_number INTEGER"),console.error("[SessionStore] Added prompt_number column to session_summaries table")),this.db.prepare("INSERT OR IGNORE INTO schema_versions (version, applied_at) VALUES (?, ?)").run(6,new Date().toISOString())}catch(e){console.error("[SessionStore] Prompt tracking migration error:",e.message)}}removeSessionSummariesUniqueConstraint(){try{if(this.db.prepare("SELECT version FROM schema_versions WHERE version = ?").get(7))return;if(!this.db.pragma("index_list(session_summaries)").some(r=>r.unique===1)){this.db.prepare("INSERT OR IGNORE INTO schema_versions (version, applied_at) VALUES (?, ?)").run(7,new Date().toISOString());return}console.error("[SessionStore] Removing UNIQUE constraint from session_summaries.sdk_session_id..."),this.db.exec("BEGIN TRANSACTION");try{this.db.exec(` + `),this.db.prepare("INSERT INTO schema_versions (version, applied_at) VALUES (?, ?)").run(4,new Date().toISOString()),console.error("[SessionStore] Migration004 applied successfully"))}catch(e){throw console.error("[SessionStore] Schema initialization error:",e.message),e}}ensureWorkerPortColumn(){try{if(this.db.prepare("SELECT version FROM schema_versions WHERE version = ?").get(5))return;this.db.pragma("table_info(sdk_sessions)").some(r=>r.name==="worker_port")||(this.db.exec("ALTER TABLE sdk_sessions ADD COLUMN worker_port INTEGER"),console.error("[SessionStore] Added worker_port column to sdk_sessions table")),this.db.prepare("INSERT OR IGNORE INTO schema_versions (version, applied_at) VALUES (?, ?)").run(5,new Date().toISOString())}catch(e){console.error("[SessionStore] Migration error:",e.message)}}ensurePromptTrackingColumns(){try{if(this.db.prepare("SELECT version FROM schema_versions WHERE version = ?").get(6))return;this.db.pragma("table_info(sdk_sessions)").some(p=>p.name==="prompt_counter")||(this.db.exec("ALTER TABLE sdk_sessions ADD COLUMN prompt_counter INTEGER DEFAULT 0"),console.error("[SessionStore] Added prompt_counter column to sdk_sessions table")),this.db.pragma("table_info(observations)").some(p=>p.name==="prompt_number")||(this.db.exec("ALTER TABLE observations ADD COLUMN prompt_number INTEGER"),console.error("[SessionStore] Added prompt_number column to observations table")),this.db.pragma("table_info(session_summaries)").some(p=>p.name==="prompt_number")||(this.db.exec("ALTER TABLE session_summaries ADD COLUMN prompt_number INTEGER"),console.error("[SessionStore] Added prompt_number column to session_summaries table")),this.db.prepare("INSERT OR IGNORE INTO schema_versions (version, applied_at) VALUES (?, ?)").run(6,new Date().toISOString())}catch(e){console.error("[SessionStore] Prompt tracking migration error:",e.message)}}removeSessionSummariesUniqueConstraint(){try{if(this.db.prepare("SELECT version FROM schema_versions WHERE version = ?").get(7))return;if(!this.db.pragma("index_list(session_summaries)").some(r=>r.unique===1)){this.db.prepare("INSERT OR IGNORE INTO schema_versions (version, applied_at) VALUES (?, ?)").run(7,new Date().toISOString());return}console.error("[SessionStore] Removing UNIQUE constraint from session_summaries.sdk_session_id..."),this.db.exec("BEGIN TRANSACTION");try{this.db.exec(` CREATE TABLE session_summaries_new ( id INTEGER PRIMARY KEY AUTOINCREMENT, sdk_session_id TEXT NOT NULL, @@ -222,7 +222,7 @@ ${e.stack}`:e.message;if(Array.isArray(e))return`[${e.length} items]`;let s=Obje SELECT files_read, files_modified FROM observations WHERE sdk_session_id = ? - `).all(e),r=new Set,i=new Set;for(let d of t){if(d.files_read)try{let n=JSON.parse(d.files_read);Array.isArray(n)&&n.forEach(c=>r.add(c))}catch{}if(d.files_modified)try{let n=JSON.parse(d.files_modified);Array.isArray(n)&&n.forEach(c=>i.add(c))}catch{}}return{filesRead:Array.from(r),filesModified:Array.from(i)}}getSessionById(e){return this.db.prepare(` + `).all(e),r=new Set,o=new Set;for(let d of t){if(d.files_read)try{let u=JSON.parse(d.files_read);Array.isArray(u)&&u.forEach(p=>r.add(p))}catch{}if(d.files_modified)try{let u=JSON.parse(d.files_modified);Array.isArray(u)&&u.forEach(p=>o.add(p))}catch{}}return{filesRead:Array.from(r),filesModified:Array.from(o)}}getSessionById(e){return this.db.prepare(` SELECT id, claude_session_id, sdk_session_id, project, user_prompt FROM sdk_sessions WHERE id = ? @@ -249,17 +249,17 @@ ${e.stack}`:e.message;if(Array.isArray(e))return`[${e.length} items]`;let s=Obje SELECT prompt_counter FROM sdk_sessions WHERE id = ? `).get(e)?.prompt_counter||1}getPromptCounter(e){return this.db.prepare(` SELECT prompt_counter FROM sdk_sessions WHERE id = ? - `).get(e)?.prompt_counter||0}createSDKSession(e,s,t){let r=new Date,i=r.getTime(),n=this.db.prepare(` + `).get(e)?.prompt_counter||0}createSDKSession(e,s,t){let r=new Date,o=r.getTime(),u=this.db.prepare(` INSERT OR IGNORE INTO sdk_sessions (claude_session_id, sdk_session_id, project, user_prompt, started_at, started_at_epoch, status) VALUES (?, ?, ?, ?, ?, ?, 'active') - `).run(e,e,s,t,r.toISOString(),i);return n.lastInsertRowid===0||n.changes===0?this.db.prepare(` + `).run(e,e,s,t,r.toISOString(),o);return u.lastInsertRowid===0||u.changes===0?this.db.prepare(` SELECT id FROM sdk_sessions WHERE claude_session_id = ? LIMIT 1 - `).get(e).id:n.lastInsertRowid}updateSDKSessionId(e,s){return this.db.prepare(` + `).get(e).id:u.lastInsertRowid}updateSDKSessionId(e,s){return this.db.prepare(` UPDATE sdk_sessions SET sdk_session_id = ? WHERE id = ? AND sdk_session_id IS NULL - `).run(s,e).changes===0?(X.debug("DB","sdk_session_id already set, skipping update",{sessionId:e,sdkSessionId:s}),!1):!0}setWorkerPort(e,s){this.db.prepare(` + `).run(s,e).changes===0?(q.debug("DB","sdk_session_id already set, skipping update",{sessionId:e,sdkSessionId:s}),!1):!0}setWorkerPort(e,s){this.db.prepare(` UPDATE sdk_sessions SET worker_port = ? WHERE id = ? @@ -268,33 +268,33 @@ ${e.stack}`:e.message;if(Array.isArray(e))return`[${e.length} items]`;let s=Obje FROM sdk_sessions WHERE id = ? LIMIT 1 - `).get(e)?.worker_port||null}saveUserPrompt(e,s,t){let r=new Date,i=r.getTime();return this.db.prepare(` + `).get(e)?.worker_port||null}saveUserPrompt(e,s,t){let r=new Date,o=r.getTime();return this.db.prepare(` INSERT INTO user_prompts (claude_session_id, prompt_number, prompt_text, created_at, created_at_epoch) VALUES (?, ?, ?, ?, ?) - `).run(e,s,t,r.toISOString(),i).lastInsertRowid}storeObservation(e,s,t,r){let i=new Date,d=i.getTime();this.db.prepare(` + `).run(e,s,t,r.toISOString(),o).lastInsertRowid}storeObservation(e,s,t,r){let o=new Date,d=o.getTime();this.db.prepare(` SELECT id FROM sdk_sessions WHERE sdk_session_id = ? `).get(e)||(this.db.prepare(` INSERT INTO sdk_sessions (claude_session_id, sdk_session_id, project, started_at, started_at_epoch, status) VALUES (?, ?, ?, ?, ?, 'active') - `).run(e,e,s,i.toISOString(),d),console.error(`[SessionStore] Auto-created session record for session_id: ${e}`)),this.db.prepare(` + `).run(e,e,s,o.toISOString(),d),console.error(`[SessionStore] Auto-created session record for session_id: ${e}`)),this.db.prepare(` INSERT INTO observations (sdk_session_id, project, type, title, subtitle, facts, narrative, concepts, files_read, files_modified, prompt_number, created_at, created_at_epoch) 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)}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,o.toISOString(),d)}storeSummary(e,s,t,r){let o=new Date,d=o.getTime();this.db.prepare(` SELECT id FROM sdk_sessions WHERE sdk_session_id = ? `).get(e)||(this.db.prepare(` INSERT INTO sdk_sessions (claude_session_id, sdk_session_id, project, started_at, started_at_epoch, status) VALUES (?, ?, ?, ?, ?, 'active') - `).run(e,e,s,i.toISOString(),d),console.error(`[SessionStore] Auto-created session record for session_id: ${e}`)),this.db.prepare(` + `).run(e,e,s,o.toISOString(),d),console.error(`[SessionStore] Auto-created session record for session_id: ${e}`)),this.db.prepare(` INSERT INTO session_summaries (sdk_session_id, project, request, investigated, learned, completed, next_steps, notes, prompt_number, created_at, created_at_epoch) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?) - `).run(e,s,t.request,t.investigated,t.learned,t.completed,t.next_steps,t.notes,r||null,i.toISOString(),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,o.toISOString(),d)}markSessionCompleted(e){let s=new Date,t=s.getTime();this.db.prepare(` UPDATE sdk_sessions SET status = 'completed', completed_at = ?, completed_at_epoch = ? WHERE id = ? @@ -306,25 +306,38 @@ ${e.stack}`:e.message;if(Array.isArray(e))return`[${e.length} items]`;let s=Obje UPDATE sdk_sessions SET status = 'failed', completed_at = ?, completed_at_epoch = ? WHERE status = 'active' - `).run(e.toISOString(),s).changes}close(){this.db.close()}};import v from"path";import{existsSync as y}from"fs";import{spawn as K}from"child_process";var V=parseInt(process.env.CLAUDE_MEM_WORKER_PORT||"37777",10),J=`http://127.0.0.1:${V}/health`;async function F(){try{return(await fetch(J,{signal:AbortSignal.timeout(500)})).ok}catch{return!1}}async function P(){try{if(await F())return!0;console.error("[claude-mem] Worker not responding, starting...");let p=M(),e=v.join(p,"plugin","scripts","worker-service.cjs");if(!y(e))return console.error(`[claude-mem] Worker service not found at ${e}`),!1;let s=v.join(p,"ecosystem.config.cjs"),t=v.join(p,"node_modules",".bin","pm2");if(!y(t))throw new Error(`PM2 binary not found at ${t}. This is a bundled dependency - try running: npm install`);if(!y(s))throw new Error(`PM2 ecosystem config not found at ${s}. Plugin installation may be corrupted.`);let r=K(t,["start",s],{detached:!0,stdio:"ignore",cwd:p});r.on("error",i=>{throw new Error(`Failed to spawn PM2: ${i.message}`)}),r.unref(),console.error("[claude-mem] Worker started with PM2");for(let i=0;i<3;i++)if(await new Promise(d=>setTimeout(d,500)),await F())return console.error("[claude-mem] Worker is healthy"),!0;return console.error("[claude-mem] Worker failed to become healthy after startup"),!1}catch(p){return console.error(`[claude-mem] Failed to start worker: ${p.message}`),!1}}var 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"};function D(p,e=!1,s=!1){P();let t=p?.cwd??process.cwd(),r=t?C.basename(t):"unknown-project",i=new N;try{let d=i.db.prepare(` - SELECT * FROM ( - SELECT sdk_session_id, request, learned, completed, next_steps, created_at, created_at_epoch - FROM session_summaries - WHERE project = ? - ORDER BY created_at_epoch DESC - LIMIT 10 - ) - ORDER BY created_at_epoch ASC - `).all(r);if(d.length===0)return e?` -${o.bright}${o.cyan}\u{1F4DD} [${r}] recent context${o.reset} -${o.gray}${"\u2500".repeat(60)}${o.reset} + `).run(e.toISOString(),s).changes}close(){this.db.close()}};import w from"path";import{existsSync as U}from"fs";import{spawn as de}from"child_process";var ce=parseInt(process.env.CLAUDE_MEM_WORKER_PORT||"37777",10),pe=`http://127.0.0.1:${ce}/health`;async function K(){try{return(await fetch(pe,{signal:AbortSignal.timeout(500)})).ok}catch{return!1}}async function V(){try{if(await K())return!0;console.error("[claude-mem] Worker not responding, starting...");let a=Y(),e=w.join(a,"plugin","scripts","worker-service.cjs");if(!U(e))return console.error(`[claude-mem] Worker service not found at ${e}`),!1;let s=w.join(a,"ecosystem.config.cjs"),t=w.join(a,"node_modules",".bin","pm2");if(!U(t))throw new Error(`PM2 binary not found at ${t}. This is a bundled dependency - try running: npm install`);if(!U(s))throw new Error(`PM2 ecosystem config not found at ${s}. Plugin installation may be corrupted.`);let r=de(t,["start",s],{detached:!0,stdio:"ignore",cwd:a});r.on("error",o=>{throw new Error(`Failed to spawn PM2: ${o.message}`)}),r.unref(),console.error("[claude-mem] Worker started with PM2");for(let o=0;o<3;o++)if(await new Promise(d=>setTimeout(d,500)),await K())return console.error("[claude-mem] Worker is healthy"),!0;return console.error("[claude-mem] Worker failed to become healthy after startup"),!1}catch(a){return console.error(`[claude-mem] Failed to start worker: ${a.message}`),!1}}var i={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 M(a){if(!a)return[];try{let e=JSON.parse(a);return Array.isArray(e)?e:[]}catch{return[]}}function J(a){return new Date(a).toLocaleString("en-US",{month:"short",day:"numeric",hour:"numeric",minute:"2-digit",hour12:!0})}function Q(a){return new Date(a).toLocaleString("en-US",{hour:"numeric",minute:"2-digit",hour12:!0})}function z(a){return new Date(a).toLocaleString("en-US",{month:"short",day:"numeric",year:"numeric"})}function ue(a){return a?Math.ceil(a.length/4):0}function me(a,e){try{return X.isAbsolute(a)?X.relative(e,a):a}catch{return a}}function le(a,e,s=3){return a.db.prepare(` + SELECT sdk_session_id + FROM sdk_sessions + WHERE project = ? AND sdk_session_id IS NOT NULL + ORDER BY started_at_epoch DESC + LIMIT ? + `).all(e,s).map(r=>r.sdk_session_id)}function _e(a,e){if(e.length===0)return[];let s=e.map(()=>"?").join(",");return a.db.prepare(` + SELECT + id, sdk_session_id, type, title, subtitle, narrative, + facts, concepts, files_read, files_modified, + created_at, created_at_epoch + FROM observations + WHERE sdk_session_id IN (${s}) + ORDER BY created_at_epoch DESC + `).all(...e)}function F(a,e=!1,s=!1){V();let t=a?.cwd??process.cwd(),r=t?X.basename(t):"unknown-project",o=new D;try{let d=le(o,r,3);if(d.length===0)return e?` +${i.bright}${i.cyan}\u{1F4DD} [${r}] recent context${i.reset} +${i.gray}${"\u2500".repeat(60)}${i.reset} -${o.dim}No previous summaries found for this project yet.${o.reset} +${i.dim}No previous sessions found for this project yet.${i.reset} `:`# [${r}] recent context -No previous summaries found for this project yet.`;let n=[];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(`# [${r}] recent context`),n.push(""));let c=!0;for(let E=0;E=1&&l<=3,k=l>3;if(c?e&&n.push(""):e?(n.push(`${o.gray}${"\u2500".repeat(60)}${o.reset}`),n.push("")):(n.push("---"),n.push("")),c=!1,k){a.request&&(e?(n.push(`${o.bright}${o.yellow}Request:${o.reset} ${a.request}`),n.push("")):(n.push(`**Request:** ${a.request}`),n.push("")));let T=new Date(a.created_at).toLocaleString();e?n.push(`${o.dim}Date: ${T}${o.reset}`):(n.push(`**Date:** ${T}`),n.push(""));continue}if(a.request&&(e?(n.push(`${o.bright}${o.yellow}Request:${o.reset} ${a.request}`),n.push("")):(n.push(`**Request:** ${a.request}`),n.push(""))),f&&a.learned&&(e?(n.push(`${o.bright}${o.blue}Learned:${o.reset} ${a.learned}`),n.push("")):(n.push(`**Learned:** ${a.learned}`),n.push(""))),a.completed&&(e?(n.push(`${o.bright}${o.green}Completed:${o.reset} ${a.completed}`),n.push("")):(n.push(`**Completed:** ${a.completed}`),n.push(""))),f&&a.next_steps&&(e?(n.push(`${o.bright}${o.magenta}Next Steps:${o.reset} ${a.next_steps}`),n.push("")):(n.push(`**Next Steps:** ${a.next_steps}`),n.push(""))),f){let T=i.db.prepare(` - SELECT files_read, files_modified - FROM observations - WHERE sdk_session_id = ? - `).all(a.sdk_session_id),h=new Set,g=new Set,b=u=>{try{return C.isAbsolute(u)?C.relative(t,u):u}catch{return u}};for(let u of T){if(u.files_read)try{let S=JSON.parse(u.files_read);Array.isArray(S)&&S.forEach(I=>h.add(b(I)))}catch{}if(u.files_modified)try{let S=JSON.parse(u.files_modified);Array.isArray(S)&&S.forEach(I=>g.add(b(I)))}catch{}}g.forEach(u=>h.delete(u)),h.size>0&&(e?n.push(`${o.dim}Files Read: ${Array.from(h).join(", ")}${o.reset}`):n.push(`**Files Read:** ${Array.from(h).join(", ")}`)),g.size>0&&(e?n.push(`${o.dim}Files Modified: ${Array.from(g).join(", ")}${o.reset}`):n.push(`**Files Modified:** ${Array.from(g).join(", ")}`))}let R=new Date(a.created_at).toLocaleString();e?n.push(`${o.dim}Date: ${R}${o.reset}`):n.push(`**Date:** ${R}`),e||n.push("")}return e&&(n.push(""),n.push(`${o.gray}${"\u2500".repeat(60)}${o.reset}`)),n.join(` -`)}finally{i.close()}}import{stdin as x}from"process";try{let p=process.argv.includes("--index");if(x.isTTY){let e=D(void 0,!0,p);console.log(e),process.exit(0)}else{let e="";x.on("data",s=>e+=s),x.on("end",()=>{let s=e.trim()?JSON.parse(e):void 0,r={hookSpecificOutput:{hookEventName:"SessionStart",additionalContext:D(s,!1,p)}};console.log(JSON.stringify(r)),process.exit(0)})}}catch(p){console.error(`[claude-mem context-hook error: ${p.message}]`),process.exit(0)} +No previous sessions found for this project yet.`;let p=_e(o,d).filter(b=>{let E=M(b.concepts);return E.includes("what-changed")||E.includes("how-it-works")||E.includes("problem-solution")||E.includes("gotcha")||E.includes("discovery")||E.includes("why-it-exists")||E.includes("decision")||E.includes("trade-off")}),c=o.db.prepare(` + SELECT request, completed, next_steps, created_at + FROM session_summaries + WHERE project = ? + ORDER BY created_at_epoch DESC + LIMIT 1 + `).get(r),R=o.db.prepare(` + SELECT id, request, created_at, created_at_epoch + FROM session_summaries + WHERE project = ? + ORDER BY created_at_epoch DESC + LIMIT 3 + `).all(r),n=[];if(e?(n.push(""),n.push(`${i.bright}${i.cyan}\u{1F4DD} [${r}] recent context${i.reset}`),n.push(`${i.gray}${"\u2500".repeat(60)}${i.reset}`),n.push("")):(n.push(`# [${r}] recent context`),n.push("")),p.length>0){e?(n.push(`${i.bright}${i.blue}\u{1F4CB} RECENT ACTIVITY TIMELINE${i.reset}`),n.push("")):(n.push("## Recent Activity Timeline"),n.push("")),e?(n.push(`${i.dim}Legend: \u{1F3AF} session-request | \u{1F534} gotcha | \u{1F7E1} problem-solution | \u{1F535} how-it-works | \u{1F7E2} what-changed | \u{1F7E3} discovery | \u{1F7E0} why-it-exists | \u{1F7E4} decision | \u2696\uFE0F trade-off${i.reset}`),n.push("")):(n.push("**Legend:** \u{1F3AF} session-request | \u{1F534} gotcha | \u{1F7E1} problem-solution | \u{1F535} how-it-works | \u{1F7E2} what-changed | \u{1F7E3} discovery | \u{1F7E0} why-it-exists | \u{1F7E4} decision | \u2696\uFE0F trade-off"),n.push(""));let b=new Map;for(let h of p){let N=z(h.created_at),g=M(h.files_modified),I=g.length>0?me(g[0],t):"General";b.has(N)||b.set(N,new Map);let O=b.get(N);O.has(I)||O.set(I,[]),O.get(I).push(h)}let E=Array.from(b.entries()).sort((h,N)=>{let g=new Date(h[0]).getTime(),I=new Date(N[0]).getTime();return g-I});for(let[h,N]of E){e?(n.push(`${i.bright}${i.cyan}${h}${i.reset}`),n.push("")):(n.push(`### ${h}`),n.push(""));let g=R.filter(f=>z(f.created_at)===h);if(g.length>0){e?n.push(`${i.dim}Session Requests${i.reset}`):n.push("**Session Requests**"),e||(n.push("| ID | Time | Title | Link |"),n.push("|----|------|-------|------|"));let f=R[0]?.id;for(let S of g.slice().reverse()){let A=Q(S.created_at),v=S.request||"Session started",l=S.id===f?"":`claude-mem://session-summary/${S.id}`;if(e){let m=l?`${i.dim}[${l}]${i.reset}`:"";n.push(` ${i.dim}#S${S.id}${i.reset} ${i.dim}${A}${i.reset} \u{1F3AF} ${v} ${m}`)}else{let m=l?`[\u2192](${l})`:"-";n.push(`| #S${S.id} | ${A} | \u{1F3AF} ${v} | ${m} |`)}}n.push("")}let I=Array.from(N.entries()).sort((f,S)=>{let A=Math.min(...f[1].map(T=>T.created_at_epoch)),v=Math.min(...S[1].map(T=>T.created_at_epoch));return A-v}),O=0;for(let[f,S]of I){if(O>=10)break;e?n.push(`${i.dim}${f}${i.reset}`):n.push(`**${f}**`),e||(n.push("| ID | Time | T | Title | Tokens |"),n.push("|----|------|---|-------|--------|"));let A="",v=S.slice(0,5).reverse();for(let T of v){let l=M(T.concepts),m="\u2022";l.includes("gotcha")?m="\u{1F534}":l.includes("decision")?m="\u{1F7E4}":l.includes("trade-off")?m="\u2696\uFE0F":l.includes("problem-solution")?m="\u{1F7E1}":l.includes("discovery")?m="\u{1F7E3}":l.includes("why-it-exists")?m="\u{1F7E0}":l.includes("how-it-works")?m="\u{1F535}":l.includes("what-changed")&&(m="\u{1F7E2}");let y=Q(T.created_at),G=T.title||"Untitled",k=ue(T.narrative),W=y!==A,Z=W?y:"";if(A=y,e){let ee=W?`${i.dim}${y}${i.reset}`:" ".repeat(y.length),se=k>0?`${i.dim}(~${k}t)${i.reset}`:"";n.push(` ${i.dim}#${T.id}${i.reset} ${ee} ${m} ${G} ${se}`)}else n.push(`| #${T.id} | ${Z||"\u2033"} | ${m} | ${G} | ~${k} |`)}n.push(""),O++}}e?n.push(`${i.dim}Use claude-mem MCP search to access records with the given ID${i.reset}`):n.push("*Use claude-mem MCP search to access records with the given ID*"),n.push("")}return c&&(e?(n.push(`${i.bright}${i.cyan}\u{1F4CB} RECENT SESSION SUMMARY${i.reset} ${i.dim}(${J(c.created_at)})${i.reset}`),n.push("")):(n.push(`## Recent Session Summary *(${J(c.created_at)})*`),n.push("")),c.request&&(e?n.push(`${i.yellow}Request:${i.reset} ${c.request}`):n.push(`**Request**: ${c.request}`),n.push("")),c.completed&&(e?n.push(`${i.green}Completed:${i.reset} ${c.completed}`):n.push(`**Completed**: ${c.completed}`),n.push("")),c.next_steps&&(e?n.push(`${i.magenta}Next Steps:${i.reset} ${c.next_steps}`):n.push(`**Next Steps**: ${c.next_steps}`),n.push(""))),e&&(n.push(`${i.gray}${"\u2500".repeat(60)}${i.reset}`),n.push("")),n.join(` +`)}finally{o.close()}}import{stdin as P}from"process";try{let a=process.argv.includes("--index");if(P.isTTY){let e=F(void 0,!0,a);console.log(e),process.exit(0)}else{let e="";P.on("data",s=>e+=s),P.on("end",()=>{let s=e.trim()?JSON.parse(e):void 0,r={hookSpecificOutput:{hookEventName:"SessionStart",additionalContext:F(s,!1,a)}};console.log(JSON.stringify(r)),process.exit(0)})}}catch(a){console.error(`[claude-mem context-hook error: ${a.message}]`),process.exit(0)} diff --git a/src/hooks/context.ts b/src/hooks/context.ts index 29b61df0..d7a5865e 100644 --- a/src/hooks/context.ts +++ b/src/hooks/context.ts @@ -22,8 +22,135 @@ const colors = { blue: '\x1b[34m', magenta: '\x1b[35m', gray: '\x1b[90m', + red: '\x1b[31m', }; +interface Observation { + id: number; + sdk_session_id: string; + type: string; + title: string | null; + subtitle: string | null; + narrative: string | null; + facts: string | null; + concepts: string | null; + files_read: string | null; + files_modified: string | null; + created_at: string; + created_at_epoch: number; +} + + +/** + * Helper: Parse JSON array safely + */ +function parseJsonArray(json: string | null): string[] { + if (!json) return []; + try { + const parsed = JSON.parse(json); + return Array.isArray(parsed) ? parsed : []; + } catch { + return []; + } +} + +/** + * Helper: Format date with time + */ +function formatDateTime(dateStr: string): string { + const date = new Date(dateStr); + return date.toLocaleString('en-US', { + month: 'short', + day: 'numeric', + hour: 'numeric', + minute: '2-digit', + hour12: true + }); +} + +/** + * Helper: Format just time (no date) + */ +function formatTime(dateStr: string): string { + const date = new Date(dateStr); + return date.toLocaleString('en-US', { + hour: 'numeric', + minute: '2-digit', + hour12: true + }); +} + +/** + * Helper: Format just date + */ +function formatDate(dateStr: string): string { + const date = new Date(dateStr); + return date.toLocaleString('en-US', { + month: 'short', + day: 'numeric', + year: 'numeric' + }); +} + +/** + * Helper: Estimate token count for text + */ +function estimateTokens(text: string | null): number { + if (!text) return 0; + // Rough estimate: ~4 characters per token + return Math.ceil(text.length / 4); +} + +/** + * Helper: Convert absolute paths to relative paths + */ +function toRelativePath(filePath: string, cwd: string): string { + try { + if (path.isAbsolute(filePath)) { + return path.relative(cwd, filePath); + } + return filePath; + } catch { + return filePath; + } +} + +/** + * Helper: Get recent session IDs for a project + */ +function getRecentSessionIds(db: SessionStore, project: string, limit: number = 3): string[] { + const sessions = db.db.prepare(` + SELECT sdk_session_id + FROM sdk_sessions + WHERE project = ? AND sdk_session_id IS NOT NULL + ORDER BY started_at_epoch DESC + LIMIT ? + `).all(project, limit) as Array<{ sdk_session_id: string }>; + + return sessions.map(s => s.sdk_session_id); +} + +/** + * Helper: Get all observations for given sessions + */ +function getObservations(db: SessionStore, sessionIds: string[]): Observation[] { + if (sessionIds.length === 0) return []; + + const placeholders = sessionIds.map(() => '?').join(','); + const observations = db.db.prepare(` + SELECT + id, sdk_session_id, type, title, subtitle, narrative, + facts, concepts, files_read, files_modified, + created_at, created_at_epoch + FROM observations + WHERE sdk_session_id IN (${placeholders}) + ORDER BY created_at_epoch DESC + `).all(...sessionIds) as Observation[]; + + return observations; +} + + /** * Context Hook - SessionStart * Shows user what happened in recent sessions @@ -36,224 +163,278 @@ export function contextHook(input?: SessionStartInput, useColors: boolean = fals const db = new SessionStore(); try { - // Get the most recent summaries, then display them chronologically (oldest to newest, like a chat) - const summaries = db.db.prepare(` - SELECT * FROM ( - SELECT sdk_session_id, request, learned, completed, next_steps, created_at, created_at_epoch - FROM session_summaries - WHERE project = ? - ORDER BY created_at_epoch DESC - LIMIT 10 - ) - ORDER BY created_at_epoch ASC - `).all(project) as Array<{ - sdk_session_id: string; - request: string | null; - learned: string | null; - completed: string | null; - next_steps: string | null; - created_at: string; - }>; + // Get recent session IDs + const sessionIds = getRecentSessionIds(db, project, 3); - if (summaries.length === 0) { + if (sessionIds.length === 0) { if (useColors) { - return `\n${colors.bright}${colors.cyan}📝 [${project}] recent context${colors.reset}\n${colors.gray}${'─'.repeat(60)}${colors.reset}\n\n${colors.dim}No previous summaries found for this project yet.${colors.reset}\n`; + return `\n${colors.bright}${colors.cyan}📝 [${project}] recent context${colors.reset}\n${colors.gray}${'─'.repeat(60)}${colors.reset}\n\n${colors.dim}No previous sessions found for this project yet.${colors.reset}\n`; } - return `# [${project}] recent context\n\nNo previous summaries found for this project yet.`; + return `# [${project}] recent context\n\nNo previous sessions found for this project yet.`; } + // Get all observations from recent sessions + const observations = getObservations(db, sessionIds); + + // Filter observations by key concepts for timeline + const timelineObs = observations.filter(obs => { + const concepts = parseJsonArray(obs.concepts); + return concepts.includes('what-changed') || + concepts.includes('how-it-works') || + concepts.includes('problem-solution') || + concepts.includes('gotcha') || + concepts.includes('discovery') || + concepts.includes('why-it-exists') || + concepts.includes('decision') || + concepts.includes('trade-off'); + }); + + // Get most recent summary + const recentSummary = db.db.prepare(` + SELECT request, completed, next_steps, created_at + FROM session_summaries + WHERE project = ? + ORDER BY created_at_epoch DESC + LIMIT 1 + `).get(project) as { request: string | null; completed: string | null; next_steps: string | null; created_at: string } | undefined; + + // Get last 3 summaries with IDs for timeline integration + const recentSummaries = db.db.prepare(` + SELECT id, request, created_at, created_at_epoch + FROM session_summaries + WHERE project = ? + ORDER BY created_at_epoch DESC + LIMIT 3 + `).all(project) as Array<{ id: number; request: string | null; created_at: string; created_at_epoch: number }>; + + // Build output const output: string[] = []; + // Header if (useColors) { output.push(''); output.push(`${colors.bright}${colors.cyan}📝 [${project}] recent context${colors.reset}`); output.push(`${colors.gray}${'─'.repeat(60)}${colors.reset}`); + output.push(''); } else { output.push(`# [${project}] recent context`); output.push(''); } - let isFirstSummary = true; - - for (let i = 0; i < summaries.length; i++) { - const summary = summaries[i]; - - // Determine verbosity tier based on position - // Most recent summary is at the end (highest index) since we display chronologically - const positionFromEnd = summaries.length - 1 - i; - const isTier1 = positionFromEnd === 0; // Most recent (full verbosity) - const isTier2 = positionFromEnd >= 1 && positionFromEnd <= 3; // Middle 3 (request + what was done) - const isTier3 = positionFromEnd > 3; // Oldest 6 (request only) - - // Add separator between summaries (but not before the first one) - if (!isFirstSummary) { - if (useColors) { - output.push(`${colors.gray}${'─'.repeat(60)}${colors.reset}`); - output.push(''); - } else { - output.push('---'); - output.push(''); - } - } else { - if (useColors) { - output.push(''); - } - } - - isFirstSummary = false; - - // TIER 3: Minimal (just Request + Date) - if (isTier3) { - if (summary.request) { - if (useColors) { - output.push(`${colors.bright}${colors.yellow}Request:${colors.reset} ${summary.request}`); - output.push(''); - } else { - output.push(`**Request:** ${summary.request}`); - output.push(''); - } - } - const dateTime = new Date(summary.created_at).toLocaleString(); - if (useColors) { - output.push(`${colors.dim}Date: ${dateTime}${colors.reset}`); - } else { - output.push(`**Date:** ${dateTime}`); - output.push(''); - } - continue; // Skip the rest for Tier 3 - } - - // TIER 1 & 2: Show Request - if (summary.request) { - if (useColors) { - output.push(`${colors.bright}${colors.yellow}Request:${colors.reset} ${summary.request}`); - output.push(''); - } else { - output.push(`**Request:** ${summary.request}`); - output.push(''); - } - } - - // TIER 1 ONLY: Show Learned - if (isTier1 && summary.learned) { - if (useColors) { - output.push(`${colors.bright}${colors.blue}Learned:${colors.reset} ${summary.learned}`); - output.push(''); - } else { - output.push(`**Learned:** ${summary.learned}`); - output.push(''); - } - } - - // TIER 1 & 2: Show Completed - if (summary.completed) { - if (useColors) { - output.push(`${colors.bright}${colors.green}Completed:${colors.reset} ${summary.completed}`); - output.push(''); - } else { - output.push(`**Completed:** ${summary.completed}`); - output.push(''); - } - } - - // TIER 1 ONLY: Show Next Steps - if (isTier1 && summary.next_steps) { - if (useColors) { - output.push(`${colors.bright}${colors.magenta}Next Steps:${colors.reset} ${summary.next_steps}`); - output.push(''); - } else { - output.push(`**Next Steps:** ${summary.next_steps}`); - output.push(''); - } - } - - // TIER 1 ONLY: Get and show files - if (isTier1) { - const observations = db.db.prepare(` - SELECT files_read, files_modified - FROM observations - WHERE sdk_session_id = ? - `).all(summary.sdk_session_id) as Array<{ - files_read: string | null; - files_modified: string | null; - }>; - - const filesReadSet = new Set(); - const filesModifiedSet = new Set(); - - // Helper function to convert absolute paths to relative paths - const toRelativePath = (filePath: string): string => { - try { - // Only convert if it's an absolute path - if (path.isAbsolute(filePath)) { - return path.relative(cwd, filePath); - } - return filePath; - } catch { - return filePath; - } - }; - - for (const obs of observations) { - if (obs.files_read) { - try { - const files = JSON.parse(obs.files_read); - if (Array.isArray(files)) { - files.forEach(f => filesReadSet.add(toRelativePath(f))); - } - } catch { - // Skip invalid JSON - } - } - - if (obs.files_modified) { - try { - const files = JSON.parse(obs.files_modified); - if (Array.isArray(files)) { - files.forEach(f => filesModifiedSet.add(toRelativePath(f))); - } - } catch { - // Skip invalid JSON - } - } - } - - // Remove files from filesReadSet if they're already in filesModifiedSet (avoid redundancy) - filesModifiedSet.forEach(file => filesReadSet.delete(file)); - - if (filesReadSet.size > 0) { - if (useColors) { - output.push(`${colors.dim}Files Read: ${Array.from(filesReadSet).join(', ')}${colors.reset}`); - } else { - output.push(`**Files Read:** ${Array.from(filesReadSet).join(', ')}`); - } - } - - if (filesModifiedSet.size > 0) { - if (useColors) { - output.push(`${colors.dim}Files Modified: ${Array.from(filesModifiedSet).join(', ')}${colors.reset}`); - } else { - output.push(`**Files Modified:** ${Array.from(filesModifiedSet).join(', ')}`); - } - } - } - - // TIER 1 & 2: Show Date - const dateTime = new Date(summary.created_at).toLocaleString(); + // SECTION 1: Chronological Timeline (grouped by file) + if (timelineObs.length > 0) { if (useColors) { - output.push(`${colors.dim}Date: ${dateTime}${colors.reset}`); + output.push(`${colors.bright}${colors.blue}📋 RECENT ACTIVITY TIMELINE${colors.reset}`); + output.push(''); } else { - output.push(`**Date:** ${dateTime}`); + output.push(`## Recent Activity Timeline`); + output.push(''); } - if (!useColors) { + // Legend/Key + if (useColors) { + output.push(`${colors.dim}Legend: 🎯 session-request | 🔴 gotcha | 🟡 problem-solution | 🔵 how-it-works | 🟢 what-changed | 🟣 discovery | 🟠 why-it-exists | 🟤 decision | ⚖️ trade-off${colors.reset}`); + output.push(''); + } else { + output.push(`**Legend:** 🎯 session-request | 🔴 gotcha | 🟡 problem-solution | 🔵 how-it-works | 🟢 what-changed | 🟣 discovery | 🟠 why-it-exists | 🟤 decision | ⚖️ trade-off`); + output.push(''); + } + + // Group observations by day, then by file + const dayGroups = new Map>(); + for (const obs of timelineObs) { + const day = formatDate(obs.created_at); + const files = parseJsonArray(obs.files_modified); + const file = files.length > 0 ? toRelativePath(files[0], cwd) : 'General'; + + if (!dayGroups.has(day)) { + dayGroups.set(day, new Map()); + } + + const fileGroups = dayGroups.get(day)!; + if (!fileGroups.has(file)) { + fileGroups.set(file, []); + } + fileGroups.get(file)!.push(obs); + } + + // Sort days chronologically + const sortedDays = Array.from(dayGroups.entries()).sort((a, b) => { + const aDate = new Date(a[0]).getTime(); + const bDate = new Date(b[0]).getTime(); + return aDate - bDate; + }); + + // Display each day's timeline + for (const [day, fileGroups] of sortedDays) { + // Day header + if (useColors) { + output.push(`${colors.bright}${colors.cyan}${day}${colors.reset}`); + output.push(''); + } else { + output.push(`### ${day}`); + output.push(''); + } + + // Check if any summaries belong to this day + const daySummaries = recentSummaries.filter(s => formatDate(s.created_at) === day); + if (daySummaries.length > 0) { + // Show session requests for this day + if (useColors) { + output.push(`${colors.dim}Session Requests${colors.reset}`); + } else { + output.push(`**Session Requests**`); + } + + if (!useColors) { + output.push(`| ID | Time | Title | Link |`); + output.push(`|----|------|-------|------|`); + } + + // Reverse to show oldest first (chronological) + const mostRecentId = recentSummaries[0]?.id; + for (const summary of daySummaries.slice().reverse()) { + const time = formatTime(summary.created_at); + const title = summary.request || 'Session started'; + const isMostRecent = summary.id === mostRecentId; + const link = isMostRecent ? '' : `claude-mem://session-summary/${summary.id}`; + + if (useColors) { + const linkPart = link ? `${colors.dim}[${link}]${colors.reset}` : ''; + output.push(` ${colors.dim}#S${summary.id}${colors.reset} ${colors.dim}${time}${colors.reset} 🎯 ${title} ${linkPart}`); + } else { + const linkCol = link ? `[→](${link})` : '-'; + output.push(`| #S${summary.id} | ${time} | 🎯 ${title} | ${linkCol} |`); + } + } + + output.push(''); + } + + // Sort files within day + const sortedFiles = Array.from(fileGroups.entries()).sort((a, b) => { + const aOldest = Math.min(...a[1].map(obs => obs.created_at_epoch)); + const bOldest = Math.min(...b[1].map(obs => obs.created_at_epoch)); + return aOldest - bOldest; + }); + + // Display each file within this day + let filesShown = 0; + for (const [file, obsGroup] of sortedFiles) { + if (filesShown >= 10) break; + + // File header + if (useColors) { + output.push(`${colors.dim}${file}${colors.reset}`); + } else { + output.push(`**${file}**`); + } + + // Table header + if (!useColors) { + output.push(`| ID | Time | T | Title | Tokens |`); + output.push(`|----|------|---|-------|--------|`); + } + + // Table rows + let lastTime = ''; + const sortedObs = obsGroup.slice(0, 5).reverse(); + for (const obs of sortedObs) { + const concepts = parseJsonArray(obs.concepts); + let icon = '•'; + + // Priority order: gotcha > decision > trade-off > problem-solution > discovery > why-it-exists > how-it-works > what-changed + if (concepts.includes('gotcha')) { + icon = '🔴'; + } else if (concepts.includes('decision')) { + icon = '🟤'; + } else if (concepts.includes('trade-off')) { + icon = '⚖️'; + } else if (concepts.includes('problem-solution')) { + icon = '🟡'; + } else if (concepts.includes('discovery')) { + icon = '🟣'; + } else if (concepts.includes('why-it-exists')) { + icon = '🟠'; + } else if (concepts.includes('how-it-works')) { + icon = '🔵'; + } else if (concepts.includes('what-changed')) { + icon = '🟢'; + } + + const time = formatTime(obs.created_at); + const title = obs.title || 'Untitled'; + const tokens = estimateTokens(obs.narrative); + + const showTime = time !== lastTime; + const timeDisplay = showTime ? time : ''; + lastTime = time; + + if (useColors) { + const timePart = showTime ? `${colors.dim}${time}${colors.reset}` : ' '.repeat(time.length); + const tokensPart = tokens > 0 ? `${colors.dim}(~${tokens}t)${colors.reset}` : ''; + output.push(` ${colors.dim}#${obs.id}${colors.reset} ${timePart} ${icon} ${title} ${tokensPart}`); + } else { + output.push(`| #${obs.id} | ${timeDisplay || '″'} | ${icon} | ${title} | ~${tokens} |`); + } + } + + output.push(''); + filesShown++; + } + } + + // Footer with MCP search instructions + if (useColors) { + output.push(`${colors.dim}Use claude-mem MCP search to access records with the given ID${colors.reset}`); + } else { + output.push(`*Use claude-mem MCP search to access records with the given ID*`); + } + output.push(''); + } + + // SECTION 2: Recent Summary + if (recentSummary) { + if (useColors) { + output.push(`${colors.bright}${colors.cyan}📋 RECENT SESSION SUMMARY${colors.reset} ${colors.dim}(${formatDateTime(recentSummary.created_at)})${colors.reset}`); + output.push(''); + } else { + output.push(`## Recent Session Summary *(${formatDateTime(recentSummary.created_at)})*`); + output.push(''); + } + + if (recentSummary.request) { + if (useColors) { + output.push(`${colors.yellow}Request:${colors.reset} ${recentSummary.request}`); + } else { + output.push(`**Request**: ${recentSummary.request}`); + } + output.push(''); + } + + if (recentSummary.completed) { + if (useColors) { + output.push(`${colors.green}Completed:${colors.reset} ${recentSummary.completed}`); + } else { + output.push(`**Completed**: ${recentSummary.completed}`); + } + output.push(''); + } + + if (recentSummary.next_steps) { + if (useColors) { + output.push(`${colors.magenta}Next Steps:${colors.reset} ${recentSummary.next_steps}`); + } else { + output.push(`**Next Steps**: ${recentSummary.next_steps}`); + } output.push(''); } } + // Footer if (useColors) { - output.push(''); output.push(`${colors.gray}${'─'.repeat(60)}${colors.reset}`); + output.push(''); } return output.join('\n');