From cedb63517603d0d9e34569166b3c64b905a29d54 Mon Sep 17 00:00:00 2001 From: Alex Newman Date: Thu, 16 Oct 2025 20:50:04 -0400 Subject: [PATCH] Refactor hooks to standardize response format and improve error handling - Introduced a new `hook-response.ts` module to create standardized hook responses. - Updated `context-hook.ts`, `new.ts`, `save.ts`, and `summary.ts` to utilize the new response format. - Enhanced error handling in `context-hook.ts` to check for input from stdin. - Refactored database interaction in hooks to ensure consistent session management. - Improved readability and maintainability of hook implementations by restructuring code. - Updated database queries to use consistent variable naming and formatting. - Modified the handling of socket connections in `save.ts` and `summary.ts` to ensure proper response on close and error events. --- claude-mem/.claude-plugin/plugin.json | 2 +- claude-mem/.mcp.json | 2 +- package.json | 2 +- scripts/hooks/context-hook.js | 22 +++---- scripts/hooks/new-hook.js | 19 +++--- scripts/hooks/save-hook.js | 21 +++---- scripts/hooks/summary-hook.js | 21 +++---- src/bin/hooks/context-hook.ts | 12 ++-- src/hooks/context.ts | 31 +++++---- src/hooks/hook-response.ts | 87 ++++++++++++++++++++++++++ src/hooks/new.ts | 9 +-- src/hooks/save.ts | 19 ++++-- src/hooks/summary.ts | 17 +++-- ~/.claude-mem/chroma/chroma.sqlite3 | Bin 163840 -> 0 bytes 14 files changed, 187 insertions(+), 77 deletions(-) create mode 100644 src/hooks/hook-response.ts delete mode 100644 ~/.claude-mem/chroma/chroma.sqlite3 diff --git a/claude-mem/.claude-plugin/plugin.json b/claude-mem/.claude-plugin/plugin.json index 8f763679..ed7ff5da 100644 --- a/claude-mem/.claude-plugin/plugin.json +++ b/claude-mem/.claude-plugin/plugin.json @@ -1,6 +1,6 @@ { "name": "claude-mem", - "version": "3.9.16", + "version": "3.9.17", "description": "Persistent memory system for Claude Code - seamlessly preserve context across sessions", "author": { "name": "Alex Newman" diff --git a/claude-mem/.mcp.json b/claude-mem/.mcp.json index e0c7e57f..422931fc 100644 --- a/claude-mem/.mcp.json +++ b/claude-mem/.mcp.json @@ -2,7 +2,7 @@ "mcpServers": { "claude-mem": { "command": "uvx", - "args": ["chroma-mcp", "--client-type", "persistent", "--data-dir", "~/.claude-mem/chroma"] + "args": ["chroma-mcp", "--client-type", "persistent", "--data-dir", "${CLAUDE_PLUGIN_ROOT}/claude-mem-chroma"] } } } \ No newline at end of file diff --git a/package.json b/package.json index 7336b96a..319abe3c 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "claude-mem", - "version": "3.9.16", + "version": "3.9.17", "description": "Memory compression system for Claude Code - persist context across sessions", "keywords": [ "claude", diff --git a/scripts/hooks/context-hook.js b/scripts/hooks/context-hook.js index 65744b0b..2d262d94 100755 --- a/scripts/hooks/context-hook.js +++ b/scripts/hooks/context-hook.js @@ -1,6 +1,6 @@ #!/usr/bin/env bun // @bun -import{Database as x}from"bun:sqlite";import{join as K,dirname as E,basename as T}from"path";import{homedir as G}from"os";import{existsSync as w,mkdirSync as O}from"fs";var B=process.env.CLAUDE_MEM_DATA_DIR||K(G(),".claude-mem"),M=process.env.CLAUDE_CONFIG_DIR||K(G(),".claude"),k=K(B,"archives"),S=K(B,"logs"),l=K(B,"trash"),h=K(B,"backups"),R=K(B,"chroma"),A=K(B,"settings.json"),L=K(B,"claude-mem.db"),j=K(M,"settings.json"),_=K(M,"commands"),I=K(M,"CLAUDE.md");function N(z){O(z,{recursive:!0})}class q{db;constructor(){N(B),this.db=new x(L,{create:!0,readwrite:!0}),this.db.run("PRAGMA journal_mode = WAL"),this.db.run("PRAGMA synchronous = NORMAL"),this.db.run("PRAGMA foreign_keys = ON")}getRecentSummaries(z,X=10){return this.db.query(` +import E from"path";import{Database as x}from"bun:sqlite";import{join as K,dirname as H,basename as P}from"path";import{homedir as L}from"os";import{existsSync as T,mkdirSync as U}from"fs";var B=process.env.CLAUDE_MEM_DATA_DIR||K(L(),".claude-mem"),J=process.env.CLAUDE_CONFIG_DIR||K(L(),".claude"),l=K(B,"archives"),A=K(B,"logs"),R=K(B,"trash"),j=K(B,"backups"),_=K(B,"chroma"),k=K(B,"settings.json"),N=K(B,"claude-mem.db"),I=K(J,"settings.json"),h=K(J,"commands"),y=K(J,"CLAUDE.md");function O(Q){U(Q,{recursive:!0})}class M{db;constructor(){O(B),this.db=new x(N,{create:!0,readwrite:!0}),this.db.run("PRAGMA journal_mode = WAL"),this.db.run("PRAGMA synchronous = NORMAL"),this.db.run("PRAGMA foreign_keys = ON")}getRecentSummaries(Q,Y=10){return this.db.query(` SELECT request, investigated, learned, completed, next_steps, files_read, files_edited, notes, created_at @@ -8,37 +8,37 @@ import{Database as x}from"bun:sqlite";import{join as K,dirname as E,basename as WHERE project = ? ORDER BY created_at_epoch DESC LIMIT ? - `).all(z,X)}findActiveSDKSession(z){return this.db.query(` + `).all(Q,Y)}findActiveSDKSession(Q){return this.db.query(` SELECT id, sdk_session_id, project FROM sdk_sessions WHERE claude_session_id = ? AND status = 'active' LIMIT 1 - `).get(z)||null}createSDKSession(z,X,Y){let $=new Date,Q=$.getTime();return this.db.query(` + `).get(Q)||null}createSDKSession(Q,Y,X){let z=new Date,W=z.getTime();return this.db.query(` INSERT INTO sdk_sessions (claude_session_id, project, user_prompt, started_at, started_at_epoch, status) VALUES (?, ?, ?, ?, ?, 'active') - `).run(z,X,Y,$.toISOString(),Q),this.db.query("SELECT last_insert_rowid() as id").get().id}updateSDKSessionId(z,X){this.db.query(` + `).run(Q,Y,X,z.toISOString(),W),this.db.query("SELECT last_insert_rowid() as id").get().id}updateSDKSessionId(Q,Y){this.db.query(` UPDATE sdk_sessions SET sdk_session_id = ? WHERE id = ? - `).run(X,z)}storeObservation(z,X,Y,$){let Q=new Date,V=Q.getTime();this.db.query(` + `).run(Y,Q)}storeObservation(Q,Y,X,z){let W=new Date,Z=W.getTime();this.db.query(` INSERT INTO observations (sdk_session_id, project, text, type, created_at, created_at_epoch) VALUES (?, ?, ?, ?, ?, ?) - `).run(z,X,$,Y,Q.toISOString(),V)}storeSummary(z,X,Y){let $=new Date,Q=$.getTime();this.db.query(` + `).run(Q,Y,z,X,W.toISOString(),Z)}storeSummary(Q,Y,X){let z=new Date,W=z.getTime();this.db.query(` INSERT INTO session_summaries (sdk_session_id, project, request, investigated, learned, completed, next_steps, files_read, files_edited, notes, created_at, created_at_epoch) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?) - `).run(z,X,Y.request||null,Y.investigated||null,Y.learned||null,Y.completed||null,Y.next_steps||null,Y.files_read||null,Y.files_edited||null,Y.notes||null,$.toISOString(),Q)}markSessionCompleted(z){let X=new Date,Y=X.getTime();this.db.query(` + `).run(Q,Y,X.request||null,X.investigated||null,X.learned||null,X.completed||null,X.next_steps||null,X.files_read||null,X.files_edited||null,X.notes||null,z.toISOString(),W)}markSessionCompleted(Q){let Y=new Date,X=Y.getTime();this.db.query(` UPDATE sdk_sessions SET status = 'completed', completed_at = ?, completed_at_epoch = ? WHERE id = ? - `).run(X.toISOString(),Y,z)}markSessionFailed(z){let X=new Date,Y=X.getTime();this.db.query(` + `).run(Y.toISOString(),X,Q)}markSessionFailed(Q){let Y=new Date,X=Y.getTime();this.db.query(` UPDATE sdk_sessions SET status = 'failed', completed_at = ?, completed_at_epoch = ? WHERE id = ? - `).run(X.toISOString(),Y,z)}close(){this.db.close()}}import v from"path";function U(z){try{if(console.error("[claude-mem context] Hook fired with input:",JSON.stringify({session_id:z?.session_id,transcript_path:z?.transcript_path,hook_event_name:z?.hook_event_name,source:z?.source,has_input:!!z})),!z)console.error("[claude-mem context] No input provided - exiting (standalone mode)"),console.log("No input provided - this script is designed to run as a Claude Code SessionStart hook"),process.exit(0);let X=z.cwd?v.basename(z.cwd):v.basename(v.dirname(z.transcript_path));console.error("[claude-mem context] Extracted project name:",X,"from",z.cwd?"cwd":"transcript_path"),console.error("[claude-mem context] Querying database for recent summaries...");let Y=new q,$=Y.getRecentSummaries(X,5);if(Y.close(),console.error("[claude-mem context] Database query complete - found",$.length,"summaries"),$.length>0)console.error("[claude-mem context] Summary previews:"),$.forEach((Z,W)=>{let F=Z.request?.substring(0,100)||Z.completed?.substring(0,100)||"(no content)";console.error(` [${W+1}]`,F+(F.length>=100?"...":""))});if($.length===0)console.error("[claude-mem context] No summaries found - outputting empty context message"),console.log(`# Recent Session Context + `).run(Y.toISOString(),X,Q)}close(){this.db.close()}}function b(Q,Y,X){if(Q==="PreCompact"){if(Y)return{continue:!0,suppressOutput:!0};return{continue:!1,stopReason:X.reason||"Pre-compact operation failed",suppressOutput:!0}}if(Q==="SessionStart"){if(Y&&X.context)return{continue:!0,suppressOutput:!0,hookSpecificOutput:{hookEventName:"SessionStart",additionalContext:X.context}};return{continue:!0,suppressOutput:!0}}if(Q==="UserPromptSubmit"||Q==="PostToolUse")return{continue:!0,suppressOutput:!0};if(Q==="Stop")return{continue:!0,suppressOutput:!0};return{continue:Y,suppressOutput:!0,...X.reason&&!Y?{stopReason:X.reason}:{}}}function q(Q,Y,X={}){let z=b(Q,Y,X);return JSON.stringify(z)}function F(Q){let Y=Q?.cwd??process.cwd(),X=Y?E.basename(Y):"unknown-project",z=new M;try{let W=z.getRecentSummaries(X,5);if(W.length===0){let $=q("SessionStart",!0,{context:`# Recent Session Context -No previous sessions found for this project yet.`),process.exit(0);console.error("[claude-mem context] Building markdown context from summaries...");let Q=[];Q.push("# Recent Session Context"),Q.push("");let V=$.length===1?"session":"sessions";Q.push(`Showing last ${$.length} ${V} for **${X}**:`),Q.push("");for(let Z of $){if(Q.push("---"),Q.push(""),Z.request)Q.push(`**Request:** ${Z.request}`);if(Z.completed)Q.push(`**Completed:** ${Z.completed}`);if(Z.learned)Q.push(`**Learned:** ${Z.learned}`);if(Z.next_steps)Q.push(`**Next Steps:** ${Z.next_steps}`);if(Z.files_read)try{let W=JSON.parse(Z.files_read);if(Array.isArray(W)&&W.length>0)Q.push(`**Files Read:** ${W.join(", ")}`)}catch{if(Z.files_read.trim())Q.push(`**Files Read:** ${Z.files_read}`)}if(Z.files_edited)try{let W=JSON.parse(Z.files_edited);if(Array.isArray(W)&&W.length>0)Q.push(`**Files Edited:** ${W.join(", ")}`)}catch{if(Z.files_edited.trim())Q.push(`**Files Edited:** ${Z.files_edited}`)}Q.push(`**Date:** ${Z.created_at.split("T")[0]}`),Q.push("")}let J=Q.join(` -`);console.error("[claude-mem context] Markdown built successfully"),console.error("[claude-mem context] Output length:",J.length,"characters,",Q.length,"lines"),console.error("[claude-mem context] Output preview (first 200 chars):",J.substring(0,200)+"..."),console.error("[claude-mem context] Outputting context to stdout for Claude Code injection"),console.log(J),console.error("[claude-mem context] Context hook completed successfully"),process.exit(0)}catch(X){console.error("[claude-mem context] ERROR occurred during context hook execution"),console.error("[claude-mem context] Error message:",X.message),console.error("[claude-mem context] Error stack:",X.stack),console.error("[claude-mem context] Exiting gracefully to avoid blocking Claude Code"),process.exit(0)}}var H=await Bun.stdin.text();try{let z=H.trim()?JSON.parse(H):void 0;U(z)}catch(z){console.error(`[claude-mem context-hook error: ${z.message}]`),process.exit(0)} +No previous sessions found for this project yet.`});console.log($);return}let Z=[];Z.push("# Recent Session Context"),Z.push("");let v=W.length===1?"session":"sessions";Z.push(`Showing last ${W.length} ${v} for **${X}**:`),Z.push("");for(let $ of W){if(Z.push("---"),Z.push(""),$.request)Z.push(`**Request:** ${$.request}`);if($.completed)Z.push(`**Completed:** ${$.completed}`);if($.learned)Z.push(`**Learned:** ${$.learned}`);if($.next_steps)Z.push(`**Next Steps:** ${$.next_steps}`);if($.files_read)try{let V=JSON.parse($.files_read);if(Array.isArray(V)&&V.length>0)Z.push(`**Files Read:** ${V.join(", ")}`)}catch{if($.files_read.trim())Z.push(`**Files Read:** ${$.files_read}`)}if($.files_edited)try{let V=JSON.parse($.files_edited);if(Array.isArray(V)&&V.length>0)Z.push(`**Files Edited:** ${V.join(", ")}`)}catch{if($.files_edited.trim())Z.push(`**Files Edited:** ${$.files_edited}`)}Z.push(`**Date:** ${$.created_at.split("T")[0]}`),Z.push("")}let G=q("SessionStart",!0,{context:Z.join(` +`)});console.log(G)}finally{z.close()}}try{if(process.stdin.isTTY)F();else{let Q=await Bun.stdin.text(),Y=Q.trim()?JSON.parse(Q):void 0;F(Y)}}catch(Q){console.error(`[claude-mem context-hook error: ${Q.message}]`),process.exit(0)} diff --git a/scripts/hooks/new-hook.js b/scripts/hooks/new-hook.js index 7ac06ba3..c9f46461 100755 --- a/scripts/hooks/new-hook.js +++ b/scripts/hooks/new-hook.js @@ -1,6 +1,6 @@ #!/usr/bin/env bun // @bun -import{Database as f}from"bun:sqlite";import{join as X,dirname as g,basename as C}from"path";import{homedir as q}from"os";import{existsSync as S,mkdirSync as U}from"fs";var Z=process.env.CLAUDE_MEM_DATA_DIR||X(q(),".claude-mem"),B=process.env.CLAUDE_CONFIG_DIR||X(q(),".claude"),l=X(Z,"archives"),y=X(Z,"logs"),h=X(Z,"trash"),j=X(Z,"backups"),A=X(Z,"chroma"),R=X(Z,"settings.json"),F=X(Z,"claude-mem.db"),_=X(B,"settings.json"),I=X(B,"commands"),c=X(B,"CLAUDE.md");function G(z){U(z,{recursive:!0})}class V{db;constructor(){G(Z),this.db=new f(F,{create:!0,readwrite:!0}),this.db.run("PRAGMA journal_mode = WAL"),this.db.run("PRAGMA synchronous = NORMAL"),this.db.run("PRAGMA foreign_keys = ON")}getRecentSummaries(z,Q=10){return this.db.query(` +import{spawn as b}from"child_process";import L from"path";import{Database as E}from"bun:sqlite";import{join as Z,dirname as w,basename as S}from"path";import{homedir as F}from"os";import{existsSync as j,mkdirSync as f}from"fs";var $=process.env.CLAUDE_MEM_DATA_DIR||Z(F(),".claude-mem"),B=process.env.CLAUDE_CONFIG_DIR||Z(F(),".claude"),R=Z($,"archives"),y=Z($,"logs"),_=Z($,"trash"),k=Z($,"backups"),I=Z($,"chroma"),h=Z($,"settings.json"),G=Z($,"claude-mem.db"),d=Z(B,"settings.json"),D=Z(B,"commands"),m=Z(B,"CLAUDE.md");function x(Q){f(Q,{recursive:!0})}class V{db;constructor(){x($),this.db=new E(G,{create:!0,readwrite:!0}),this.db.run("PRAGMA journal_mode = WAL"),this.db.run("PRAGMA synchronous = NORMAL"),this.db.run("PRAGMA foreign_keys = ON")}getRecentSummaries(Q,X=10){return this.db.query(` SELECT request, investigated, learned, completed, next_steps, files_read, files_edited, notes, created_at @@ -8,35 +8,34 @@ import{Database as f}from"bun:sqlite";import{join as X,dirname as g,basename as WHERE project = ? ORDER BY created_at_epoch DESC LIMIT ? - `).all(z,Q)}findActiveSDKSession(z){return this.db.query(` + `).all(Q,X)}findActiveSDKSession(Q){return this.db.query(` SELECT id, sdk_session_id, project FROM sdk_sessions WHERE claude_session_id = ? AND status = 'active' LIMIT 1 - `).get(z)||null}createSDKSession(z,Q,W){let Y=new Date,$=Y.getTime();return this.db.query(` + `).get(Q)||null}createSDKSession(Q,X,W){let Y=new Date,z=Y.getTime();return this.db.query(` INSERT INTO sdk_sessions (claude_session_id, project, user_prompt, started_at, started_at_epoch, status) VALUES (?, ?, ?, ?, ?, 'active') - `).run(z,Q,W,Y.toISOString(),$),this.db.query("SELECT last_insert_rowid() as id").get().id}updateSDKSessionId(z,Q){this.db.query(` + `).run(Q,X,W,Y.toISOString(),z),this.db.query("SELECT last_insert_rowid() as id").get().id}updateSDKSessionId(Q,X){this.db.query(` UPDATE sdk_sessions SET sdk_session_id = ? WHERE id = ? - `).run(Q,z)}storeObservation(z,Q,W,Y){let $=new Date,K=$.getTime();this.db.query(` + `).run(X,Q)}storeObservation(Q,X,W,Y){let z=new Date,K=z.getTime();this.db.query(` INSERT INTO observations (sdk_session_id, project, text, type, created_at, created_at_epoch) VALUES (?, ?, ?, ?, ?, ?) - `).run(z,Q,Y,W,$.toISOString(),K)}storeSummary(z,Q,W){let Y=new Date,$=Y.getTime();this.db.query(` + `).run(Q,X,Y,W,z.toISOString(),K)}storeSummary(Q,X,W){let Y=new Date,z=Y.getTime();this.db.query(` INSERT INTO session_summaries (sdk_session_id, project, request, investigated, learned, completed, next_steps, files_read, files_edited, notes, created_at, created_at_epoch) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?) - `).run(z,Q,W.request||null,W.investigated||null,W.learned||null,W.completed||null,W.next_steps||null,W.files_read||null,W.files_edited||null,W.notes||null,Y.toISOString(),$)}markSessionCompleted(z){let Q=new Date,W=Q.getTime();this.db.query(` + `).run(Q,X,W.request||null,W.investigated||null,W.learned||null,W.completed||null,W.next_steps||null,W.files_read||null,W.files_edited||null,W.notes||null,Y.toISOString(),z)}markSessionCompleted(Q){let X=new Date,W=X.getTime();this.db.query(` UPDATE sdk_sessions SET status = 'completed', completed_at = ?, completed_at_epoch = ? WHERE id = ? - `).run(Q.toISOString(),W,z)}markSessionFailed(z){let Q=new Date,W=Q.getTime();this.db.query(` + `).run(X.toISOString(),W,Q)}markSessionFailed(Q){let X=new Date,W=X.getTime();this.db.query(` UPDATE sdk_sessions SET status = 'failed', completed_at = ?, completed_at_epoch = ? WHERE id = ? - `).run(Q.toISOString(),W,z)}close(){this.db.close()}}import x from"path";import{spawn as H}from"child_process";function L(z){try{if(!z)console.log("No input provided - this script is designed to run as a Claude Code UserPromptSubmit hook"),console.log(` -Expected input format:`),console.log(JSON.stringify({session_id:"string",cwd:"string",prompt:"string"},null,2)),process.exit(0);let{session_id:Q,cwd:W,prompt:Y}=z,$=x.basename(W),K=new V;if(K.findActiveSDKSession(Q))K.close(),console.log('{"continue": true, "suppressOutput": true}'),process.exit(0);let J=K.createSDKSession(Q,$,Y);K.close();let M=process.env.CLAUDE_PLUGIN_ROOT;if(!M)throw new Error("CLAUDE_PLUGIN_ROOT not set - claude-mem must be installed as a Claude Code plugin");let N=x.join(M,"scripts","hooks","worker.js");H("bun",[N,J.toString()],{detached:!0,stdio:"ignore"}).unref(),console.log('{"continue": true, "suppressOutput": true}'),process.exit(0)}catch(Q){console.error(`[claude-mem new error: ${Q.message}]`),console.log('{"continue": true, "suppressOutput": true}'),process.exit(0)}}var O=await Bun.stdin.text();try{let z=O.trim()?JSON.parse(O):void 0;L(z)}catch(z){console.error(`[claude-mem new-hook error: ${z.message}]`),console.log('{"continue": true, "suppressOutput": true}'),process.exit(0)} + `).run(X.toISOString(),W,Q)}close(){this.db.close()}}function H(Q,X,W){if(Q==="PreCompact"){if(X)return{continue:!0,suppressOutput:!0};return{continue:!1,stopReason:W.reason||"Pre-compact operation failed",suppressOutput:!0}}if(Q==="SessionStart"){if(X&&W.context)return{continue:!0,suppressOutput:!0,hookSpecificOutput:{hookEventName:"SessionStart",additionalContext:W.context}};return{continue:!0,suppressOutput:!0}}if(Q==="UserPromptSubmit"||Q==="PostToolUse")return{continue:!0,suppressOutput:!0};if(Q==="Stop")return{continue:!0,suppressOutput:!0};return{continue:X,suppressOutput:!0,...W.reason&&!X?{stopReason:W.reason}:{}}}function v(Q,X,W={}){let Y=H(Q,X,W);return JSON.stringify(Y)}function O(Q){if(!Q)throw new Error("newHook requires input");let{session_id:X,cwd:W,prompt:Y}=Q,z=L.basename(W),K=new V;try{if(K.findActiveSDKSession(X)){console.log(v("UserPromptSubmit",!0));return}let M=K.createSDKSession(X,z,Y),q=process.env.CLAUDE_PLUGIN_ROOT;if(!q)throw new Error("CLAUDE_PLUGIN_ROOT not set");let U=L.join(q,"scripts","hooks","worker.js");b("bun",[U,M.toString()],{detached:!0,stdio:"ignore"}).unref(),console.log(v("UserPromptSubmit",!0))}finally{K.close()}}var N=await Bun.stdin.text();try{let Q=N.trim()?JSON.parse(N):void 0;O(Q)}catch(Q){console.error(`[claude-mem new-hook error: ${Q.message}]`),console.log('{"continue": true, "suppressOutput": true}'),process.exit(0)} diff --git a/scripts/hooks/save-hook.js b/scripts/hooks/save-hook.js index 6e222add..12d18c2c 100755 --- a/scripts/hooks/save-hook.js +++ b/scripts/hooks/save-hook.js @@ -1,6 +1,6 @@ #!/usr/bin/env bun // @bun -import b from"net";import{Database as O}from"bun:sqlite";import{join as Y,dirname as w,basename as C}from"path";import{homedir as F}from"os";import{existsSync as S,mkdirSync as H}from"fs";var $=process.env.CLAUDE_MEM_DATA_DIR||Y(F(),".claude-mem"),J=process.env.CLAUDE_CONFIG_DIR||Y(F(),".claude"),h=Y($,"archives"),l=Y($,"logs"),R=Y($,"trash"),j=Y($,"backups"),A=Y($,"chroma"),I=Y($,"settings.json"),G=Y($,"claude-mem.db"),_=Y(J,"settings.json"),p=Y(J,"commands"),d=Y(J,"CLAUDE.md");function v(z){return Y($,`worker-${z}.sock`)}function x(z){H(z,{recursive:!0})}class M{db;constructor(){x($),this.db=new O(G,{create:!0,readwrite:!0}),this.db.run("PRAGMA journal_mode = WAL"),this.db.run("PRAGMA synchronous = NORMAL"),this.db.run("PRAGMA foreign_keys = ON")}getRecentSummaries(z,X=10){return this.db.query(` +import w from"net";import{Database as H}from"bun:sqlite";import{join as $,dirname as T,basename as l}from"path";import{homedir as x}from"os";import{existsSync as y,mkdirSync as E}from"fs";var z=process.env.CLAUDE_MEM_DATA_DIR||$(x(),".claude-mem"),M=process.env.CLAUDE_CONFIG_DIR||$(x(),".claude"),I=$(z,"archives"),k=$(z,"logs"),_=$(z,"trash"),h=$(z,"backups"),D=$(z,"chroma"),d=$(z,"settings.json"),N=$(z,"claude-mem.db"),m=$(M,"settings.json"),u=$(M,"commands"),c=$(M,"CLAUDE.md");function L(Q){return $(z,`worker-${Q}.sock`)}function U(Q){E(Q,{recursive:!0})}class q{db;constructor(){U(z),this.db=new H(N,{create:!0,readwrite:!0}),this.db.run("PRAGMA journal_mode = WAL"),this.db.run("PRAGMA synchronous = NORMAL"),this.db.run("PRAGMA foreign_keys = ON")}getRecentSummaries(Q,Y=10){return this.db.query(` SELECT request, investigated, learned, completed, next_steps, files_read, files_edited, notes, created_at @@ -8,36 +8,35 @@ import b from"net";import{Database as O}from"bun:sqlite";import{join as Y,dirnam WHERE project = ? ORDER BY created_at_epoch DESC LIMIT ? - `).all(z,X)}findActiveSDKSession(z){return this.db.query(` + `).all(Q,Y)}findActiveSDKSession(Q){return this.db.query(` SELECT id, sdk_session_id, project FROM sdk_sessions WHERE claude_session_id = ? AND status = 'active' LIMIT 1 - `).get(z)||null}createSDKSession(z,X,Q){let Z=new Date,W=Z.getTime();return this.db.query(` + `).get(Q)||null}createSDKSession(Q,Y,X){let Z=new Date,W=Z.getTime();return this.db.query(` INSERT INTO sdk_sessions (claude_session_id, project, user_prompt, started_at, started_at_epoch, status) VALUES (?, ?, ?, ?, ?, 'active') - `).run(z,X,Q,Z.toISOString(),W),this.db.query("SELECT last_insert_rowid() as id").get().id}updateSDKSessionId(z,X){this.db.query(` + `).run(Q,Y,X,Z.toISOString(),W),this.db.query("SELECT last_insert_rowid() as id").get().id}updateSDKSessionId(Q,Y){this.db.query(` UPDATE sdk_sessions SET sdk_session_id = ? WHERE id = ? - `).run(X,z)}storeObservation(z,X,Q,Z){let W=new Date,B=W.getTime();this.db.query(` + `).run(Y,Q)}storeObservation(Q,Y,X,Z){let W=new Date,B=W.getTime();this.db.query(` INSERT INTO observations (sdk_session_id, project, text, type, created_at, created_at_epoch) VALUES (?, ?, ?, ?, ?, ?) - `).run(z,X,Z,Q,W.toISOString(),B)}storeSummary(z,X,Q){let Z=new Date,W=Z.getTime();this.db.query(` + `).run(Q,Y,Z,X,W.toISOString(),B)}storeSummary(Q,Y,X){let Z=new Date,W=Z.getTime();this.db.query(` INSERT INTO session_summaries (sdk_session_id, project, request, investigated, learned, completed, next_steps, files_read, files_edited, notes, created_at, created_at_epoch) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?) - `).run(z,X,Q.request||null,Q.investigated||null,Q.learned||null,Q.completed||null,Q.next_steps||null,Q.files_read||null,Q.files_edited||null,Q.notes||null,Z.toISOString(),W)}markSessionCompleted(z){let X=new Date,Q=X.getTime();this.db.query(` + `).run(Q,Y,X.request||null,X.investigated||null,X.learned||null,X.completed||null,X.next_steps||null,X.files_read||null,X.files_edited||null,X.notes||null,Z.toISOString(),W)}markSessionCompleted(Q){let Y=new Date,X=Y.getTime();this.db.query(` UPDATE sdk_sessions SET status = 'completed', completed_at = ?, completed_at_epoch = ? WHERE id = ? - `).run(X.toISOString(),Q,z)}markSessionFailed(z){let X=new Date,Q=X.getTime();this.db.query(` + `).run(Y.toISOString(),X,Q)}markSessionFailed(Q){let Y=new Date,X=Y.getTime();this.db.query(` UPDATE sdk_sessions SET status = 'failed', completed_at = ?, completed_at_epoch = ? WHERE id = ? - `).run(X.toISOString(),Q,z)}close(){this.db.close()}}var E=new Set(["TodoWrite","ListMcpResourcesTool"]);function N(z){try{if(!z)console.log("No input provided - this script is designed to run as a Claude Code PostToolUse hook"),console.log(` -Expected input format:`),console.log(JSON.stringify({session_id:"string",cwd:"string",tool_name:"string",tool_input:{},tool_output:{}},null,2)),process.exit(0);let{session_id:X,tool_name:Q,tool_input:Z,tool_output:W}=z;if(E.has(Q))console.log('{"continue": true, "suppressOutput": true}'),process.exit(0);let B=new M,K=B.findActiveSDKSession(X);if(B.close(),!K)console.log('{"continue": true, "suppressOutput": true}'),process.exit(0);let q=v(K.id),U={type:"observation",tool_name:Q,tool_input:JSON.stringify(Z),tool_output:JSON.stringify(W)},V=b.connect(q,()=>{V.write(JSON.stringify(U)+` -`),V.end()});V.on("error",(f)=>{console.error(`[claude-mem save] Socket error: ${f.message}`)}),V.on("close",()=>{console.log('{"continue": true, "suppressOutput": true}'),process.exit(0)})}catch(X){console.error(`[claude-mem save error: ${X.message}]`),console.log('{"continue": true, "suppressOutput": true}'),process.exit(0)}}var L=await Bun.stdin.text();try{let z=L.trim()?JSON.parse(L):void 0;N(z)}catch(z){console.error(`[claude-mem save-hook error: ${z.message}]`),console.log('{"continue": true, "suppressOutput": true}'),process.exit(0)} + `).run(Y.toISOString(),X,Q)}close(){this.db.close()}}function g(Q,Y,X){if(Q==="PreCompact"){if(Y)return{continue:!0,suppressOutput:!0};return{continue:!1,stopReason:X.reason||"Pre-compact operation failed",suppressOutput:!0}}if(Q==="SessionStart"){if(Y&&X.context)return{continue:!0,suppressOutput:!0,hookSpecificOutput:{hookEventName:"SessionStart",additionalContext:X.context}};return{continue:!0,suppressOutput:!0}}if(Q==="UserPromptSubmit"||Q==="PostToolUse")return{continue:!0,suppressOutput:!0};if(Q==="Stop")return{continue:!0,suppressOutput:!0};return{continue:Y,suppressOutput:!0,...X.reason&&!Y?{stopReason:X.reason}:{}}}function J(Q,Y,X={}){let Z=g(Q,Y,X);return JSON.stringify(Z)}var C=new Set(["TodoWrite","ListMcpResourcesTool"]);function f(Q){if(!Q)throw new Error("saveHook requires input");let{session_id:Y,tool_name:X,tool_input:Z,tool_output:W}=Q;if(C.has(X)){console.log(J("PostToolUse",!0));return}let B=new q,K=B.findActiveSDKSession(Y);if(B.close(),!K){console.log(J("PostToolUse",!0));return}let F=L(K.id),b={type:"observation",tool_name:X,tool_input:JSON.stringify(Z),tool_output:JSON.stringify(W)},V=w.connect(F,()=>{V.write(JSON.stringify(b)+` +`),V.end()}),G=!1,v=()=>{if(G)return;G=!0,console.log(J("PostToolUse",!0))};V.on("close",v),V.on("error",v)}var O=await Bun.stdin.text();try{let Q=O.trim()?JSON.parse(O):void 0;f(Q)}catch(Q){console.error(`[claude-mem save-hook error: ${Q.message}]`),console.log('{"continue": true, "suppressOutput": true}'),process.exit(0)} diff --git a/scripts/hooks/summary-hook.js b/scripts/hooks/summary-hook.js index c44b4e7f..8194fef2 100755 --- a/scripts/hooks/summary-hook.js +++ b/scripts/hooks/summary-hook.js @@ -1,6 +1,6 @@ #!/usr/bin/env bun // @bun -import U from"net";import{Database as O}from"bun:sqlite";import{join as Z,dirname as b,basename as E}from"path";import{homedir as M}from"os";import{existsSync as C,mkdirSync as N}from"fs";var K=process.env.CLAUDE_MEM_DATA_DIR||Z(M(),".claude-mem"),v=process.env.CLAUDE_CONFIG_DIR||Z(M(),".claude"),P=Z(K,"archives"),l=Z(K,"logs"),S=Z(K,"trash"),k=Z(K,"backups"),y=Z(K,"chroma"),h=Z(K,"settings.json"),q=Z(K,"claude-mem.db"),R=Z(v,"settings.json"),j=Z(v,"commands"),A=Z(v,"CLAUDE.md");function F(z){return Z(K,`worker-${z}.sock`)}function G(z){N(z,{recursive:!0})}class J{db;constructor(){G(K),this.db=new O(q,{create:!0,readwrite:!0}),this.db.run("PRAGMA journal_mode = WAL"),this.db.run("PRAGMA synchronous = NORMAL"),this.db.run("PRAGMA foreign_keys = ON")}getRecentSummaries(z,Q=10){return this.db.query(` +import E from"net";import{Database as f}from"bun:sqlite";import{join as $,dirname as w,basename as P}from"path";import{homedir as F}from"os";import{existsSync as T,mkdirSync as U}from"fs";var z=process.env.CLAUDE_MEM_DATA_DIR||$(F(),".claude-mem"),v=process.env.CLAUDE_CONFIG_DIR||$(F(),".claude"),A=$(z,"archives"),j=$(z,"logs"),R=$(z,"trash"),_=$(z,"backups"),y=$(z,"chroma"),I=$(z,"settings.json"),G=$(z,"claude-mem.db"),k=$(v,"settings.json"),h=$(v,"commands"),D=$(v,"CLAUDE.md");function x(Q){return $(z,`worker-${Q}.sock`)}function L(Q){U(Q,{recursive:!0})}class J{db;constructor(){L(z),this.db=new f(G,{create:!0,readwrite:!0}),this.db.run("PRAGMA journal_mode = WAL"),this.db.run("PRAGMA synchronous = NORMAL"),this.db.run("PRAGMA foreign_keys = ON")}getRecentSummaries(Q,Y=10){return this.db.query(` SELECT request, investigated, learned, completed, next_steps, files_read, files_edited, notes, created_at @@ -8,36 +8,35 @@ import U from"net";import{Database as O}from"bun:sqlite";import{join as Z,dirnam WHERE project = ? ORDER BY created_at_epoch DESC LIMIT ? - `).all(z,Q)}findActiveSDKSession(z){return this.db.query(` + `).all(Q,Y)}findActiveSDKSession(Q){return this.db.query(` SELECT id, sdk_session_id, project FROM sdk_sessions WHERE claude_session_id = ? AND status = 'active' LIMIT 1 - `).get(z)||null}createSDKSession(z,Q,X){let Y=new Date,$=Y.getTime();return this.db.query(` + `).get(Q)||null}createSDKSession(Q,Y,X){let Z=new Date,K=Z.getTime();return this.db.query(` INSERT INTO sdk_sessions (claude_session_id, project, user_prompt, started_at, started_at_epoch, status) VALUES (?, ?, ?, ?, ?, 'active') - `).run(z,Q,X,Y.toISOString(),$),this.db.query("SELECT last_insert_rowid() as id").get().id}updateSDKSessionId(z,Q){this.db.query(` + `).run(Q,Y,X,Z.toISOString(),K),this.db.query("SELECT last_insert_rowid() as id").get().id}updateSDKSessionId(Q,Y){this.db.query(` UPDATE sdk_sessions SET sdk_session_id = ? WHERE id = ? - `).run(Q,z)}storeObservation(z,Q,X,Y){let $=new Date,W=$.getTime();this.db.query(` + `).run(Y,Q)}storeObservation(Q,Y,X,Z){let K=new Date,B=K.getTime();this.db.query(` INSERT INTO observations (sdk_session_id, project, text, type, created_at, created_at_epoch) VALUES (?, ?, ?, ?, ?, ?) - `).run(z,Q,Y,X,$.toISOString(),W)}storeSummary(z,Q,X){let Y=new Date,$=Y.getTime();this.db.query(` + `).run(Q,Y,Z,X,K.toISOString(),B)}storeSummary(Q,Y,X){let Z=new Date,K=Z.getTime();this.db.query(` INSERT INTO session_summaries (sdk_session_id, project, request, investigated, learned, completed, next_steps, files_read, files_edited, notes, created_at, created_at_epoch) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?) - `).run(z,Q,X.request||null,X.investigated||null,X.learned||null,X.completed||null,X.next_steps||null,X.files_read||null,X.files_edited||null,X.notes||null,Y.toISOString(),$)}markSessionCompleted(z){let Q=new Date,X=Q.getTime();this.db.query(` + `).run(Q,Y,X.request||null,X.investigated||null,X.learned||null,X.completed||null,X.next_steps||null,X.files_read||null,X.files_edited||null,X.notes||null,Z.toISOString(),K)}markSessionCompleted(Q){let Y=new Date,X=Y.getTime();this.db.query(` UPDATE sdk_sessions SET status = 'completed', completed_at = ?, completed_at_epoch = ? WHERE id = ? - `).run(Q.toISOString(),X,z)}markSessionFailed(z){let Q=new Date,X=Q.getTime();this.db.query(` + `).run(Y.toISOString(),X,Q)}markSessionFailed(Q){let Y=new Date,X=Y.getTime();this.db.query(` UPDATE sdk_sessions SET status = 'failed', completed_at = ?, completed_at_epoch = ? WHERE id = ? - `).run(Q.toISOString(),X,z)}close(){this.db.close()}}function x(z){try{if(console.error("[claude-mem summary] Hook fired",{input:z?{session_id:z.session_id,cwd:z.cwd}:null}),!z)console.log("No input provided - this script is designed to run as a Claude Code Stop hook"),console.log(` -Expected input format:`),console.log(JSON.stringify({session_id:"string",cwd:"string"},null,2)),process.exit(0);let{session_id:Q}=z;console.error("[claude-mem summary] Searching for active SDK session",{session_id:Q});let X=new J,Y=X.findActiveSDKSession(Q);if(X.close(),!Y)console.error("[claude-mem summary] No active SDK session found",{session_id:Q}),console.log('{"continue": true, "suppressOutput": true}'),process.exit(0);console.error("[claude-mem summary] Active SDK session found",{session_id:Y.id,collection_name:Y.collection_name,worker_pid:Y.worker_pid});let $=F(Y.id),W={type:"finalize"};console.error("[claude-mem summary] Attempting to send FINALIZE message to worker socket",{socketPath:$,message:W});let B=U.connect($,()=>{console.error("[claude-mem summary] Socket connection established, sending message"),B.write(JSON.stringify(W)+` -`),B.end()});B.on("error",(V)=>{console.error("[claude-mem summary] Socket error occurred",{error:V.message,code:V.code,socketPath:$})}),B.on("close",()=>{console.error("[claude-mem summary] Socket connection closed successfully"),console.log('{"continue": true, "suppressOutput": true}'),process.exit(0)})}catch(Q){console.error("[claude-mem summary] Unexpected error in hook",{error:Q.message,stack:Q.stack,name:Q.name}),console.log('{"continue": true, "suppressOutput": true}'),process.exit(0)}}var L=await Bun.stdin.text();try{let z=L.trim()?JSON.parse(L):void 0;x(z)}catch(z){console.error(`[claude-mem summary-hook error: ${z.message}]`),console.log('{"continue": true, "suppressOutput": true}'),process.exit(0)} + `).run(Y.toISOString(),X,Q)}close(){this.db.close()}}function b(Q,Y,X){if(Q==="PreCompact"){if(Y)return{continue:!0,suppressOutput:!0};return{continue:!1,stopReason:X.reason||"Pre-compact operation failed",suppressOutput:!0}}if(Q==="SessionStart"){if(Y&&X.context)return{continue:!0,suppressOutput:!0,hookSpecificOutput:{hookEventName:"SessionStart",additionalContext:X.context}};return{continue:!0,suppressOutput:!0}}if(Q==="UserPromptSubmit"||Q==="PostToolUse")return{continue:!0,suppressOutput:!0};if(Q==="Stop")return{continue:!0,suppressOutput:!0};return{continue:Y,suppressOutput:!0,...X.reason&&!Y?{stopReason:X.reason}:{}}}function M(Q,Y,X={}){let Z=b(Q,Y,X);return JSON.stringify(Z)}function N(Q){if(!Q)throw new Error("summaryHook requires input");let{session_id:Y}=Q,X=new J,Z=X.findActiveSDKSession(Y);if(X.close(),!Z){console.log(M("Stop",!0));return}let K=x(Z.id),B={type:"finalize"},W=E.connect(K,()=>{W.write(JSON.stringify(B)+` +`),W.end()}),V=!1,q=()=>{if(V)return;V=!0,console.log(M("Stop",!0))};W.on("close",q),W.on("error",q)}var O=await Bun.stdin.text();try{let Q=O.trim()?JSON.parse(O):void 0;N(Q)}catch(Q){console.error(`[claude-mem summary-hook error: ${Q.message}]`),console.log('{"continue": true, "suppressOutput": true}'),process.exit(0)} diff --git a/src/bin/hooks/context-hook.ts b/src/bin/hooks/context-hook.ts index a04d8217..97a85623 100644 --- a/src/bin/hooks/context-hook.ts +++ b/src/bin/hooks/context-hook.ts @@ -7,12 +7,14 @@ import { contextHook } from '../../hooks/context.js'; -// Read input from stdin -const input = await Bun.stdin.text(); - try { - const parsed = input.trim() ? JSON.parse(input) : undefined; - contextHook(parsed); + if (process.stdin.isTTY) { + contextHook(); + } else { + const input = await Bun.stdin.text(); + const parsed = input.trim() ? JSON.parse(input) : undefined; + contextHook(parsed); + } } catch (error: any) { console.error(`[claude-mem context-hook error: ${error.message}]`); process.exit(0); diff --git a/src/hooks/context.ts b/src/hooks/context.ts index 5561067e..3b39a379 100644 --- a/src/hooks/context.ts +++ b/src/hooks/context.ts @@ -1,12 +1,13 @@ -import { HooksDatabase } from '../services/sqlite/HooksDatabase.js'; import path from 'path'; +import { HooksDatabase } from '../services/sqlite/HooksDatabase.js'; +import { createHookResponse } from './hook-response.js'; export interface SessionStartInput { - session_id: string; - transcript_path: string; - cwd: string; - hook_event_name: string; - source: "startup" | "resume" | "clear" | "compact"; + session_id?: string; + transcript_path?: string; + cwd?: string; + hook_event_name?: string; + source?: "startup" | "resume" | "clear" | "compact"; [key: string]: any; } @@ -15,18 +16,19 @@ export interface SessionStartInput { * Shows user what happened in recent sessions */ export function contextHook(input?: SessionStartInput): void { - if (!input) { - throw new Error('contextHook requires input'); - } + const cwd = input?.cwd ?? process.cwd(); + const project = cwd ? path.basename(cwd) : 'unknown-project'; - const project = input.cwd ? path.basename(input.cwd) : path.basename(path.dirname(input.transcript_path)); const db = new HooksDatabase(); try { const summaries = db.getRecentSummaries(project, 5); if (summaries.length === 0) { - console.log('# Recent Session Context\n\nNo previous sessions found for this project yet.'); + const response = createHookResponse('SessionStart', true, { + context: '# Recent Session Context\n\nNo previous sessions found for this project yet.' + }); + console.log(response); return; } @@ -87,8 +89,11 @@ export function contextHook(input?: SessionStartInput): void { output.push(''); } - console.log(output.join('\n')); + const response = createHookResponse('SessionStart', true, { + context: output.join('\n') + }); + console.log(response); } finally { db.close(); } -} +} \ No newline at end of file diff --git a/src/hooks/hook-response.ts b/src/hooks/hook-response.ts new file mode 100644 index 00000000..a1a0b838 --- /dev/null +++ b/src/hooks/hook-response.ts @@ -0,0 +1,87 @@ +export type HookType = 'PreCompact' | 'SessionStart' | 'UserPromptSubmit' | 'PostToolUse' | 'Stop' | string; + +export interface HookResponseOptions { + reason?: string; + context?: string; +} + +export interface HookResponse { + continue?: boolean; + suppressOutput?: boolean; + stopReason?: string; + hookSpecificOutput?: { + hookEventName: 'SessionStart'; + additionalContext: string; + }; +} + +function buildHookResponse( + hookType: HookType, + success: boolean, + options: HookResponseOptions +): HookResponse { + if (hookType === 'PreCompact') { + if (success) { + return { + continue: true, + suppressOutput: true + }; + } + + return { + continue: false, + stopReason: options.reason || 'Pre-compact operation failed', + suppressOutput: true + }; + } + + if (hookType === 'SessionStart') { + if (success && options.context) { + return { + continue: true, + suppressOutput: true, + hookSpecificOutput: { + hookEventName: 'SessionStart', + additionalContext: options.context + } + }; + } + + return { + continue: true, + suppressOutput: true + }; + } + + if (hookType === 'UserPromptSubmit' || hookType === 'PostToolUse') { + return { + continue: true, + suppressOutput: true + }; + } + + if (hookType === 'Stop') { + return { + continue: true, + suppressOutput: true + }; + } + + return { + continue: success, + suppressOutput: true, + ...(options.reason && !success ? { stopReason: options.reason } : {}) + }; +} + +/** + * Creates a standardized hook response using the HookTemplates system. + */ +export function createHookResponse( + hookType: HookType, + success: boolean, + options: HookResponseOptions = {} +): string { + const response = buildHookResponse(hookType, success, options); + return JSON.stringify(response); +} diff --git a/src/hooks/new.ts b/src/hooks/new.ts index fa7a70c4..5550ef37 100644 --- a/src/hooks/new.ts +++ b/src/hooks/new.ts @@ -1,6 +1,7 @@ -import { HooksDatabase } from '../services/sqlite/HooksDatabase.js'; -import path from 'path'; import { spawn } from 'child_process'; +import path from 'path'; +import { HooksDatabase } from '../services/sqlite/HooksDatabase.js'; +import { createHookResponse } from './hook-response.js'; export interface UserPromptSubmitInput { session_id: string; @@ -26,7 +27,7 @@ export function newHook(input?: UserPromptSubmitInput): void { const existing = db.findActiveSDKSession(session_id); if (existing) { - console.log('{"continue": true, "suppressOutput": true}'); + console.log(createHookResponse('UserPromptSubmit', true)); return; } @@ -46,7 +47,7 @@ export function newHook(input?: UserPromptSubmitInput): void { child.unref(); - console.log('{"continue": true, "suppressOutput": true}'); + console.log(createHookResponse('UserPromptSubmit', true)); } finally { db.close(); } diff --git a/src/hooks/save.ts b/src/hooks/save.ts index 44c8d125..fefccbd8 100644 --- a/src/hooks/save.ts +++ b/src/hooks/save.ts @@ -1,6 +1,7 @@ import net from 'net'; import { HooksDatabase } from '../services/sqlite/HooksDatabase.js'; import { getWorkerSocketPath } from '../shared/paths.js'; +import { createHookResponse } from './hook-response.js'; export interface PostToolUseInput { session_id: string; @@ -29,7 +30,7 @@ export function saveHook(input?: PostToolUseInput): void { const { session_id, tool_name, tool_input, tool_output } = input; if (SKIP_TOOLS.has(tool_name)) { - console.log('{"continue": true, "suppressOutput": true}'); + console.log(createHookResponse('PostToolUse', true)); return; } @@ -38,7 +39,7 @@ export function saveHook(input?: PostToolUseInput): void { db.close(); if (!session) { - console.log('{"continue": true, "suppressOutput": true}'); + console.log(createHookResponse('PostToolUse', true)); return; } @@ -55,7 +56,15 @@ export function saveHook(input?: PostToolUseInput): void { client.end(); }); - client.on('close', () => { - console.log('{"continue": true, "suppressOutput": true}'); - }); + let responded = false; + const respond = () => { + if (responded) { + return; + } + responded = true; + console.log(createHookResponse('PostToolUse', true)); + }; + + client.on('close', respond); + client.on('error', respond); } diff --git a/src/hooks/summary.ts b/src/hooks/summary.ts index 298b8728..d7019a5d 100644 --- a/src/hooks/summary.ts +++ b/src/hooks/summary.ts @@ -1,6 +1,7 @@ import net from 'net'; import { HooksDatabase } from '../services/sqlite/HooksDatabase.js'; import { getWorkerSocketPath } from '../shared/paths.js'; +import { createHookResponse } from './hook-response.js'; export interface StopInput { session_id: string; @@ -23,7 +24,7 @@ export function summaryHook(input?: StopInput): void { db.close(); if (!session) { - console.log('{"continue": true, "suppressOutput": true}'); + console.log(createHookResponse('Stop', true)); return; } @@ -37,7 +38,15 @@ export function summaryHook(input?: StopInput): void { client.end(); }); - client.on('close', () => { - console.log('{"continue": true, "suppressOutput": true}'); - }); + let responded = false; + const respond = () => { + if (responded) { + return; + } + responded = true; + console.log(createHookResponse('Stop', true)); + }; + + client.on('close', respond); + client.on('error', respond); } diff --git a/~/.claude-mem/chroma/chroma.sqlite3 b/~/.claude-mem/chroma/chroma.sqlite3 deleted file mode 100644 index d23ea3a93659b8a99d8f465533899e6759fe49b8..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 163840 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 A1ONa4