1515// }
1616// }
1717//
18- // The same command handles both events; it dispatches on hook_event_name
19- // from the JSON payload.
18+ // The same command handles both events; it dispatches on hook_event_name.
19+ // Unlike the Codex/Antigravity adapters, `Stop` recovers both the user and
20+ // assistant text from the session transcript file rather than the payload.
2021
2122import { readFile } from 'node:fs/promises'
22- import { BaseAdapter } from './adapter.js'
23+ import { StdinHookAdapter } from './stdin-hook-adapter.js'
24+ import type { HookInput , HookTurn } from './stdin-hook-adapter.js'
2325import type { MemoriaAdapterConfig } from './adapter.js'
24- import { hashTurn } from './hook-state.js'
25- import type { RecallHit } from '../core/types.js'
2626
2727export interface ClaudeCodeAdapterConfig extends MemoriaAdapterConfig {
2828 /** Max transcript lines to scan backwards when locating the last turn (default: 50) */
@@ -46,86 +46,37 @@ export interface ClaudeCodeHookOutput {
4646 }
4747}
4848
49- export class ClaudeCodeAdapter extends BaseAdapter {
49+ export class ClaudeCodeAdapter extends StdinHookAdapter {
50+ protected readonly injectEvents = [ 'UserPromptSubmit' ] as const
51+ protected readonly stopEvents = [ 'Stop' ] as const
52+ protected readonly defaultConversationId = 'claude-code-session'
53+
5054 private readonly transcriptScanLimit : number
5155
5256 constructor ( config : ClaudeCodeAdapterConfig ) {
5357 super ( config )
5458 this . transcriptScanLimit = config . transcriptScanLimit ?? 50
5559 }
5660
57- /**
58- * Dispatch on hook_event_name. Always resolves; never throws.
59- * Errors are swallowed when failOpen is true (the default).
60- */
61- async handleHookEvent ( input : ClaudeCodeHookInput ) : Promise < ClaudeCodeHookOutput > {
62- const event = input . hook_event_name ?? ''
63- if ( event === 'UserPromptSubmit' ) return this . handleUserPromptSubmit ( input )
64- if ( event === 'Stop' ) {
65- await this . handleStop ( input )
66- return { }
67- }
68- return { }
61+ /** Recover the completed turn from the session transcript rather than the payload. */
62+ protected async extractTurn ( input : HookInput ) : Promise < HookTurn | null > {
63+ const transcriptPath = typeof input . transcript_path === 'string' ? input . transcript_path : ''
64+ if ( ! transcriptPath ) return null
65+ return this . readLastTurn ( transcriptPath )
6966 }
7067
71- /** Recall relevant memory and inject it via additionalContext. */
72- async handleUserPromptSubmit ( input : ClaudeCodeHookInput ) : Promise < ClaudeCodeHookOutput > {
73- const userMessage = ( input . prompt ?? '' ) . trim ( )
74- const conversationId = input . session_id ?? 'claude-code-session'
75- if ( ! userMessage ) {
76- return { hookSpecificOutput : { hookEventName : 'UserPromptSubmit' } }
77- }
78-
79- try {
80- const rc = await this . recallForContext ( { userMessage, conversationId } )
81- return {
82- hookSpecificOutput : {
83- hookEventName : 'UserPromptSubmit' ,
84- additionalContext : rc . injectedText
85- }
86- }
87- } catch ( error ) {
88- if ( ! this . config . failOpen ) throw error
89- return { hookSpecificOutput : { hookEventName : 'UserPromptSubmit' } }
90- }
91- }
92-
93- /** Persist the last user/assistant turn from the transcript file. */
94- async handleStop ( input : ClaudeCodeHookInput ) : Promise < void > {
95- try {
96- const transcriptPath = input . transcript_path
97- const conversationId = input . session_id ?? 'claude-code-session'
98- if ( ! transcriptPath ) return
99-
100- const turn = await this . readLastTurn ( transcriptPath )
101- if ( ! turn ) return
102- const contentHash = hashTurn ( `${ turn . user } \n${ turn . assistant } ` )
103- if ( ! this . shouldWrite ( conversationId , contentHash ) ) return
104-
105- await this . client . remember ( {
106- timestamp : new Date ( ) . toISOString ( ) ,
107- project : this . config . project ,
108- summary : turn . assistant . slice ( 0 , 200 ) ,
109- events : [
110- {
111- event_type : 'ConversationTurn' ,
112- timestamp : new Date ( ) . toISOString ( ) ,
113- content : { user : turn . user , assistant : turn . assistant }
114- }
115- ]
116- } )
117- this . markWritten ( conversationId , contentHash )
118- } catch ( error ) {
119- if ( ! this . config . failOpen ) throw error
120- }
68+ protected buildInjectOutput ( eventName : string , text ?: string ) : ClaudeCodeHookOutput {
69+ const output : ClaudeCodeHookOutput = { hookSpecificOutput : { hookEventName : eventName } }
70+ if ( text !== undefined ) output . hookSpecificOutput ! . additionalContext = text
71+ return output
12172 }
12273
12374 /**
12475 * Scan the JSONL transcript backwards and extract the most recent
12576 * user/assistant pair. Each transcript line is a message envelope
12677 * like `{"type":"user","message":{"role":"user","content":[{"type":"text","text":"..."}]}}`.
12778 */
128- private async readLastTurn ( transcriptPath : string ) : Promise < { user : string ; assistant : string } | null > {
79+ private async readLastTurn ( transcriptPath : string ) : Promise < HookTurn | null > {
12980 const raw = await readFile ( transcriptPath , 'utf8' ) . catch ( ( ) => '' )
13081 if ( ! raw ) return null
13182 const lines = raw . split ( '\n' ) . filter ( Boolean ) . slice ( - this . transcriptScanLimit )
@@ -143,20 +94,6 @@ export class ClaudeCodeAdapter extends BaseAdapter {
14394 if ( ! lastUser || ! lastAssistant ) return null
14495 return { user : lastUser , assistant : lastAssistant }
14596 }
146-
147- protected override formatRecallText ( hits : RecallHit [ ] ) : string {
148- if ( hits . length === 0 ) return ''
149- const lines = hits . map ( ( h ) => {
150- const date = h . timestamp . slice ( 0 , 10 )
151- const tag = h . type === 'decision' ? 'Decision' : h . type === 'skill' ? 'Skill' : 'Session'
152- return `- [${ tag } ${ date } @ ${ h . project } ] ${ h . snippet } `
153- } )
154- return [
155- '## Memoria — Relevant past memory' ,
156- ...lines ,
157- ''
158- ] . join ( '\n' )
159- }
16097}
16198
16299function extractRoleFromTranscriptLine ( line : string ) : 'user' | 'assistant' | null {
0 commit comments