import { existsSync, readFileSync, writeFileSync, mkdirSync } from 'fs'; import { homedir } from 'os'; import { join, dirname } from 'path'; import { paths } from '../../shared/paths.js'; import type { TranscriptSchema, TranscriptWatchConfig } from './types.js'; export const DEFAULT_CONFIG_PATH = paths.transcriptsConfig(); export const DEFAULT_STATE_PATH = paths.transcriptsState(); export const CODEX_SAMPLE_SCHEMA: TranscriptSchema = { name: 'codex', version: '0.3', description: 'Legacy schema for Codex session JSONL files. Codex native hooks are preferred.', events: [ { name: 'session-meta', match: { path: 'type', equals: 'session_meta' }, action: 'session_context', fields: { sessionId: 'payload.id', cwd: 'payload.cwd' } }, { name: 'turn-context', match: { path: 'type', equals: 'turn_context' }, action: 'session_context', fields: { cwd: 'payload.cwd' } }, { name: 'user-message', match: { path: 'payload.type', equals: 'user_message' }, action: 'session_init', fields: { prompt: 'payload.message' } }, { name: 'assistant-message', match: { path: 'payload.type', equals: 'agent_message' }, action: 'assistant_message', fields: { message: 'payload.message' } }, { name: 'tool-use', match: { path: 'payload.type', in: ['function_call', 'custom_tool_call', 'web_search_call'] }, action: 'tool_use', fields: { toolId: 'payload.call_id', toolName: { coalesce: [ 'payload.name', 'payload.type' ] }, toolInput: { coalesce: [ 'payload.arguments', 'payload.input', 'payload.command', 'payload.action' ] } } }, { name: 'tool-result', match: { path: 'payload.type', in: ['function_call_output', 'custom_tool_call_output'] }, action: 'tool_result', fields: { toolId: 'payload.call_id', toolResponse: 'payload.output' } }, { name: 'exec-command-end', match: { path: 'payload.type', in: ['exec_command_end', 'exec_command_output'] }, action: 'observation', fields: { toolUseId: 'payload.call_id', toolName: { value: 'exec_command' }, toolInput: { coalesce: [ 'payload.command', 'payload.input' ] }, toolResponse: { coalesce: [ 'payload.aggregated_output', 'payload.output', 'payload.stdout', 'payload.stderr' ] } } }, { name: 'session-end', match: { path: 'payload.type', in: ['turn_aborted', 'turn_completed', 'task_complete'] }, action: 'session_end' } ] }; export const SAMPLE_CONFIG: TranscriptWatchConfig = { version: 1, schemas: {}, watches: [], stateFile: DEFAULT_STATE_PATH }; export function isNativeHookBackedCodexWatch(watch: { name?: string; path?: string; schema?: string | TranscriptSchema }): boolean { const schemaName = typeof watch.schema === 'string' ? watch.schema : watch.schema?.name; const nameOrSchemaIsCodex = watch.name === 'codex' || schemaName === 'codex'; if (!nameOrSchemaIsCodex || !watch.path) return false; const normalizedPath = expandHomePath(watch.path).replace(/\\/g, '/'); const codexSessionsRoot = join(homedir(), '.codex', 'sessions').replace(/\\/g, '/'); return normalizedPath === `${codexSessionsRoot}/**/*.jsonl`; } export function filterNativeHookBackedCodexWatches( config: TranscriptWatchConfig, allowCodexTranscriptIngestion: boolean ): { config: TranscriptWatchConfig; removed: number } { if (allowCodexTranscriptIngestion) { return { config, removed: 0 }; } const watches = config.watches.filter(watch => !isNativeHookBackedCodexWatch(watch)); return { config: { ...config, watches, }, removed: config.watches.length - watches.length, }; } export function expandHomePath(inputPath: string): string { if (!inputPath) return inputPath; if (inputPath.startsWith('~')) { return join(homedir(), inputPath.slice(1)); } return inputPath; } export function loadTranscriptWatchConfig(path = DEFAULT_CONFIG_PATH): TranscriptWatchConfig { const resolvedPath = expandHomePath(path); if (!existsSync(resolvedPath)) { throw new Error(`Transcript watch config not found: ${resolvedPath}`); } const raw = readFileSync(resolvedPath, 'utf-8'); const parsed = JSON.parse(raw) as TranscriptWatchConfig; if (!parsed.version || !parsed.watches) { throw new Error(`Invalid transcript watch config: ${resolvedPath}`); } if (!parsed.stateFile) { parsed.stateFile = DEFAULT_STATE_PATH; } return parsed; } export function writeSampleConfig(path = DEFAULT_CONFIG_PATH): void { const resolvedPath = expandHomePath(path); const dir = dirname(resolvedPath); if (!existsSync(dir)) { mkdirSync(dir, { recursive: true }); } writeFileSync(resolvedPath, JSON.stringify(SAMPLE_CONFIG, null, 2)); }