Files
claude-mem/src/services/transcripts/config.ts
T

177 lines
5.1 KiB
TypeScript

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));
}