/** * Context Handler - SessionStart * * Extracted from context-hook.ts - calls worker to generate context. * Returns context as hookSpecificOutput for Claude Code to inject. */ import type { EventHandler, NormalizedHookInput, HookResult } from '../types.js'; import { executeWithWorkerFallback, isWorkerFallback, getWorkerPort, } from '../../shared/worker-utils.js'; import { getProjectContext } from '../../utils/project-name.js'; import { HOOK_EXIT_CODES } from '../../shared/hook-constants.js'; import { logger } from '../../utils/logger.js'; import { loadFromFileOnce } from '../../shared/hook-settings.js'; export const contextHandler: EventHandler = { async execute(input: NormalizedHookInput): Promise { const cwd = input.cwd ?? process.cwd(); const context = getProjectContext(cwd); const port = getWorkerPort(); // Plan 05 Phase 4: settings via process-scope cache. const settings = loadFromFileOnce(); const showTerminalOutput = settings.CLAUDE_MEM_CONTEXT_SHOW_TERMINAL_OUTPUT === 'true'; // Pass all projects (parent + worktree if applicable) for unified timeline const projectsParam = context.allProjects.join(','); const apiPath = `/api/context/inject?projects=${encodeURIComponent(projectsParam)}`; const colorApiPath = input.platform === 'claude-code' ? `${apiPath}&colors=true` : apiPath; const emptyResult: HookResult = { hookSpecificOutput: { hookEventName: 'SessionStart', additionalContext: '' }, exitCode: HOOK_EXIT_CODES.SUCCESS, }; // Plan 05 Phase 2: single helper for ensure-worker-alive → request → fallback. const contextResult = await executeWithWorkerFallback(apiPath, 'GET'); if (isWorkerFallback(contextResult)) { return emptyResult; } let additionalContext: string; if (typeof contextResult === 'string') { additionalContext = contextResult.trim(); } else if (contextResult === undefined) { additionalContext = ''; } else { // Unexpected non-string body — log and fall back to empty. logger.warn('HOOK', 'Context response was not a string', { type: typeof contextResult }); return emptyResult; } let coloredTimeline = ''; if (showTerminalOutput) { const colorResult = await executeWithWorkerFallback(colorApiPath, 'GET'); if (!isWorkerFallback(colorResult) && typeof colorResult === 'string') { coloredTimeline = colorResult.trim(); } } const platform = input.platform; // Use colored timeline for display if available, otherwise fall back to // plain markdown context (especially useful for platforms like Gemini // where we want to ensure visibility even if colors aren't fetched). const displayContent = coloredTimeline || (platform === 'gemini-cli' || platform === 'gemini' ? additionalContext : ''); const systemMessage = showTerminalOutput && displayContent ? `${displayContent}\n\nView Observations Live @ http://localhost:${port}` : undefined; return { hookSpecificOutput: { hookEventName: 'SessionStart', additionalContext }, systemMessage }; } };