feat: add concise codex trace inspection
This commit is contained in:
+689
-15
@@ -1,10 +1,13 @@
|
||||
import { closeSync, copyFileSync, existsSync, lstatSync, mkdirSync, openSync, readdirSync, readSync, realpathSync, statSync, writeFileSync } from "node:fs";
|
||||
import { closeSync, copyFileSync, createReadStream, existsSync, lstatSync, mkdirSync, openSync, readdirSync, readFileSync, readlinkSync, readSync, realpathSync, statSync, writeFileSync } from "node:fs";
|
||||
import { homedir } from "node:os";
|
||||
import path from "node:path";
|
||||
import { createInterface } from "node:readline";
|
||||
import { repoRoot } from "./config";
|
||||
import type { RenderedCliResult } from "./output";
|
||||
|
||||
type CodexTraceAction = "help" | "list" | "collect" | "show";
|
||||
type CodexTraceAction = "help" | "list" | "collect" | "show" | "grep" | "active";
|
||||
type CodexTraceKind = "session-jsonl" | "history-jsonl" | "shell-snapshot" | "log" | "trace-named" | "jsonl" | "sqlite-db" | "sqlite-sidecar" | "unrecognized";
|
||||
type CodexTraceOutput = "text" | "wide" | "name" | "json" | "yaml";
|
||||
|
||||
interface CodexTraceOptions {
|
||||
action: CodexTraceAction;
|
||||
@@ -21,6 +24,13 @@ interface CodexTraceOptions {
|
||||
dryRun: boolean;
|
||||
full: boolean;
|
||||
allowOutsideRoot: boolean;
|
||||
pattern: string | null;
|
||||
fixed: boolean;
|
||||
includeSystem: boolean;
|
||||
contextChars: number;
|
||||
since: string | null;
|
||||
fileLimit: number;
|
||||
output: CodexTraceOutput;
|
||||
}
|
||||
|
||||
interface CodexTraceCandidate {
|
||||
@@ -41,21 +51,28 @@ const defaultMaxFileBytes = 25 * 1024 * 1024;
|
||||
const maxFileBytesLimit = 512 * 1024 * 1024;
|
||||
const defaultTailBytes = 12_000;
|
||||
const maxTailBytes = 1_000_000;
|
||||
const defaultContextChars = 700;
|
||||
const maxContextChars = 4_000;
|
||||
const defaultFileLimit = 8;
|
||||
const maxFileLimit = 50;
|
||||
|
||||
export function codexTraceHelp(): Record<string, unknown> {
|
||||
return {
|
||||
ok: true,
|
||||
command: "codex trace",
|
||||
output: "json",
|
||||
output: "text",
|
||||
description: "Collect bounded local Codex trace artifacts. Defaults to ~/.codex and can scan another root with --root <dir>.",
|
||||
usage: [
|
||||
"bun scripts/cli.ts codex trace list [--root ~/.codex] [--limit 30]",
|
||||
"bun scripts/cli.ts codex trace active [--root ~/.codex]",
|
||||
"bun scripts/cli.ts codex trace grep --pattern 'playwright|auth-login-failed' [--file sessions/...jsonl] [--since ISO]",
|
||||
"bun scripts/cli.ts codex trace collect [--root ~/.codex] [--output .state/codex-trace/<timestamp>] [--limit 30]",
|
||||
"bun scripts/cli.ts codex trace show --file sessions/2026/06/16/<session>.jsonl [--root ~/.codex] [--tail-bytes 12000]",
|
||||
],
|
||||
safety: [
|
||||
"Default scan includes sessions/*.jsonl, history.jsonl, shell_snapshots, *.log, and trace-named text files.",
|
||||
"auth.json, config.toml, cache/.tmp/generated_images, and sqlite files are skipped by default.",
|
||||
"grep parses JSONL records, skips bootstrap/system payloads by default, and returns bounded match-centered summaries instead of whole lines.",
|
||||
"Use --include-sqlite or --include-sensitive only for reviewed local diagnostics; values are not printed unless show reads a text file.",
|
||||
"collect copies bounded files and writes a manifest; it does not compress, upload, or delete source files.",
|
||||
],
|
||||
@@ -70,16 +87,25 @@ export function codexTraceHelp(): Record<string, unknown> {
|
||||
"--include-cache": "Traverse cache, .tmp, generated_images, plugins, and skills directories. Off by default.",
|
||||
"--dry-run": "For collect, report the plan without copying files.",
|
||||
"--file <path>": "For show, file path relative to --root or an absolute path under --root.",
|
||||
"--pattern <regex>": "For grep, pattern to match against structured event text.",
|
||||
"--fixed": "For grep, treat --pattern as a literal string.",
|
||||
"--since <ISO>": "For grep, skip JSONL events before the timestamp string.",
|
||||
"--context-chars <n>": `For grep, max summary characters per match, default ${defaultContextChars}, max ${maxContextChars}.`,
|
||||
"--file-limit <n>": `For grep without --file, max target files, default ${defaultFileLimit}, max ${maxFileLimit}.`,
|
||||
"--include-system": "For grep, include session_meta/system instruction records. Off by default.",
|
||||
"-o json|yaml|name|wide": "Output mode. Default is concise text/table.",
|
||||
"--tail-bytes <n>": `For show, tail bytes to return, default ${defaultTailBytes}, max ${maxTailBytes}.`,
|
||||
"--full": "For show, read from the beginning up to --max-file-bytes instead of tailing.",
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
export function runCodexTraceCommand(args: string[]): Record<string, unknown> {
|
||||
export async function runCodexTraceCommand(args: string[]): Promise<Record<string, unknown> | RenderedCliResult> {
|
||||
const options = parseCodexTraceOptions(args);
|
||||
if (options.action === "help") return codexTraceHelp();
|
||||
if (options.action === "help") return renderCodexTraceResult(options, "codex trace help", codexTraceHelp());
|
||||
if (options.action === "active") return codexTraceActive(options);
|
||||
if (options.action === "show") return codexTraceShow(options);
|
||||
if (options.action === "grep") return codexTraceGrep(options);
|
||||
const candidates = scanCodexTraceRoot(options);
|
||||
if (options.action === "list") return codexTraceList(options, candidates);
|
||||
return codexTraceCollect(options, candidates);
|
||||
@@ -89,12 +115,12 @@ function parseCodexTraceOptions(args: string[]): CodexTraceOptions {
|
||||
const rawAction = args[0];
|
||||
const action: CodexTraceAction =
|
||||
rawAction === undefined || rawAction === "help" || rawAction === "--help" || rawAction === "-h" ? "help"
|
||||
: rawAction === "list" || rawAction === "collect" || rawAction === "show" ? rawAction
|
||||
: (() => { throw new Error(`codex trace action must be one of: list, collect, show, help; got ${rawAction}`); })();
|
||||
: rawAction === "list" || rawAction === "collect" || rawAction === "show" || rawAction === "grep" || rawAction === "active" ? rawAction
|
||||
: (() => { throw new Error(`codex trace action must be one of: list, active, grep, collect, show, help; got ${rawAction}`); })();
|
||||
const optionArgs = action === "help" && rawAction === undefined ? args : args.slice(1);
|
||||
assertKnownOptions(optionArgs, {
|
||||
flags: ["--dry-run", "--full", "--include-sqlite", "--include-sensitive", "--include-cache", "--allow-outside-root", "--help", "-h"],
|
||||
values: ["--root", "--output", "--file", "--limit", "--max-depth", "--max-file-bytes", "--tail-bytes"],
|
||||
flags: ["--dry-run", "--full", "--include-sqlite", "--include-sensitive", "--include-cache", "--allow-outside-root", "--include-system", "--fixed", "--wide", "--help", "-h"],
|
||||
values: ["--root", "--output", "--file", "--limit", "--max-depth", "--max-file-bytes", "--tail-bytes", "--pattern", "--since", "--context-chars", "--file-limit", "-o", "--format"],
|
||||
});
|
||||
if (optionArgs.includes("--help") || optionArgs.includes("-h")) {
|
||||
return {
|
||||
@@ -112,9 +138,18 @@ function parseCodexTraceOptions(args: string[]): CodexTraceOptions {
|
||||
dryRun: false,
|
||||
full: false,
|
||||
allowOutsideRoot: false,
|
||||
pattern: null,
|
||||
fixed: false,
|
||||
includeSystem: false,
|
||||
contextChars: defaultContextChars,
|
||||
since: null,
|
||||
fileLimit: defaultFileLimit,
|
||||
output: "text",
|
||||
};
|
||||
}
|
||||
const root = resolveUserPath(optionValue(optionArgs, "--root") ?? defaultCodexRoot());
|
||||
const positionalPattern = action === "grep" && optionArgs[0] !== undefined && !optionArgs[0].startsWith("-") ? optionArgs[0] : null;
|
||||
const output = parseOutputMode(optionValue(optionArgs, "-o") ?? optionValue(optionArgs, "--format"), hasFlag(optionArgs, "--wide"));
|
||||
return {
|
||||
action,
|
||||
root,
|
||||
@@ -130,12 +165,19 @@ function parseCodexTraceOptions(args: string[]): CodexTraceOptions {
|
||||
dryRun: hasFlag(optionArgs, "--dry-run"),
|
||||
full: hasFlag(optionArgs, "--full"),
|
||||
allowOutsideRoot: hasFlag(optionArgs, "--allow-outside-root"),
|
||||
pattern: optionValue(optionArgs, "--pattern") ?? positionalPattern,
|
||||
fixed: hasFlag(optionArgs, "--fixed"),
|
||||
includeSystem: hasFlag(optionArgs, "--include-system"),
|
||||
contextChars: positiveIntegerOption(optionArgs, "--context-chars", defaultContextChars, maxContextChars),
|
||||
since: optionValue(optionArgs, "--since") ?? null,
|
||||
fileLimit: positiveIntegerOption(optionArgs, "--file-limit", defaultFileLimit, maxFileLimit),
|
||||
output,
|
||||
};
|
||||
}
|
||||
|
||||
function codexTraceList(options: CodexTraceOptions, candidates: CodexTraceCandidate[]): Record<string, unknown> {
|
||||
function codexTraceList(options: CodexTraceOptions, candidates: CodexTraceCandidate[]): Record<string, unknown> | RenderedCliResult {
|
||||
const selected = selectedCandidates(options, candidates);
|
||||
return {
|
||||
const raw = {
|
||||
ok: true,
|
||||
command: "codex trace list",
|
||||
root: options.root,
|
||||
@@ -151,9 +193,10 @@ function codexTraceList(options: CodexTraceOptions, candidates: CodexTraceCandid
|
||||
},
|
||||
valuesPrinted: false,
|
||||
};
|
||||
return renderCodexTraceResult(options, "codex trace list", raw);
|
||||
}
|
||||
|
||||
function codexTraceCollect(options: CodexTraceOptions, candidates: CodexTraceCandidate[]): Record<string, unknown> {
|
||||
function codexTraceCollect(options: CodexTraceOptions, candidates: CodexTraceCandidate[]): Record<string, unknown> | RenderedCliResult {
|
||||
const selected = selectedCandidates(options, candidates);
|
||||
const outputDir = options.outputDir ?? path.join(repoRoot, ".state", "codex-trace", timestampForPath());
|
||||
const copied: Record<string, unknown>[] = [];
|
||||
@@ -185,7 +228,7 @@ function codexTraceCollect(options: CodexTraceOptions, candidates: CodexTraceCan
|
||||
valuesPrinted: false,
|
||||
};
|
||||
if (!options.dryRun) writeFileSync(path.join(outputDir, "manifest.json"), JSON.stringify(manifest, null, 2) + "\n", { mode: 0o600 });
|
||||
return {
|
||||
const raw = {
|
||||
ok: true,
|
||||
command: "codex trace collect",
|
||||
root: options.root,
|
||||
@@ -199,9 +242,10 @@ function codexTraceCollect(options: CodexTraceOptions, candidates: CodexTraceCan
|
||||
skipped: skippedSummary(candidates),
|
||||
valuesPrinted: false,
|
||||
};
|
||||
return renderCodexTraceResult(options, "codex trace collect", raw);
|
||||
}
|
||||
|
||||
function codexTraceShow(options: CodexTraceOptions): Record<string, unknown> {
|
||||
function codexTraceShow(options: CodexTraceOptions): Record<string, unknown> | RenderedCliResult {
|
||||
if (options.file === null) throw new Error("codex trace show requires --file <path>");
|
||||
const filePath = resolveTraceFile(options.root, options.file);
|
||||
const rootReal = safeRealpath(options.root);
|
||||
@@ -222,7 +266,7 @@ function codexTraceShow(options: CodexTraceOptions): Record<string, unknown> {
|
||||
const readBytes = options.full ? Math.min(stats.size, options.maxFileBytes) : Math.min(stats.size, options.tailBytes);
|
||||
const start = options.full ? 0 : Math.max(0, stats.size - readBytes);
|
||||
const content = textReadable ? redactTraceText(readFileSlice(fileReal, start, readBytes)) : null;
|
||||
return {
|
||||
const raw = {
|
||||
ok: true,
|
||||
command: "codex trace show",
|
||||
root: options.root,
|
||||
@@ -243,6 +287,213 @@ function codexTraceShow(options: CodexTraceOptions): Record<string, unknown> {
|
||||
content,
|
||||
valuesPrinted: content !== null,
|
||||
};
|
||||
return renderCodexTraceResult(options, "codex trace show", raw);
|
||||
}
|
||||
|
||||
function codexTraceActive(options: CodexTraceOptions): Record<string, unknown> | RenderedCliResult {
|
||||
const rootReal = safeRealpath(options.root);
|
||||
const procDir = "/proc";
|
||||
if (rootReal === null) {
|
||||
const raw = {
|
||||
ok: true,
|
||||
command: "codex trace active",
|
||||
root: options.root,
|
||||
rootPresent: false,
|
||||
supported: existsSync(procDir),
|
||||
sessions: [],
|
||||
valuesPrinted: false,
|
||||
};
|
||||
return renderCodexTraceResult(options, "codex trace active", raw);
|
||||
}
|
||||
const sessions = activeCodexSessionFiles(rootReal).slice(0, options.limit);
|
||||
const raw = {
|
||||
ok: true,
|
||||
command: "codex trace active",
|
||||
root: options.root,
|
||||
rootPresent: true,
|
||||
supported: existsSync(procDir),
|
||||
count: sessions.length,
|
||||
sessions,
|
||||
next: {
|
||||
grep: "bun scripts/cli.ts codex trace grep --pattern <regex> --file <relative-path>",
|
||||
collect: "bun scripts/cli.ts codex trace collect --limit 30",
|
||||
},
|
||||
valuesPrinted: false,
|
||||
};
|
||||
return renderCodexTraceResult(options, "codex trace active", raw);
|
||||
}
|
||||
|
||||
async function codexTraceGrep(options: CodexTraceOptions): Promise<Record<string, unknown> | RenderedCliResult> {
|
||||
if (options.pattern === null || options.pattern.length === 0) throw new Error("codex trace grep requires --pattern <regex> or a positional pattern");
|
||||
const matcher = makeMatcher(options.pattern, options.fixed);
|
||||
const rootReal = safeRealpath(options.root);
|
||||
if (rootReal === null) throw new Error(`codex trace root does not exist: ${options.root}`);
|
||||
const files = grepTargetFiles(options, rootReal);
|
||||
const results: Record<string, unknown>[] = [];
|
||||
let linesScanned = 0;
|
||||
let matchedTotal = 0;
|
||||
for (const file of files) {
|
||||
const report = await grepJsonlFile(file, rootReal, matcher, options, results, options.limit);
|
||||
linesScanned += report.linesScanned;
|
||||
matchedTotal += report.matchedTotal;
|
||||
if (results.length >= options.limit) break;
|
||||
}
|
||||
const raw = {
|
||||
ok: true,
|
||||
command: "codex trace grep",
|
||||
root: options.root,
|
||||
pattern: options.pattern,
|
||||
fixed: options.fixed,
|
||||
since: options.since,
|
||||
policy: {
|
||||
limit: options.limit,
|
||||
fileLimit: options.fileLimit,
|
||||
contextChars: options.contextChars,
|
||||
includeSystem: options.includeSystem,
|
||||
valuesRedacted: true,
|
||||
},
|
||||
filesScanned: files.length,
|
||||
linesScanned,
|
||||
matchedTotal,
|
||||
matchesReturned: results.length,
|
||||
truncated: matchedTotal > results.length,
|
||||
files: files.map((file) => file.relativePath),
|
||||
results,
|
||||
valuesPrinted: results.length > 0,
|
||||
};
|
||||
return renderCodexTraceResult(options, "codex trace grep", raw);
|
||||
}
|
||||
|
||||
function renderCodexTraceResult(options: CodexTraceOptions, command: string, raw: Record<string, unknown>): Record<string, unknown> | RenderedCliResult {
|
||||
const ok = raw.ok !== false;
|
||||
if (options.output === "json") return renderedCliResult(ok, command, `${JSON.stringify(raw, null, 2)}\n`, "application/json");
|
||||
if (options.output === "yaml") return renderedCliResult(ok, command, `${Bun.YAML.stringify(raw)}\n`, "application/yaml");
|
||||
if (options.output === "name") return renderedCliResult(ok, command, renderCodexTraceNames(raw));
|
||||
if (command === "codex trace help" || raw.command === "codex trace") return renderedCliResult(ok, command, renderCodexTraceHelp(raw));
|
||||
if (raw.command === "codex trace active") return renderedCliResult(ok, command, renderCodexTraceActive(raw, options));
|
||||
if (raw.command === "codex trace grep") return renderedCliResult(ok, command, renderCodexTraceGrep(raw, options));
|
||||
if (raw.command === "codex trace list") return renderedCliResult(ok, command, renderCodexTraceList(raw, options));
|
||||
if (raw.command === "codex trace collect") return renderedCliResult(ok, command, renderCodexTraceCollect(raw, options));
|
||||
if (raw.command === "codex trace show") return renderedCliResult(ok, command, renderCodexTraceShow(raw));
|
||||
return raw;
|
||||
}
|
||||
|
||||
function renderedCliResult(ok: boolean, command: string, renderedText: string, contentType: RenderedCliResult["contentType"] = "text/plain"): RenderedCliResult {
|
||||
return { ok, command, renderedText: renderedText.endsWith("\n") ? renderedText : `${renderedText}\n`, contentType };
|
||||
}
|
||||
|
||||
function renderCodexTraceHelp(raw: Record<string, unknown>): string {
|
||||
const usage = stringArray(raw.usage);
|
||||
const safety = stringArray(raw.safety);
|
||||
const options = asRecord(raw.options) ?? {};
|
||||
const lines = [
|
||||
"Codex trace",
|
||||
String(raw.description ?? ""),
|
||||
"",
|
||||
"Usage:",
|
||||
...usage.map((line) => ` ${line}`),
|
||||
"",
|
||||
"Safety:",
|
||||
...safety.map((line) => ` - ${line}`),
|
||||
"",
|
||||
"Options:",
|
||||
...Object.entries(options).map(([key, value]) => ` ${key.padEnd(24)} ${String(value)}`),
|
||||
];
|
||||
return lines.filter((line, index) => line.length > 0 || lines[index - 1]?.length !== 0).join("\n");
|
||||
}
|
||||
|
||||
function renderCodexTraceActive(raw: Record<string, unknown>, options: CodexTraceOptions): string {
|
||||
const sessions = recordArray(raw.sessions);
|
||||
if (sessions.length === 0) return "No active Codex session files found.";
|
||||
const rows = sessions.map((session) => [
|
||||
String(session.pid ?? "-"),
|
||||
String(session.fd ?? "-"),
|
||||
ageFromIso(stringValue(session.mtime)),
|
||||
formatBytes(numberValue(session.bytes)),
|
||||
truncateMiddle(String(session.path ?? "-"), options.output === "wide" ? 140 : 82),
|
||||
]);
|
||||
return [
|
||||
renderTable(["PID", "FD", "AGE", "SIZE", "SESSION"], rows),
|
||||
"",
|
||||
"Next:",
|
||||
" bun scripts/cli.ts codex trace grep --file <session> --pattern <regex>",
|
||||
].join("\n");
|
||||
}
|
||||
|
||||
function renderCodexTraceGrep(raw: Record<string, unknown>, options: CodexTraceOptions): string {
|
||||
const results = recordArray(raw.results);
|
||||
const header = `Matched ${String(raw.matchesReturned ?? 0)}/${String(raw.matchedTotal ?? 0)} events in ${String(raw.filesScanned ?? 0)} file(s), scanned ${String(raw.linesScanned ?? 0)} lines${raw.truncated === true ? " (truncated)" : ""}.`;
|
||||
if (results.length === 0) return `${header}\nNo matching trace events found.`;
|
||||
const rows = results.map((result) => [
|
||||
shortIso(stringValue(result.timestamp)),
|
||||
`${truncateMiddle(String(result.file ?? "-"), options.output === "wide" ? 96 : 42)}:${String(result.line ?? "-")}`,
|
||||
truncateMiddle(String(result.item ?? result.type ?? "-"), 24),
|
||||
signalLabel(asRecord(result.signals)),
|
||||
truncateOneLine(String(result.summary ?? ""), options.output === "wide" ? 220 : 96),
|
||||
]);
|
||||
return [
|
||||
header,
|
||||
renderTable(["TIME", "LOCATION", "ITEM", "SIGNALS", "SUMMARY"], rows),
|
||||
].join("\n");
|
||||
}
|
||||
|
||||
function renderCodexTraceList(raw: Record<string, unknown>, options: CodexTraceOptions): string {
|
||||
const files = recordArray(raw.files);
|
||||
if (files.length === 0) return `No trace files found under ${String(raw.root ?? "-")}.`;
|
||||
const rows = files.map((file) => [
|
||||
truncateMiddle(String(file.path ?? "-"), options.output === "wide" ? 140 : 82),
|
||||
String(file.kind ?? "-"),
|
||||
formatBytes(numberValue(file.bytes)),
|
||||
ageFromIso(stringValue(file.mtime)),
|
||||
]);
|
||||
const skipped = asRecord(raw.skipped) ?? {};
|
||||
const skippedText = Object.entries(skipped).map(([key, value]) => `${key}=${String(value)}`).join(" ");
|
||||
return [
|
||||
`Root: ${String(raw.root ?? "-")}`,
|
||||
renderTable(["PATH", "KIND", "SIZE", "AGE"], rows),
|
||||
skippedText.length > 0 ? `Skipped: ${skippedText}` : "",
|
||||
Number(raw.omitted ?? 0) > 0 ? `Omitted: ${String(raw.omitted)}` : "",
|
||||
].filter((line) => line.length > 0).join("\n");
|
||||
}
|
||||
|
||||
function renderCodexTraceCollect(raw: Record<string, unknown>, options: CodexTraceOptions): string {
|
||||
const files = recordArray(raw.files);
|
||||
const headline = raw.dryRun === true ? "Dry-run collect" : "Collected";
|
||||
const rows = files.map((file) => [
|
||||
truncateMiddle(String(file.source ?? "-"), options.output === "wide" ? 120 : 72),
|
||||
String(file.kind ?? "-"),
|
||||
formatBytes(numberValue(file.bytes)),
|
||||
]);
|
||||
return [
|
||||
`${headline}: ${String(raw.copiedCount ?? 0)} file(s), ${formatBytes(numberValue(raw.copiedBytes))}`,
|
||||
`Output: ${String(raw.outputDir ?? "-")}`,
|
||||
raw.manifest === null || raw.manifest === undefined ? "" : `Manifest: ${String(raw.manifest)}`,
|
||||
rows.length > 0 ? renderTable(["SOURCE", "KIND", "SIZE"], rows) : "",
|
||||
].filter((line) => line.length > 0).join("\n");
|
||||
}
|
||||
|
||||
function renderCodexTraceShow(raw: Record<string, unknown>): string {
|
||||
const policy = asRecord(raw.contentPolicy) ?? {};
|
||||
const content = stringValue(raw.content);
|
||||
const lines = [
|
||||
`Name: ${String(raw.relativePath ?? raw.file ?? "-")}`,
|
||||
`Kind: ${String(raw.kind ?? "-")}`,
|
||||
`Size: ${formatBytes(numberValue(raw.bytes))}`,
|
||||
`Mode: ${String(policy.mode ?? "-")} bytes=${String(policy.bytesReturned ?? 0)} truncated=${String(policy.truncated ?? false)}`,
|
||||
];
|
||||
if (content !== null) lines.push("---", content);
|
||||
else lines.push("Content: <not text-readable>");
|
||||
return lines.join("\n");
|
||||
}
|
||||
|
||||
function renderCodexTraceNames(raw: Record<string, unknown>): string {
|
||||
const sessions = recordArray(raw.sessions);
|
||||
if (sessions.length > 0) return sessions.map((session) => `session/${String(session.path ?? "")}`).join("\n");
|
||||
const files = recordArray(raw.files);
|
||||
if (files.length > 0) return files.map((file) => String(file.path ?? file.source ?? file.relativePath ?? "")).filter((line) => line.length > 0).join("\n");
|
||||
const results = recordArray(raw.results);
|
||||
if (results.length > 0) return results.map((result) => `${String(result.file ?? "")}:${String(result.line ?? "")}`).join("\n");
|
||||
return "";
|
||||
}
|
||||
|
||||
function scanCodexTraceRoot(options: CodexTraceOptions): CodexTraceCandidate[] {
|
||||
@@ -291,6 +542,175 @@ function scanCodexTraceRoot(options: CodexTraceOptions): CodexTraceCandidate[] {
|
||||
return candidates.sort((left, right) => right.mtimeMs - left.mtimeMs || left.relativePath.localeCompare(right.relativePath));
|
||||
}
|
||||
|
||||
function activeCodexSessionFiles(rootReal: string): Record<string, unknown>[] {
|
||||
const procDir = "/proc";
|
||||
if (!existsSync(procDir)) return [];
|
||||
const rows = new Map<string, Record<string, unknown>>();
|
||||
let procEntries;
|
||||
try {
|
||||
procEntries = readdirSync(procDir, { withFileTypes: true });
|
||||
} catch {
|
||||
return [];
|
||||
}
|
||||
for (const procEntry of procEntries) {
|
||||
if (!procEntry.isDirectory() || !/^\d+$/u.test(procEntry.name)) continue;
|
||||
const pid = Number(procEntry.name);
|
||||
const fdDir = path.join(procDir, procEntry.name, "fd");
|
||||
let fdEntries;
|
||||
try {
|
||||
fdEntries = readdirSync(fdDir, { withFileTypes: true });
|
||||
} catch {
|
||||
continue;
|
||||
}
|
||||
const cmdline = readProcCmdline(pid);
|
||||
if (!isCodexSessionProcess(cmdline)) continue;
|
||||
for (const fdEntry of fdEntries) {
|
||||
let target;
|
||||
try {
|
||||
target = readlinkSync(path.join(fdDir, fdEntry.name));
|
||||
} catch {
|
||||
continue;
|
||||
}
|
||||
const deletedSuffix = " (deleted)";
|
||||
if (target.endsWith(deletedSuffix)) target = target.slice(0, -deletedSuffix.length);
|
||||
if (!target.endsWith(".jsonl") || !isWithin(rootReal, target) || !normalizePath(path.relative(rootReal, target)).startsWith("sessions/")) continue;
|
||||
const realTarget = safeRealpath(target);
|
||||
if (realTarget === null || !isWithin(rootReal, realTarget)) continue;
|
||||
let stats;
|
||||
try {
|
||||
stats = statSync(realTarget);
|
||||
} catch {
|
||||
continue;
|
||||
}
|
||||
const relativePath = path.relative(rootReal, realTarget);
|
||||
const key = `${pid}:${realTarget}`;
|
||||
rows.set(key, {
|
||||
pid,
|
||||
fd: fdEntry.name,
|
||||
path: relativePath,
|
||||
bytes: stats.size,
|
||||
mtime: stats.mtime.toISOString(),
|
||||
command: truncateOneLine(cmdline, 180),
|
||||
grepCommand: `bun scripts/cli.ts codex trace grep --file ${shellWord(relativePath)} --pattern <regex>`,
|
||||
showCommand: `bun scripts/cli.ts codex trace show --file ${shellWord(relativePath)}`,
|
||||
});
|
||||
}
|
||||
}
|
||||
return [...rows.values()].sort((left, right) => String(right.mtime).localeCompare(String(left.mtime)) || Number(left.pid) - Number(right.pid));
|
||||
}
|
||||
|
||||
function grepTargetFiles(options: CodexTraceOptions, rootReal: string): CodexTraceCandidate[] {
|
||||
if (options.file !== null) {
|
||||
const filePath = resolveTraceFile(options.root, options.file);
|
||||
const fileReal = safeRealpath(filePath);
|
||||
if (fileReal === null) throw new Error(`codex trace grep file does not exist: ${filePath}`);
|
||||
if (!options.allowOutsideRoot && !isWithin(rootReal, fileReal)) throw new Error("codex trace grep refuses file outside --root");
|
||||
const stats = statSync(fileReal);
|
||||
const relativePath = path.relative(rootReal, fileReal);
|
||||
return [{
|
||||
path: fileReal,
|
||||
relativePath,
|
||||
kind: classifyTraceKind(relativePath, path.basename(fileReal)),
|
||||
bytes: stats.size,
|
||||
mtimeMs: stats.mtimeMs,
|
||||
included: true,
|
||||
skippedReason: null,
|
||||
}];
|
||||
}
|
||||
const active = activeCodexSessionFiles(rootReal)
|
||||
.map((row) => String(row.path))
|
||||
.filter((relativePath) => relativePath.length > 0)
|
||||
.map((relativePath) => {
|
||||
const filePath = path.join(rootReal, relativePath);
|
||||
const stats = statSync(filePath);
|
||||
return {
|
||||
path: filePath,
|
||||
relativePath,
|
||||
kind: classifyTraceKind(relativePath, path.basename(filePath)),
|
||||
bytes: stats.size,
|
||||
mtimeMs: stats.mtimeMs,
|
||||
included: true,
|
||||
skippedReason: null,
|
||||
} satisfies CodexTraceCandidate;
|
||||
});
|
||||
const scanned = scanCodexTraceRoot(options).filter((candidate) => candidate.included && isTextReadableTrace(candidate.kind, candidate.path));
|
||||
const byPath = new Map<string, CodexTraceCandidate>();
|
||||
for (const candidate of [...active, ...scanned]) {
|
||||
if (!isTextReadableTrace(candidate.kind, candidate.path)) continue;
|
||||
byPath.set(candidate.path, candidate);
|
||||
}
|
||||
return [...byPath.values()].sort((left, right) => right.mtimeMs - left.mtimeMs).slice(0, options.fileLimit);
|
||||
}
|
||||
|
||||
async function grepJsonlFile(
|
||||
candidate: CodexTraceCandidate,
|
||||
rootReal: string,
|
||||
matcher: RegExp,
|
||||
options: CodexTraceOptions,
|
||||
results: Record<string, unknown>[],
|
||||
limit: number,
|
||||
): Promise<{ linesScanned: number; matchedTotal: number }> {
|
||||
let linesScanned = 0;
|
||||
let matchedTotal = 0;
|
||||
const input = createReadStream(candidate.path, { encoding: "utf8" });
|
||||
const reader = createInterface({ input, crlfDelay: Infinity });
|
||||
try {
|
||||
for await (const line of reader) {
|
||||
linesScanned += 1;
|
||||
const event = parseJsonlEvent(line);
|
||||
if (event === null) {
|
||||
const rawMatch = matcher.test(line);
|
||||
matcher.lastIndex = 0;
|
||||
if (!rawMatch) continue;
|
||||
matchedTotal += 1;
|
||||
if (results.length < limit) {
|
||||
results.push({
|
||||
file: candidate.relativePath,
|
||||
line: linesScanned,
|
||||
timestamp: null,
|
||||
type: "raw",
|
||||
fields: ["raw"],
|
||||
summary: truncateOneLine(redactTraceText(line), options.contextChars),
|
||||
signals: traceSignals(line),
|
||||
});
|
||||
}
|
||||
continue;
|
||||
}
|
||||
if (!options.includeSystem && event.type === "session_meta") continue;
|
||||
if (!options.includeSystem && isBootstrapTraceEvent(event)) continue;
|
||||
if (options.since !== null && typeof event.timestamp === "string" && event.timestamp < options.since) continue;
|
||||
const fields = traceEventFields(event);
|
||||
const matchedFields: string[] = [];
|
||||
const matchedValues: string[] = [];
|
||||
for (const field of fields) {
|
||||
if (matcher.test(field.value)) {
|
||||
matchedFields.push(field.name);
|
||||
matchedValues.push(field.value);
|
||||
}
|
||||
matcher.lastIndex = 0;
|
||||
}
|
||||
if (matchedFields.length === 0) continue;
|
||||
matchedTotal += 1;
|
||||
if (results.length >= limit) continue;
|
||||
const summary = summarizeMatchedTraceEvent(event, fields, matcher, options.contextChars);
|
||||
results.push({
|
||||
file: path.relative(rootReal, candidate.path),
|
||||
line: linesScanned,
|
||||
timestamp: typeof event.timestamp === "string" ? event.timestamp : null,
|
||||
type: event.type,
|
||||
item: traceEventItem(event),
|
||||
fields: matchedFields,
|
||||
summary,
|
||||
signals: traceSignals(matchedValues.join(" ")),
|
||||
});
|
||||
}
|
||||
} finally {
|
||||
reader.close();
|
||||
input.destroy();
|
||||
}
|
||||
return { linesScanned, matchedTotal };
|
||||
}
|
||||
|
||||
function selectedCandidates(options: CodexTraceOptions, candidates: CodexTraceCandidate[]): CodexTraceCandidate[] {
|
||||
return candidates.filter((item) => item.included).slice(0, options.limit);
|
||||
}
|
||||
@@ -377,6 +797,151 @@ function policySummary(options: CodexTraceOptions): Record<string, unknown> {
|
||||
};
|
||||
}
|
||||
|
||||
function parseJsonlEvent(line: string): Record<string, unknown> | null {
|
||||
try {
|
||||
const parsed = JSON.parse(line);
|
||||
return typeof parsed === "object" && parsed !== null && !Array.isArray(parsed) ? parsed as Record<string, unknown> : null;
|
||||
} catch {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
function isBootstrapTraceEvent(event: Record<string, unknown>): boolean {
|
||||
const payload = asRecord(event.payload);
|
||||
if (event.type === "session_meta") return true;
|
||||
if (event.type !== "response_item") return false;
|
||||
if (payload?.type !== "message") return false;
|
||||
const content = Array.isArray(payload.content) ? payload.content : [];
|
||||
const text = content
|
||||
.map((item) => stringValue(asRecord(item)?.text))
|
||||
.filter((value): value is string => value !== null)
|
||||
.join("\n");
|
||||
return text.includes("<permissions instructions>")
|
||||
|| text.includes("<environment_context>")
|
||||
|| text.includes("# AGENTS.md instructions")
|
||||
|| text.includes("You are Codex, a coding agent")
|
||||
|| text.includes("Knowledge cutoff:");
|
||||
}
|
||||
|
||||
function traceEventFields(event: Record<string, unknown>): Array<{ name: string; value: string }> {
|
||||
const payload = asRecord(event.payload);
|
||||
const fields: Array<{ name: string; value: string }> = [];
|
||||
addField(fields, "timestamp", event.timestamp);
|
||||
addField(fields, "type", event.type);
|
||||
addField(fields, "payload.type", payload?.type);
|
||||
addField(fields, "payload.role", payload?.role);
|
||||
addField(fields, "payload.name", payload?.name);
|
||||
addField(fields, "message", payload?.message);
|
||||
addField(fields, "arguments", payload?.arguments);
|
||||
addField(fields, "output", payload?.output);
|
||||
const content = Array.isArray(payload?.content) ? payload.content : [];
|
||||
for (const [index, item] of content.entries()) {
|
||||
const record = asRecord(item);
|
||||
addField(fields, `content.${index}.type`, record?.type);
|
||||
addField(fields, `content.${index}.text`, record?.text);
|
||||
}
|
||||
return fields;
|
||||
}
|
||||
|
||||
function summarizeTraceEvent(event: Record<string, unknown>, maxChars: number): string {
|
||||
const payload = asRecord(event.payload);
|
||||
const eventType = stringValue(event.type);
|
||||
const payloadType = stringValue(payload?.type);
|
||||
if (eventType === "response_item" && payloadType === "function_call") {
|
||||
const name = stringValue(payload?.name) ?? "function_call";
|
||||
const args = parseJsonObject(stringValue(payload?.arguments) ?? "");
|
||||
const cmd = stringValue(args?.cmd);
|
||||
return truncateOneLine(redactTraceText(cmd === null ? `${name} ${stringValue(payload?.arguments) ?? ""}` : `${name}: ${cmd}`), maxChars);
|
||||
}
|
||||
if (eventType === "response_item" && payloadType === "function_call_output") {
|
||||
const callId = stringValue(payload?.call_id);
|
||||
const output = stringValue(payload?.output) ?? "";
|
||||
return truncateOneLine(redactTraceText(`function_call_output${callId === null ? "" : ` ${callId}`}: ${output}`), maxChars);
|
||||
}
|
||||
if (eventType === "response_item" && payloadType === "message") {
|
||||
const content = Array.isArray(payload?.content) ? payload.content : [];
|
||||
const texts = content.map((item) => stringValue(asRecord(item)?.text)).filter((value): value is string => value !== null);
|
||||
return truncateOneLine(redactTraceText(texts.join(" ")), maxChars);
|
||||
}
|
||||
if (eventType === "event_msg") {
|
||||
return truncateOneLine(redactTraceText(stringValue(payload?.message) ?? JSON.stringify(payload ?? {})), maxChars);
|
||||
}
|
||||
return truncateOneLine(redactTraceText(JSON.stringify(event)), maxChars);
|
||||
}
|
||||
|
||||
function summarizeMatchedTraceEvent(event: Record<string, unknown>, fields: Array<{ name: string; value: string }>, matcher: RegExp, maxChars: number): string {
|
||||
const item = traceEventItem(event);
|
||||
for (const field of fields) {
|
||||
const match = matcher.exec(field.value);
|
||||
matcher.lastIndex = 0;
|
||||
if (match === null || match.index < 0) continue;
|
||||
const snippet = snippetAroundMatch(redactTraceText(field.value), match.index, maxChars);
|
||||
return truncateOneLine(`${item === null ? String(event.type ?? "event") : item} ${field.name}: ${snippet}`, maxChars + 120);
|
||||
}
|
||||
return summarizeTraceEvent(event, maxChars);
|
||||
}
|
||||
|
||||
function snippetAroundMatch(value: string, matchIndex: number, maxChars: number): string {
|
||||
const halfWindow = Math.max(40, Math.floor(maxChars / 2));
|
||||
const start = Math.max(0, matchIndex - halfWindow);
|
||||
const end = Math.min(value.length, start + maxChars);
|
||||
const prefix = start > 0 ? "…" : "";
|
||||
const suffix = end < value.length ? "…" : "";
|
||||
return `${prefix}${value.slice(start, end)}${suffix}`;
|
||||
}
|
||||
|
||||
function traceEventItem(event: Record<string, unknown>): string | null {
|
||||
const payload = asRecord(event.payload);
|
||||
return stringValue(payload?.name) ?? stringValue(payload?.type) ?? stringValue(payload?.role);
|
||||
}
|
||||
|
||||
function traceSignals(text: string): Record<string, boolean> {
|
||||
return {
|
||||
authLoginFailed: /auth-login-failed/iu.test(text),
|
||||
status503: /status["']?\s*[:=]\s*503|Service Unavailable/iu.test(text),
|
||||
sshHint: /UNIDESK_SSH_HINT|UNIDESK_SSH_TCP_POOL_HINT/iu.test(text),
|
||||
transTimeoutHint: /UNIDESK_TRAN_TIMEOUT_HINT/iu.test(text),
|
||||
sshTiming: /UNIDESK_SSH_TIMING/iu.test(text),
|
||||
downloadProgress: /unidesk\.ssh\.download\.progress/iu.test(text),
|
||||
sshPlaywright: /"command"\s*:\s*"ssh playwright"|command['"]?\s*[:=]\s*['"]?ssh playwright/iu.test(text),
|
||||
};
|
||||
}
|
||||
|
||||
function addField(fields: Array<{ name: string; value: string }>, name: string, value: unknown): void {
|
||||
const text = stringValue(value);
|
||||
if (text !== null && text.length > 0) fields.push({ name, value: text });
|
||||
}
|
||||
|
||||
function asRecord(value: unknown): Record<string, unknown> | null {
|
||||
return typeof value === "object" && value !== null && !Array.isArray(value) ? value as Record<string, unknown> : null;
|
||||
}
|
||||
|
||||
function stringValue(value: unknown): string | null {
|
||||
return typeof value === "string" ? value : null;
|
||||
}
|
||||
|
||||
function numberValue(value: unknown): number | null {
|
||||
return typeof value === "number" && Number.isFinite(value) ? value : null;
|
||||
}
|
||||
|
||||
function stringArray(value: unknown): string[] {
|
||||
return Array.isArray(value) ? value.map((item) => String(item)) : [];
|
||||
}
|
||||
|
||||
function recordArray(value: unknown): Record<string, unknown>[] {
|
||||
return Array.isArray(value) ? value.map(asRecord).filter((item): item is Record<string, unknown> => item !== null) : [];
|
||||
}
|
||||
|
||||
function parseJsonObject(value: string): Record<string, unknown> | null {
|
||||
if (value.trim().length === 0) return null;
|
||||
try {
|
||||
const parsed = JSON.parse(value);
|
||||
return asRecord(parsed);
|
||||
} catch {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
function readFileSlice(filePath: string, start: number, length: number): string {
|
||||
if (length <= 0) return "";
|
||||
const fd = openSync(filePath, "r");
|
||||
@@ -396,12 +961,121 @@ function redactTraceText(text: string): string {
|
||||
.replace(/((?:api[_-]?key|token|authorization|password|secret)["']?\s*[:=]\s*["']?)[^"',\s}]+/giu, "$1<redacted>");
|
||||
}
|
||||
|
||||
function truncateOneLine(value: string, maxChars: number): string {
|
||||
const oneLine = value.replace(/\s+/gu, " ").trim();
|
||||
return oneLine.length <= maxChars ? oneLine : `${oneLine.slice(0, Math.max(0, maxChars - 1))}…`;
|
||||
}
|
||||
|
||||
function truncateMiddle(value: string, maxChars: number): string {
|
||||
if (value.length <= maxChars) return value;
|
||||
if (maxChars <= 8) return value.slice(0, maxChars);
|
||||
const keep = maxChars - 1;
|
||||
const left = Math.ceil(keep * 0.58);
|
||||
const right = keep - left;
|
||||
return `${value.slice(0, left)}…${value.slice(value.length - right)}`;
|
||||
}
|
||||
|
||||
function renderTable(headers: string[], rows: string[][]): string {
|
||||
const widths = headers.map((header, index) => Math.max(header.length, ...rows.map((row) => visibleLength(row[index] ?? ""))));
|
||||
const renderRow = (row: string[]) => row.map((cell, index) => padRight(cell, widths[index] ?? 0)).join(" ").trimEnd();
|
||||
return [renderRow(headers), ...rows.map(renderRow)].join("\n");
|
||||
}
|
||||
|
||||
function padRight(value: string, width: number): string {
|
||||
const length = visibleLength(value);
|
||||
return length >= width ? value : `${value}${" ".repeat(width - length)}`;
|
||||
}
|
||||
|
||||
function visibleLength(value: string): number {
|
||||
return [...value].length;
|
||||
}
|
||||
|
||||
function formatBytes(value: number | null): string {
|
||||
if (value === null) return "-";
|
||||
const units = ["B", "Ki", "Mi", "Gi"];
|
||||
let size = value;
|
||||
let unitIndex = 0;
|
||||
while (size >= 1024 && unitIndex < units.length - 1) {
|
||||
size /= 1024;
|
||||
unitIndex += 1;
|
||||
}
|
||||
return unitIndex === 0 ? `${value}B` : `${size.toFixed(size >= 10 ? 0 : 1)}${units[unitIndex]}`;
|
||||
}
|
||||
|
||||
function ageFromIso(value: string | null): string {
|
||||
if (value === null) return "-";
|
||||
const time = Date.parse(value);
|
||||
if (!Number.isFinite(time)) return "-";
|
||||
const seconds = Math.max(0, Math.floor((Date.now() - time) / 1000));
|
||||
if (seconds < 60) return `${seconds}s`;
|
||||
const minutes = Math.floor(seconds / 60);
|
||||
if (minutes < 60) return `${minutes}m`;
|
||||
const hours = Math.floor(minutes / 60);
|
||||
if (hours < 48) return `${hours}h`;
|
||||
return `${Math.floor(hours / 24)}d`;
|
||||
}
|
||||
|
||||
function shortIso(value: string | null): string {
|
||||
if (value === null) return "-";
|
||||
return value.replace(/\.\d{3}Z$/u, "Z");
|
||||
}
|
||||
|
||||
function signalLabel(signals: Record<string, unknown> | null): string {
|
||||
if (signals === null) return "-";
|
||||
const labels = [
|
||||
signals.authLoginFailed === true ? "auth" : "",
|
||||
signals.status503 === true ? "503" : "",
|
||||
signals.sshHint === true ? "ssh-hint" : "",
|
||||
signals.transTimeoutHint === true ? "timeout" : "",
|
||||
signals.sshTiming === true ? "timing" : "",
|
||||
signals.downloadProgress === true ? "download" : "",
|
||||
signals.sshPlaywright === true ? "playwright" : "",
|
||||
].filter((label) => label.length > 0);
|
||||
return labels.length === 0 ? "-" : labels.join(",");
|
||||
}
|
||||
|
||||
function resolveTraceFile(root: string, file: string): string {
|
||||
if (file === "~" || file.startsWith("~/")) return resolveUserPath(file);
|
||||
if (path.isAbsolute(file)) return file;
|
||||
return path.resolve(root, file);
|
||||
}
|
||||
|
||||
function readProcCmdline(pid: number): string {
|
||||
try {
|
||||
const raw = readFileSync(path.join("/proc", String(pid), "cmdline"), "utf8");
|
||||
return raw.split("\0").filter(Boolean).join(" ");
|
||||
} catch {
|
||||
return "";
|
||||
}
|
||||
}
|
||||
|
||||
function isCodexSessionProcess(cmdline: string): boolean {
|
||||
const [executable] = cmdline.split(/\s+/u).filter(Boolean);
|
||||
if (executable === undefined) return false;
|
||||
return path.basename(executable) === "codex";
|
||||
}
|
||||
|
||||
function makeMatcher(pattern: string, fixed: boolean): RegExp {
|
||||
const source = fixed ? escapeRegExp(pattern) : pattern;
|
||||
try {
|
||||
return new RegExp(source, "iu");
|
||||
} catch (error) {
|
||||
if (fixed) throw error;
|
||||
return new RegExp(escapeRegExp(pattern), "iu");
|
||||
}
|
||||
}
|
||||
|
||||
function escapeRegExp(value: string): string {
|
||||
return value.replace(/[.*+?^${}()|[\]\\]/gu, "\\$&");
|
||||
}
|
||||
|
||||
function parseOutputMode(raw: string | undefined, wide: boolean): CodexTraceOutput {
|
||||
if (wide) return "wide";
|
||||
if (raw === undefined) return "text";
|
||||
if (raw === "json" || raw === "yaml" || raw === "name" || raw === "wide") return raw;
|
||||
throw new Error(`codex trace output mode must be one of: json, yaml, name, wide; got ${raw}`);
|
||||
}
|
||||
|
||||
function defaultCodexRoot(): string {
|
||||
return path.join(homedir(), ".codex");
|
||||
}
|
||||
|
||||
+6
-1
@@ -389,6 +389,8 @@ function codexHelp(): unknown {
|
||||
output: "json",
|
||||
usage: [
|
||||
"bun scripts/cli.ts codex trace list [--root ~/.codex] [--limit 30]",
|
||||
"bun scripts/cli.ts codex trace active [--root ~/.codex]",
|
||||
"bun scripts/cli.ts codex trace grep --pattern 'playwright|auth-login-failed' [--file sessions/...jsonl] [--since ISO]",
|
||||
"bun scripts/cli.ts codex trace collect [--root ~/.codex] [--output .state/codex-trace/<timestamp>] [--limit 30]",
|
||||
"bun scripts/cli.ts codex trace show --file sessions/2026/06/16/<session>.jsonl [--root ~/.codex] [--tail-bytes 12000]",
|
||||
"bun scripts/cli.ts codex deploy <commitId> # disabled legacy deployment entry",
|
||||
@@ -442,6 +444,9 @@ function codexHelp(): unknown {
|
||||
defaultRoot: "~/.codex",
|
||||
defaultIncludes: ["sessions/*.jsonl", "history.jsonl", "shell_snapshots", "*.log", "trace-named text files"],
|
||||
defaultExcludes: ["auth/config", "sqlite", "cache/.tmp/generated_images/plugins/skills"],
|
||||
active: "codex trace active finds open Codex session JSONL files from /proc without requiring lsof.",
|
||||
grep: "codex trace grep parses JSONL records and returns bounded summaries/signals instead of printing whole matching lines.",
|
||||
output: "Default output is concise text/table; use -o json|yaml|name|wide for machine or wider output.",
|
||||
collectOutput: ".state/codex-trace/<timestamp>/manifest.json",
|
||||
note: "Use --root <dir> to scan another local Codex trace directory; collect copies bounded files locally and never uploads or deletes source files.",
|
||||
},
|
||||
@@ -726,7 +731,7 @@ export async function staticNamespaceHelp(args: string[]): Promise<unknown | nul
|
||||
if (top === "gc") return gcHelp();
|
||||
if (top === "commander") return commanderHelp();
|
||||
if (top === "schedule") return scheduleHelp();
|
||||
if (top === "codex" && sub === "trace") return loadHelp(async () => (await import("./codex-trace")).codexTraceHelp(), codexHelp());
|
||||
if (top === "codex" && sub === "trace") return null;
|
||||
if (top === "codex") return codexHelp();
|
||||
if (top === "job") return jobHelp();
|
||||
if (top === "debug") return debugHelp();
|
||||
|
||||
Reference in New Issue
Block a user