2026-10-24 23:00:49 -04:00
|
|
|
/**
|
|
|
|
|
* @fileoverview `codeman history read`: the command-line front of the offline
|
|
|
|
|
* Codex history reader (`local-history.ts`).
|
|
|
|
|
*
|
|
|
|
|
* Deliberately separate from `codeman agent` (`cli-agent.ts`): that command is
|
|
|
|
|
* a client of a running Codeman and refuses outside a managed session, while
|
|
|
|
|
* this one reads rollout files from disk and needs neither a server nor
|
|
|
|
|
* `CODEMAN_MUX`. It never writes, never inspects a live pane and never sends
|
|
|
|
|
* input.
|
|
|
|
|
*
|
|
|
|
|
* Every bound the reader enforces has a flag here with the reader's own
|
|
|
|
|
* maximum, so a large store is a matter of raising a number rather than an
|
|
|
|
|
* impossible lookup. Output is JSON unless `--format text` is given; `--json`
|
|
|
|
|
* is the JSON spelling and refuses to combine with `--format text`.
|
|
|
|
|
*
|
|
|
|
|
* @module cli-history
|
|
|
|
|
*/
|
2026-10-24 20:43:17 -04:00
|
|
|
import type { Command } from 'commander';
|
2026-10-24 23:00:49 -04:00
|
|
|
import {
|
|
|
|
|
formatLocalHistory,
|
|
|
|
|
HISTORY_DEFAULT_MAX_ENTRIES,
|
|
|
|
|
HISTORY_DEFAULT_METADATA_BYTES,
|
|
|
|
|
HISTORY_DEFAULT_OUTPUT_BYTES,
|
|
|
|
|
HISTORY_DEFAULT_SCAN_BYTES,
|
|
|
|
|
HISTORY_MAX_ENTRIES,
|
|
|
|
|
HISTORY_MAX_METADATA_BYTES,
|
|
|
|
|
HISTORY_MAX_OUTPUT_BYTES,
|
|
|
|
|
HISTORY_MAX_SCAN_BYTES,
|
|
|
|
|
readLocalHistory,
|
2026-10-25 09:08:54 -04:00
|
|
|
validateOutputBytes,
|
2026-10-24 23:00:49 -04:00
|
|
|
} from './local-history.js';
|
2026-10-24 20:43:17 -04:00
|
|
|
|
2026-10-25 09:08:54 -04:00
|
|
|
/**
|
|
|
|
|
* A budget flag's value as a number. Only plain decimal digits count, so `0x10`,
|
|
|
|
|
* `1e3` and ` 5 ` are refused rather than read as 16, 1000 and 5; anything else
|
|
|
|
|
* becomes NaN, which the reader's range check rejects with the flag's name.
|
|
|
|
|
*/
|
|
|
|
|
function budgetFlag(value: string | boolean | undefined): number {
|
|
|
|
|
return typeof value === 'string' && /^\d+$/.test(value) ? Number(value) : NaN;
|
|
|
|
|
}
|
|
|
|
|
|
2026-10-24 20:43:17 -04:00
|
|
|
export function registerHistoryCommands(program: Command): Command {
|
|
|
|
|
const history = program
|
|
|
|
|
.command('history')
|
|
|
|
|
.description('Read local saved native transcripts without a running server');
|
|
|
|
|
history
|
|
|
|
|
.command('read')
|
|
|
|
|
.description('Read Codex history by file, native id, or root conversation and descendants')
|
|
|
|
|
.option('--file <path>', 'Explicit local JSONL transcript (regular file only)')
|
|
|
|
|
.option('--session-id <id>', 'Native Codex conversation UUID or unique prefix (at least 8 characters)')
|
|
|
|
|
.option('--root-session-id <uuid>', 'Native root UUID, including transitive child conversations')
|
2026-10-24 23:00:49 -04:00
|
|
|
.option(
|
|
|
|
|
'--sessions-root <path>',
|
|
|
|
|
'Local Codex sessions directory (default: $CODEX_HOME/sessions, else ~/.codex/sessions)'
|
|
|
|
|
)
|
2026-10-24 20:43:17 -04:00
|
|
|
.option('--topic <text>', 'Case-insensitive literal match, preserving whole matching turns')
|
2026-10-24 23:00:49 -04:00
|
|
|
.option('--after <date>', 'Inclusive YYYY-MM-DD lower bound, as a UTC date; preserves whole matching turns')
|
|
|
|
|
.option('--before <date>', 'Inclusive YYYY-MM-DD upper bound, as a UTC date; preserves whole matching turns')
|
|
|
|
|
.option(
|
|
|
|
|
'--max-bytes <n>',
|
|
|
|
|
`Maximum complete output bytes (1-${HISTORY_MAX_OUTPUT_BYTES})`,
|
|
|
|
|
String(HISTORY_DEFAULT_OUTPUT_BYTES)
|
|
|
|
|
)
|
2026-10-24 20:43:17 -04:00
|
|
|
.option(
|
|
|
|
|
'--scan-bytes <n>',
|
2026-10-24 23:00:49 -04:00
|
|
|
`Read budget for the selected transcripts (1-${HISTORY_MAX_SCAN_BYTES})`,
|
2026-10-24 20:43:17 -04:00
|
|
|
String(HISTORY_DEFAULT_SCAN_BYTES)
|
|
|
|
|
)
|
2026-10-24 23:00:49 -04:00
|
|
|
.option(
|
|
|
|
|
'--metadata-bytes <n>',
|
|
|
|
|
`Read budget for attributing every rollout in the store (1-${HISTORY_MAX_METADATA_BYTES})`,
|
|
|
|
|
String(HISTORY_DEFAULT_METADATA_BYTES)
|
|
|
|
|
)
|
|
|
|
|
.option(
|
|
|
|
|
'--max-entries <n>',
|
|
|
|
|
`Directory entries visited in the store (1-${HISTORY_MAX_ENTRIES})`,
|
|
|
|
|
String(HISTORY_DEFAULT_MAX_ENTRIES)
|
|
|
|
|
)
|
2026-10-24 20:43:17 -04:00
|
|
|
.option('--format <format>', 'json or text', 'json')
|
2026-10-24 23:00:49 -04:00
|
|
|
.option('--json', 'JSON output (the default; conflicts with --format text)')
|
|
|
|
|
.action(async (options: Record<string, string | boolean | undefined>, command: Command) => {
|
2026-10-24 20:43:17 -04:00
|
|
|
try {
|
2026-10-24 23:00:49 -04:00
|
|
|
if (options.format !== 'json' && options.format !== 'text') throw new Error('--format must be json or text');
|
|
|
|
|
if (options.json && options.format === 'text' && command.getOptionValueSource('format') === 'cli')
|
|
|
|
|
throw new Error('--json conflicts with --format text; pass one of them');
|
|
|
|
|
const format = options.format;
|
2026-10-25 09:08:54 -04:00
|
|
|
// Every flag is checked before the first file is read: --max-bytes would otherwise
|
|
|
|
|
// only be judged after the whole store had been scanned.
|
|
|
|
|
const maxBytes = budgetFlag(options.maxBytes);
|
|
|
|
|
validateOutputBytes(maxBytes);
|
2026-10-24 20:43:17 -04:00
|
|
|
const result = await readLocalHistory({
|
|
|
|
|
file: options.file as string | undefined,
|
|
|
|
|
sessionId: options.sessionId as string | undefined,
|
|
|
|
|
rootSessionId: options.rootSessionId as string | undefined,
|
|
|
|
|
sessionsRoot: options.sessionsRoot as string | undefined,
|
|
|
|
|
topic: options.topic as string | undefined,
|
|
|
|
|
after: options.after as string | undefined,
|
|
|
|
|
before: options.before as string | undefined,
|
2026-10-25 09:08:54 -04:00
|
|
|
scanBytes: budgetFlag(options.scanBytes),
|
|
|
|
|
metadataBytes: budgetFlag(options.metadataBytes),
|
|
|
|
|
maxEntries: budgetFlag(options.maxEntries),
|
2026-10-24 20:43:17 -04:00
|
|
|
});
|
2026-10-25 09:08:54 -04:00
|
|
|
program.configureOutput().writeOut?.(`${formatLocalHistory(result, format, maxBytes)}\n`);
|
2026-10-24 20:43:17 -04:00
|
|
|
} catch (error) {
|
|
|
|
|
program.error(error instanceof Error ? error.message : String(error), { exitCode: 1, code: 'history.read' });
|
|
|
|
|
}
|
|
|
|
|
});
|
|
|
|
|
return history;
|
|
|
|
|
}
|