2026-07-12 00:28:17 +08:00
|
|
|
// Core's pure parse/discover helpers — node:sqlite-free by construction, so the compiled
|
2026-07-09 09:20:37 +08:00
|
|
|
// providers can be consumed by the app (better-sqlite3 / a Node without
|
2026-07-12 00:37:32 +08:00
|
|
|
// node:sqlite). Originally extracted verbatim from db/indexer; it now exposes a
|
|
|
|
|
// typed seam while remaining limited to node:fs/path/os.
|
2026-08-04 05:49:51 +08:00
|
|
|
import { closeSync, existsSync, openSync, readSync, readdirSync, statSync } from 'node:fs';
|
|
|
|
|
import { homedir } from 'node:os';
|
|
|
|
|
import { isAbsolute, join, normalize } from 'node:path';
|
2026-07-09 09:20:37 +08:00
|
|
|
|
2026-08-04 11:33:01 -04:00
|
|
|
import type { InventoryIssue } from './providers/types.ts';
|
|
|
|
|
|
2026-08-04 05:49:51 +08:00
|
|
|
const CLAUDE_DIR = join(homedir(), '.claude');
|
|
|
|
|
const CODEX_DIR = join(homedir(), '.codex');
|
|
|
|
|
const PROJECTS_DIR = join(CLAUDE_DIR, 'projects');
|
|
|
|
|
const CODEX_SESSIONS_DIR = join(CODEX_DIR, 'sessions');
|
2026-07-09 09:20:37 +08:00
|
|
|
const TEXT_LIMIT = 10000;
|
|
|
|
|
|
2026-07-12 00:37:32 +08:00
|
|
|
type JsonRecord = Record<string, any>;
|
|
|
|
|
type JsonValue = any;
|
|
|
|
|
|
2026-08-04 11:33:01 -04:00
|
|
|
function sourceInventoryIssue(path: string, error: unknown): InventoryIssue {
|
|
|
|
|
return { path, error: error instanceof Error ? error.message : String(error) };
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
type DiscoveryIssueHandler = (issue: InventoryIssue) => void;
|
|
|
|
|
|
2026-07-12 00:44:06 +08:00
|
|
|
export interface ClaudeJsonlFile {
|
2026-07-12 00:37:32 +08:00
|
|
|
path: string;
|
|
|
|
|
sessionId: string;
|
|
|
|
|
project: string;
|
|
|
|
|
isSubagent: boolean;
|
|
|
|
|
agentId?: string;
|
|
|
|
|
workflowRunId?: string;
|
|
|
|
|
source?: 'claude';
|
|
|
|
|
}
|
|
|
|
|
|
2026-07-12 00:44:06 +08:00
|
|
|
export interface CodexJsonlFile {
|
2026-07-12 00:37:32 +08:00
|
|
|
path: string;
|
|
|
|
|
source: 'codex';
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
interface CodexLineRecord {
|
|
|
|
|
lineNum: number;
|
|
|
|
|
obj: JsonRecord;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// ---- message/text helpers ----
|
|
|
|
|
function trunc(s: any): any {
|
2026-07-09 09:20:37 +08:00
|
|
|
return typeof s === 'string' && s.length > TEXT_LIMIT ? s.slice(0, TEXT_LIMIT) : s;
|
|
|
|
|
}
|
|
|
|
|
|
2026-07-12 00:37:32 +08:00
|
|
|
function truncJson(obj: JsonValue, limit = TEXT_LIMIT): string | null {
|
2026-07-09 09:20:37 +08:00
|
|
|
if (obj === null || obj === undefined) return null;
|
2026-07-12 00:37:32 +08:00
|
|
|
const walk = (v: JsonValue): JsonValue => {
|
2026-07-09 09:20:37 +08:00
|
|
|
if (typeof v === 'string') return v.length > limit ? v.slice(0, limit) + '...[truncated]' : v;
|
|
|
|
|
if (Array.isArray(v)) return v.map(walk);
|
|
|
|
|
if (typeof v === 'object' && v !== null) {
|
2026-07-12 00:37:32 +08:00
|
|
|
const out: JsonRecord = {};
|
2026-07-09 09:20:37 +08:00
|
|
|
for (const [k, val] of Object.entries(v)) out[k] = walk(val);
|
|
|
|
|
return out;
|
|
|
|
|
}
|
|
|
|
|
return v;
|
|
|
|
|
};
|
|
|
|
|
return JSON.stringify(walk(obj));
|
|
|
|
|
}
|
|
|
|
|
|
2026-07-12 00:37:32 +08:00
|
|
|
function extractText(content: JsonValue): string | null {
|
2026-07-09 09:20:37 +08:00
|
|
|
if (typeof content === 'string') return trunc(content);
|
|
|
|
|
if (!Array.isArray(content)) return null;
|
2026-07-12 00:37:32 +08:00
|
|
|
const parts: string[] = [];
|
2026-07-09 09:20:37 +08:00
|
|
|
for (const b of content) {
|
|
|
|
|
if (b.type === 'text' && b.text) parts.push(b.text);
|
|
|
|
|
else if (b.type === 'thinking' && b.thinking) parts.push(b.thinking);
|
|
|
|
|
}
|
|
|
|
|
return parts.length ? trunc(parts.join('\n')) : null;
|
|
|
|
|
}
|
|
|
|
|
|
2026-07-12 00:37:32 +08:00
|
|
|
function extractContentType(content: JsonValue): string {
|
2026-07-09 09:20:37 +08:00
|
|
|
if (typeof content === 'string') return 'text';
|
|
|
|
|
if (!Array.isArray(content) || !content.length) return 'unknown';
|
2026-07-12 00:37:32 +08:00
|
|
|
const types = new Set<string>();
|
2026-07-09 09:20:37 +08:00
|
|
|
let sawUnknown = false;
|
|
|
|
|
for (const b of content) {
|
|
|
|
|
if (!b || typeof b !== 'object') { sawUnknown = true; continue; }
|
|
|
|
|
if (b.type === 'text') types.add('text');
|
|
|
|
|
else if (b.type === 'thinking') types.add('thinking');
|
|
|
|
|
else if (b.type === 'tool_use') types.add('tool_use');
|
|
|
|
|
else if (b.type === 'tool_result') types.add('tool_result');
|
|
|
|
|
else sawUnknown = true;
|
|
|
|
|
}
|
|
|
|
|
return !sawUnknown && types.size === 1 ? [...types][0] : 'unknown';
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
const COMMAND_ENVELOPE_RE = /^\s*(<command-name>[^<]+<\/command-name>|<(?:task-notification|system-reminder)\b|<local-command(?:\b|-))/;
|
2026-07-21 00:58:34 +08:00
|
|
|
const SKILL_INSTRUCTIONS_RE = /^\s*Base directory for this skill(?:\s*:|\s*\r?\n)/;
|
2026-07-09 09:20:37 +08:00
|
|
|
|
2026-07-12 00:37:32 +08:00
|
|
|
function extractMessageIsMeta(record: JsonRecord, text: string | null = extractText(record?.message?.content)): 0 | 1 {
|
2026-07-09 09:20:37 +08:00
|
|
|
const msg = record?.message || {};
|
|
|
|
|
if (record?.isMeta === true || msg.isMeta === true) return 1;
|
|
|
|
|
return typeof text === 'string' && COMMAND_ENVELOPE_RE.test(text) ? 1 : 0;
|
|
|
|
|
}
|
|
|
|
|
|
2026-07-21 00:58:34 +08:00
|
|
|
function isSkillInstructions(text: unknown): boolean {
|
|
|
|
|
return typeof text === 'string' && SKILL_INSTRUCTIONS_RE.test(text);
|
|
|
|
|
}
|
|
|
|
|
|
2026-07-12 00:37:32 +08:00
|
|
|
function filePath(name: string, input: JsonRecord | null | undefined): string | null {
|
2026-07-09 09:20:37 +08:00
|
|
|
if (!input) return null;
|
|
|
|
|
return ['Read', 'Edit', 'Write', 'NotebookEdit'].includes(name) ? (input.file_path || null) : null;
|
|
|
|
|
}
|
|
|
|
|
|
2026-08-04 05:49:51 +08:00
|
|
|
function isDir(p: string): boolean { try { return statSync(p).isDirectory(); } catch { return false; } }
|
2026-07-09 09:20:37 +08:00
|
|
|
|
2026-07-12 00:37:32 +08:00
|
|
|
function readLines(filePath: string, callback: (line: string) => boolean | void): void {
|
2026-08-04 05:49:51 +08:00
|
|
|
const fd = openSync(filePath, 'r');
|
2026-07-09 09:20:37 +08:00
|
|
|
const bufSize = 64 * 1024;
|
|
|
|
|
const buf = Buffer.alloc(bufSize);
|
|
|
|
|
let remainder = '';
|
|
|
|
|
let bytesRead;
|
|
|
|
|
try {
|
2026-08-04 05:49:51 +08:00
|
|
|
while ((bytesRead = readSync(fd, buf, 0, bufSize, null)) > 0) {
|
2026-07-09 09:20:37 +08:00
|
|
|
const chunk = remainder + buf.toString('utf8', 0, bytesRead);
|
|
|
|
|
const lines = chunk.split('\n');
|
2026-07-12 00:37:32 +08:00
|
|
|
remainder = lines.pop() ?? '';
|
2026-07-09 09:20:37 +08:00
|
|
|
for (const line of lines) {
|
|
|
|
|
if (line && callback(line) === false) return;
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
if (remainder) callback(remainder);
|
|
|
|
|
} finally {
|
2026-08-04 05:49:51 +08:00
|
|
|
closeSync(fd);
|
2026-07-09 09:20:37 +08:00
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
2026-07-12 00:37:32 +08:00
|
|
|
// ---- project-path + discovery helpers ----
|
|
|
|
|
function legacyProjectPathFromSlug(project: string | null | undefined): string | null {
|
2026-07-09 09:20:37 +08:00
|
|
|
if (!project) return null;
|
|
|
|
|
return '/' + project.replace(/-/g, '/').replace(/^\//, '');
|
|
|
|
|
}
|
|
|
|
|
|
2026-07-12 00:37:32 +08:00
|
|
|
function normalizeObservedCwd(cwd: unknown): string | null {
|
2026-08-04 05:49:51 +08:00
|
|
|
if (typeof cwd !== 'string' || !cwd.trim() || !isAbsolute(cwd)) return null;
|
|
|
|
|
return normalize(cwd);
|
2026-07-09 09:20:37 +08:00
|
|
|
}
|
|
|
|
|
|
2026-07-12 00:37:32 +08:00
|
|
|
function projectSlugFromPath(projectPath: string | null): string | null {
|
2026-07-09 09:20:37 +08:00
|
|
|
const normalized = normalizeObservedCwd(projectPath);
|
|
|
|
|
if (!normalized) return null;
|
|
|
|
|
return '-' + normalized.replace(/^[\\/]+/, '').replace(/[\\/]+/g, '-');
|
|
|
|
|
}
|
|
|
|
|
|
2026-07-12 00:37:32 +08:00
|
|
|
function inferProjectPath(project: string | null | undefined, observedCwds: unknown[] = []): string | null {
|
|
|
|
|
const byPath = new Map<string, { path: string; count: number; first: number }>();
|
2026-07-09 09:20:37 +08:00
|
|
|
for (const cwd of observedCwds) {
|
|
|
|
|
const normalized = normalizeObservedCwd(cwd);
|
|
|
|
|
if (!normalized) continue;
|
|
|
|
|
const current = byPath.get(normalized) || { path: normalized, count: 0, first: byPath.size };
|
|
|
|
|
current.count++;
|
|
|
|
|
byPath.set(normalized, current);
|
|
|
|
|
}
|
|
|
|
|
const best = [...byPath.values()].sort((a, b) => b.count - a.count || a.first - b.first)[0];
|
|
|
|
|
return best?.path || legacyProjectPathFromSlug(project);
|
|
|
|
|
}
|
|
|
|
|
|
2026-08-04 11:33:01 -04:00
|
|
|
function discoverJsonlFiles(
|
|
|
|
|
projectsDir = PROJECTS_DIR,
|
|
|
|
|
reportIssue?: DiscoveryIssueHandler,
|
|
|
|
|
): ClaudeJsonlFile[] {
|
2026-07-12 00:37:32 +08:00
|
|
|
const files: ClaudeJsonlFile[] = [];
|
2026-08-04 05:49:51 +08:00
|
|
|
if (!existsSync(projectsDir)) return files;
|
2026-07-09 09:20:37 +08:00
|
|
|
let projects;
|
2026-08-04 11:33:01 -04:00
|
|
|
try { projects = readdirSync(projectsDir); } catch (error) { reportIssue?.(sourceInventoryIssue(projectsDir, error)); return files; }
|
2026-07-09 09:20:37 +08:00
|
|
|
for (const proj of projects) {
|
2026-08-04 05:49:51 +08:00
|
|
|
const projPath = join(projectsDir, proj);
|
2026-07-09 09:20:37 +08:00
|
|
|
if (!isDir(projPath)) continue;
|
|
|
|
|
let entries;
|
2026-08-04 11:33:01 -04:00
|
|
|
try { entries = readdirSync(projPath); } catch (error) { reportIssue?.(sourceInventoryIssue(projPath, error)); continue; }
|
2026-07-09 09:20:37 +08:00
|
|
|
for (const f of entries) {
|
|
|
|
|
if (f.endsWith('.jsonl'))
|
2026-08-04 05:49:51 +08:00
|
|
|
files.push({ path: join(projPath, f), sessionId: f.slice(0, -6), project: proj, isSubagent: false });
|
2026-07-09 09:20:37 +08:00
|
|
|
}
|
|
|
|
|
for (const sd of entries) {
|
2026-08-04 05:49:51 +08:00
|
|
|
const saDir = join(projPath, sd, 'subagents');
|
2026-07-09 09:20:37 +08:00
|
|
|
if (!isDir(saDir)) continue;
|
|
|
|
|
let saEntries;
|
2026-08-04 11:33:01 -04:00
|
|
|
try { saEntries = readdirSync(saDir); } catch (error) { reportIssue?.(sourceInventoryIssue(saDir, error)); continue; }
|
2026-07-09 09:20:37 +08:00
|
|
|
for (const sf of saEntries) {
|
|
|
|
|
if (sf.endsWith('.jsonl'))
|
2026-08-04 05:49:51 +08:00
|
|
|
files.push({ path: join(saDir, sf), sessionId: sd, project: proj, isSubagent: true, agentId: sf.slice(0, -6) });
|
2026-07-09 09:20:37 +08:00
|
|
|
}
|
2026-08-04 05:49:51 +08:00
|
|
|
const wfRoot = join(saDir, 'workflows');
|
2026-07-09 09:20:37 +08:00
|
|
|
if (!isDir(wfRoot)) continue;
|
|
|
|
|
let wfDirs;
|
2026-08-04 11:33:01 -04:00
|
|
|
try { wfDirs = readdirSync(wfRoot); } catch (error) { reportIssue?.(sourceInventoryIssue(wfRoot, error)); continue; }
|
2026-07-09 09:20:37 +08:00
|
|
|
for (const wfDir of wfDirs) {
|
2026-08-04 05:49:51 +08:00
|
|
|
const wfPath = join(wfRoot, wfDir);
|
2026-07-09 09:20:37 +08:00
|
|
|
if (!isDir(wfPath)) continue;
|
|
|
|
|
let wfEntries;
|
2026-08-04 11:33:01 -04:00
|
|
|
try { wfEntries = readdirSync(wfPath); } catch (error) { reportIssue?.(sourceInventoryIssue(wfPath, error)); continue; }
|
2026-07-09 09:20:37 +08:00
|
|
|
for (const wf of wfEntries) {
|
|
|
|
|
if (wf.endsWith('.jsonl'))
|
2026-08-04 05:49:51 +08:00
|
|
|
files.push({ path: join(wfPath, wf), sessionId: sd, project: proj, isSubagent: true, agentId: wf.slice(0, -6), workflowRunId: wfDir });
|
2026-07-09 09:20:37 +08:00
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
return files;
|
|
|
|
|
}
|
|
|
|
|
|
2026-08-04 11:33:01 -04:00
|
|
|
function discoverCodexJsonlFiles(
|
|
|
|
|
sessionsDir = CODEX_SESSIONS_DIR,
|
|
|
|
|
reportIssue?: DiscoveryIssueHandler,
|
|
|
|
|
): CodexJsonlFile[] {
|
2026-07-12 00:37:32 +08:00
|
|
|
const files: CodexJsonlFile[] = [];
|
2026-08-04 05:49:51 +08:00
|
|
|
if (!existsSync(sessionsDir)) return files;
|
2026-07-12 00:37:32 +08:00
|
|
|
const walk = (dir: string): void => {
|
2026-07-09 09:20:37 +08:00
|
|
|
let entries;
|
2026-08-04 11:33:01 -04:00
|
|
|
try { entries = readdirSync(dir, { withFileTypes: true }); } catch (error) { reportIssue?.(sourceInventoryIssue(dir, error)); return; }
|
2026-07-09 09:20:37 +08:00
|
|
|
for (const entry of entries) {
|
2026-08-04 05:49:51 +08:00
|
|
|
const fp = join(dir, entry.name);
|
2026-07-09 09:20:37 +08:00
|
|
|
if (entry.isDirectory()) {
|
|
|
|
|
walk(fp);
|
|
|
|
|
} else if (entry.isFile() && entry.name.endsWith('.jsonl')) {
|
|
|
|
|
files.push({ path: fp, source: 'codex' });
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
};
|
2026-07-20 22:43:23 +08:00
|
|
|
walk(sessionsDir);
|
2026-07-09 09:20:37 +08:00
|
|
|
return files;
|
|
|
|
|
}
|
|
|
|
|
|
2026-07-12 00:37:32 +08:00
|
|
|
// ---- Codex pure helpers ----
|
|
|
|
|
function codexDbId(id: unknown): string | null {
|
2026-07-09 09:20:37 +08:00
|
|
|
if (!id) return null;
|
|
|
|
|
const raw = String(id).replace(/^codex:/, '');
|
|
|
|
|
return `codex:${raw}`;
|
|
|
|
|
}
|
|
|
|
|
|
2026-07-12 00:37:32 +08:00
|
|
|
function codexRawId(id: unknown): string | null {
|
2026-07-09 09:20:37 +08:00
|
|
|
return id ? String(id).replace(/^codex:/, '') : null;
|
|
|
|
|
}
|
|
|
|
|
|
2026-07-12 00:37:32 +08:00
|
|
|
function codexLineUuid(threadId: unknown, lineNum: number): string {
|
2026-07-09 09:20:37 +08:00
|
|
|
return `codex:${codexRawId(threadId)}:${String(lineNum).padStart(6, '0')}`;
|
|
|
|
|
}
|
|
|
|
|
|
2026-07-21 00:58:34 +08:00
|
|
|
function codexCallId(threadId: unknown, callId: unknown): string | null {
|
|
|
|
|
if (!threadId || !callId) return null;
|
|
|
|
|
return `codex:${codexRawId(threadId)}:${String(callId).replace(/^codex:/, '')}`;
|
2026-07-09 09:20:37 +08:00
|
|
|
}
|
|
|
|
|
|
2026-07-12 00:37:32 +08:00
|
|
|
function codexParentThreadId(meta: JsonRecord): string | null {
|
2026-07-09 09:20:37 +08:00
|
|
|
const subagent = meta?.source?.subagent;
|
|
|
|
|
return subagent?.thread_spawn?.parent_thread_id
|
|
|
|
|
|| meta?.forked_from_id
|
|
|
|
|
|| subagent?.parent_thread_id
|
|
|
|
|
|| null;
|
|
|
|
|
}
|
|
|
|
|
|
2026-07-12 00:37:32 +08:00
|
|
|
function codexIsGuardianThread(meta: JsonRecord, records: CodexLineRecord[] = []): boolean {
|
2026-07-09 09:20:37 +08:00
|
|
|
const subagent = meta?.source?.subagent;
|
|
|
|
|
if (subagent?.other === 'guardian') return true;
|
|
|
|
|
if (meta?.thread_source !== 'subagent') return false;
|
|
|
|
|
return records.some(({ obj }) => obj?.payload?.model === 'codex-auto-review' || obj?.model === 'codex-auto-review');
|
|
|
|
|
}
|
|
|
|
|
|
2026-07-12 00:37:32 +08:00
|
|
|
function readCodexGuardianThreadInfo(filePath: string): { threadRawId: string; lineNum: number } | null {
|
|
|
|
|
const records: CodexLineRecord[] = [];
|
|
|
|
|
let metaRecord: CodexLineRecord | null = null;
|
2026-07-09 09:20:37 +08:00
|
|
|
let lineNum = 0;
|
|
|
|
|
readLines(filePath, (line) => {
|
|
|
|
|
lineNum++;
|
2026-07-12 00:37:32 +08:00
|
|
|
let obj: JsonRecord;
|
2026-07-09 09:20:37 +08:00
|
|
|
try {
|
|
|
|
|
obj = JSON.parse(line);
|
|
|
|
|
} catch {
|
|
|
|
|
return;
|
|
|
|
|
}
|
|
|
|
|
records.push({ lineNum, obj });
|
|
|
|
|
if (obj?.type === 'session_meta' && obj.payload?.id) {
|
|
|
|
|
metaRecord = { lineNum, obj };
|
|
|
|
|
if (obj.payload?.source?.subagent?.other === 'guardian') return false;
|
|
|
|
|
if (obj.payload?.thread_source !== 'subagent') return false;
|
|
|
|
|
}
|
|
|
|
|
if (metaRecord && codexIsGuardianThread(metaRecord.obj.payload, records)) return false;
|
|
|
|
|
});
|
2026-07-12 00:37:32 +08:00
|
|
|
const capturedMeta = metaRecord as CodexLineRecord | null;
|
|
|
|
|
const meta = capturedMeta?.obj?.payload;
|
2026-07-09 09:20:37 +08:00
|
|
|
if (!meta || !codexIsGuardianThread(meta, records)) return null;
|
2026-07-12 00:37:32 +08:00
|
|
|
const threadRawId = codexRawId(meta.id);
|
|
|
|
|
return threadRawId ? { threadRawId, lineNum } : null;
|
2026-07-09 09:20:37 +08:00
|
|
|
}
|
|
|
|
|
|
2026-07-12 00:37:32 +08:00
|
|
|
function codexAgentNickname(meta: JsonRecord): string | null {
|
2026-07-09 09:20:37 +08:00
|
|
|
return meta?.agent_nickname
|
|
|
|
|
|| meta?.source?.subagent?.thread_spawn?.agent_nickname
|
|
|
|
|
|| null;
|
|
|
|
|
}
|
|
|
|
|
|
2026-07-12 00:37:32 +08:00
|
|
|
function codexAgentRole(meta: JsonRecord): string | null {
|
2026-07-09 09:20:37 +08:00
|
|
|
return meta?.agent_role
|
|
|
|
|
|| meta?.source?.subagent?.thread_spawn?.agent_role
|
|
|
|
|
|| null;
|
|
|
|
|
}
|
|
|
|
|
|
2026-07-12 00:37:32 +08:00
|
|
|
function parseCodexJsonInput(value: JsonValue): JsonValue {
|
2026-07-09 09:20:37 +08:00
|
|
|
if (value === null || value === undefined || value === '') return {};
|
|
|
|
|
if (typeof value !== 'string') return value;
|
|
|
|
|
try { return JSON.parse(value); } catch { return value; }
|
|
|
|
|
}
|
|
|
|
|
|
2026-07-22 01:02:25 +08:00
|
|
|
interface CodexUsage {
|
|
|
|
|
inputTokens: number | null;
|
|
|
|
|
outputTokens: number | null;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
function codexUsage(payload: JsonRecord): CodexUsage {
|
2026-07-09 09:20:37 +08:00
|
|
|
const usage = payload?.info?.last_token_usage || payload?.info?.total_token_usage || payload?.last_token_usage || null;
|
2026-07-22 01:02:25 +08:00
|
|
|
if (!usage) return { inputTokens: null, outputTokens: null };
|
2026-07-09 09:20:37 +08:00
|
|
|
return {
|
2026-07-22 01:02:25 +08:00
|
|
|
inputTokens: typeof usage.input_tokens === 'number' && Number.isFinite(usage.input_tokens)
|
|
|
|
|
? usage.input_tokens
|
|
|
|
|
: null,
|
|
|
|
|
outputTokens: typeof usage.output_tokens === 'number' && Number.isFinite(usage.output_tokens)
|
|
|
|
|
? usage.output_tokens
|
|
|
|
|
: null,
|
2026-07-09 09:20:37 +08:00
|
|
|
};
|
|
|
|
|
}
|
|
|
|
|
|
2026-07-12 00:37:32 +08:00
|
|
|
function codexEventText(payload: JsonRecord): string | null {
|
2026-07-09 09:20:37 +08:00
|
|
|
if (typeof payload?.message === 'string') return payload.message;
|
|
|
|
|
if (Array.isArray(payload?.text_elements) && payload.text_elements.length) {
|
2026-07-12 00:37:32 +08:00
|
|
|
const parts = payload.text_elements.map((item: JsonValue) => typeof item === 'string' ? item : item?.text).filter(Boolean);
|
2026-07-09 09:20:37 +08:00
|
|
|
if (parts.length) return parts.join('\n');
|
|
|
|
|
}
|
|
|
|
|
if (typeof payload?.text === 'string') return payload.text;
|
|
|
|
|
return null;
|
|
|
|
|
}
|
|
|
|
|
|
2026-07-12 00:37:32 +08:00
|
|
|
function codexMessagePayloadText(payload: JsonRecord): string | null {
|
2026-07-09 09:20:37 +08:00
|
|
|
if (!Array.isArray(payload?.content)) return null;
|
2026-07-12 00:37:32 +08:00
|
|
|
const parts: string[] = [];
|
2026-07-21 00:58:34 +08:00
|
|
|
for (let index = 0; index < payload.content.length; index++) {
|
|
|
|
|
const block = payload.content[index];
|
|
|
|
|
const image = payload.content[index + 1];
|
|
|
|
|
const close = payload.content[index + 2];
|
|
|
|
|
if (
|
|
|
|
|
block?.type === 'input_text'
|
|
|
|
|
&& typeof block.text === 'string'
|
|
|
|
|
&& block.text.trim() === '<image>'
|
|
|
|
|
&& image?.type === 'input_image'
|
|
|
|
|
&& close?.type === 'input_text'
|
|
|
|
|
&& typeof close.text === 'string'
|
|
|
|
|
&& close.text.trim() === '</image>'
|
|
|
|
|
) {
|
|
|
|
|
index += 2;
|
|
|
|
|
continue;
|
|
|
|
|
}
|
2026-07-09 09:20:37 +08:00
|
|
|
if (typeof block?.text === 'string') parts.push(block.text);
|
|
|
|
|
}
|
|
|
|
|
return parts.length ? parts.join('\n') : null;
|
|
|
|
|
}
|
|
|
|
|
|
2026-07-12 00:37:32 +08:00
|
|
|
function codexVisibleMessageKey(role: unknown, text: unknown): string {
|
2026-07-09 09:20:37 +08:00
|
|
|
return `${role || ''}\u0000${text || ''}`;
|
|
|
|
|
}
|
|
|
|
|
|
2026-07-12 00:37:32 +08:00
|
|
|
function codexToolInput(payload: JsonRecord): JsonValue {
|
2026-07-09 09:20:37 +08:00
|
|
|
if (payload?.type === 'custom_tool_call') return parseCodexJsonInput(payload.input);
|
|
|
|
|
if (payload?.type === 'tool_search_call') return parseCodexJsonInput(payload.arguments);
|
|
|
|
|
if (payload?.type === 'web_search_call') return { action: payload.action || null };
|
|
|
|
|
return parseCodexJsonInput(payload?.arguments);
|
|
|
|
|
}
|
|
|
|
|
|
2026-07-12 00:37:32 +08:00
|
|
|
function codexToolOutput(payload: JsonRecord): string | null {
|
2026-07-09 09:20:37 +08:00
|
|
|
if (typeof payload?.output === 'string') return payload.output;
|
|
|
|
|
if (payload?.output !== undefined) return JSON.stringify(payload.output);
|
|
|
|
|
if (payload?.tools !== undefined) return JSON.stringify(payload.tools);
|
|
|
|
|
if (payload?.execution !== undefined) return JSON.stringify(payload.execution);
|
|
|
|
|
return null;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
export {
|
2026-08-04 05:49:51 +08:00
|
|
|
CLAUDE_DIR, CODEX_DIR, PROJECTS_DIR, CODEX_SESSIONS_DIR, TEXT_LIMIT,
|
2026-07-21 00:58:34 +08:00
|
|
|
trunc, truncJson, extractText, extractContentType, extractMessageIsMeta, isSkillInstructions, filePath, isDir, readLines,
|
2026-07-09 09:20:37 +08:00
|
|
|
legacyProjectPathFromSlug, normalizeObservedCwd, projectSlugFromPath, inferProjectPath,
|
2026-08-04 11:33:01 -04:00
|
|
|
discoverJsonlFiles, discoverCodexJsonlFiles, sourceInventoryIssue,
|
2026-07-09 09:20:37 +08:00
|
|
|
codexDbId, codexRawId, codexLineUuid, codexCallId, codexParentThreadId, codexIsGuardianThread,
|
|
|
|
|
readCodexGuardianThreadInfo, codexAgentNickname, codexAgentRole, parseCodexJsonInput,
|
|
|
|
|
codexUsage, codexEventText, codexMessagePayloadText, codexVisibleMessageKey, codexToolInput, codexToolOutput,
|
|
|
|
|
};
|