feat: add code queue submit cli

This commit is contained in:
Codex
2026-05-17 05:49:27 +00:00
parent 68a14cdd4e
commit 2b2a327301
5 changed files with 250 additions and 9 deletions
+205 -1
View File
@@ -1,3 +1,4 @@
import { readFileSync } from "node:fs";
import { type UniDeskConfig } from "./config";
import { coreInternalFetch } from "./microservices";
@@ -33,6 +34,19 @@ interface CodexJudgeOptions {
includePrompt: boolean;
}
interface CodexSubmitOptions {
prompt: string;
queueId: string | undefined;
providerId: string | undefined;
cwd: string | undefined;
model: string | undefined;
reasoningEffort: string | undefined;
executionMode: string | undefined;
maxAttempts: number | undefined;
referenceTaskIds: string[];
dryRun: boolean;
}
type CodexRequestInit = { method?: string; body?: unknown };
type CodexResponseFetcher = (path: string, init?: CodexRequestInit) => unknown;
type AsyncCodexResponseFetcher = (path: string, init?: CodexRequestInit) => Promise<unknown>;
@@ -66,6 +80,16 @@ function stringList(value: unknown): string[] {
return asArray(value).map((item) => String(item ?? "")).filter((item) => item.length > 0);
}
function textPreview(value: string, maxChars: number): Record<string, unknown> {
const truncated = value.length > maxChars;
return {
text: truncated ? value.slice(0, maxChars) : value,
chars: value.length,
truncated,
omittedChars: truncated ? value.length - maxChars : 0,
};
}
function fmtDuration(ms: unknown): string {
const value = Number(ms);
if (!Number.isFinite(value) || value < 0) return "--";
@@ -624,6 +648,19 @@ function optionValue(args: string[], names: string[]): string | undefined {
return undefined;
}
function optionValues(args: string[], names: string[]): string[] {
const values: string[] = [];
for (let index = 0; index < args.length; index += 1) {
const name = args[index] ?? "";
if (!names.includes(name)) continue;
const raw = args[index + 1];
if (raw === undefined || raw.trim().length === 0) throw new Error(`${name} requires a non-empty value`);
values.push(raw.trim());
index += 1;
}
return values;
}
function positionalArgs(args: string[]): string[] {
const positions: string[] = [];
for (let index = 0; index < args.length; index += 1) {
@@ -637,6 +674,19 @@ function positionalArgs(args: string[]): string[] {
return positions;
}
function positionalArgsWithValueOptions(args: string[], valueOptions: Set<string>): string[] {
const positions: string[] = [];
for (let index = 0; index < args.length; index += 1) {
const value = args[index] ?? "";
if (value.startsWith("--")) {
if (valueOptions.has(value)) index += 1;
continue;
}
positions.push(value);
}
return positions;
}
function requireMergeSourceQueueId(args: string[], command: string): string {
const raw = optionValue(args, ["--source", "--from", "--queue"]) ?? positionalArgs(args)[0];
if (raw === undefined || raw.trim().length === 0) throw new Error(`${command} requires source queue id, for example: codex queue merge old --into default`);
@@ -665,8 +715,158 @@ function codexMoveTask(taskId: string, queueId: string): unknown {
return unwrapCodexResponse(coreInternalFetch(`/api/microservices/code-queue/proxy/api/tasks/${encodeURIComponent(taskId)}/move`, { method: "POST", body: { queueId } }));
}
function promptFromSubmitArgs(args: string[]): string {
const promptFile = optionValue(args, ["--prompt-file", "--file"]);
const promptStdin = hasFlag(args, "--prompt-stdin") || hasFlag(args, "--stdin");
const promptArgs = positionalArgsWithValueOptions(args, new Set([
"--prompt-file",
"--file",
"--queue",
"--queue-id",
"--provider",
"--provider-id",
"--cwd",
"--workdir",
"--model",
"--reasoning-effort",
"--execution-mode",
"--mode",
"--max-attempts",
"--reference-task-id",
"--reference",
"--ref",
]));
const sources = [promptFile !== undefined, promptStdin, promptArgs.length > 0].filter(Boolean).length;
if (sources !== 1) throw new Error("codex submit requires exactly one prompt source: positional prompt, --prompt-file, or --prompt-stdin");
const text = promptFile !== undefined
? (promptFile === "-" ? readFileSync(0, "utf8") : readFileSync(promptFile, "utf8"))
: promptStdin
? readFileSync(0, "utf8")
: promptArgs.join(" ");
if (text.trim().length === 0) throw new Error("codex submit prompt must not be empty");
return text;
}
function referenceTaskIdsFromOptions(args: string[]): string[] {
const values = optionValues(args, ["--reference-task-id", "--reference", "--ref"]);
const ids: string[] = [];
for (const value of values.flatMap((item) => item.split(/[,\s]+/u))) {
const id = value.trim();
if (id.length > 0 && !ids.includes(id)) ids.push(id);
}
return ids;
}
function parseSubmitOptions(args: string[]): CodexSubmitOptions {
const maxAttempts = args.some((arg) => arg === "--max-attempts")
? positiveIntegerOption(args, ["--max-attempts"], 99, 99)
: undefined;
return {
prompt: promptFromSubmitArgs(args),
queueId: optionValue(args, ["--queue", "--queue-id"]),
providerId: optionValue(args, ["--provider-id", "--provider"]),
cwd: optionValue(args, ["--cwd", "--workdir"]),
model: optionValue(args, ["--model"]),
reasoningEffort: optionValue(args, ["--reasoning-effort"]),
executionMode: optionValue(args, ["--execution-mode", "--mode"]),
maxAttempts,
referenceTaskIds: referenceTaskIdsFromOptions(args),
dryRun: hasFlag(args, "--dry-run"),
};
}
function submitPayload(options: CodexSubmitOptions): Record<string, unknown> {
return {
prompt: options.prompt,
...(options.queueId === undefined ? {} : { queueId: options.queueId }),
...(options.providerId === undefined ? {} : { providerId: options.providerId }),
...(options.cwd === undefined ? {} : { cwd: options.cwd }),
...(options.model === undefined ? {} : { model: options.model }),
...(options.reasoningEffort === undefined ? {} : { reasoningEffort: options.reasoningEffort }),
...(options.executionMode === undefined ? {} : { executionMode: options.executionMode }),
...(options.maxAttempts === undefined ? {} : { maxAttempts: options.maxAttempts }),
...(options.referenceTaskIds.length === 0 ? {} : { referenceTaskIds: options.referenceTaskIds }),
};
}
function compactTaskMutationResponse(task: unknown): Record<string, unknown> {
const record = asRecord(task) ?? {};
const taskId = asString(record.id);
return {
id: taskId || null,
queueId: record.queueId ?? null,
status: record.status ?? null,
queuedReason: record.queuedReason ?? null,
providerId: record.providerId ?? null,
model: record.model ?? null,
reasoningEffort: record.reasoningEffort ?? null,
cwd: record.cwd ?? null,
executionMode: record.executionMode ?? null,
maxAttempts: record.maxAttempts ?? null,
currentAttempt: record.currentAttempt ?? null,
cancelRequested: record.cancelRequested ?? null,
createdAt: record.createdAt ?? null,
startedAt: record.startedAt ?? null,
updatedAt: record.updatedAt ?? null,
finishedAt: record.finishedAt ?? null,
prompt: textPreview(asString(record.displayPrompt ?? record.basePrompt ?? record.prompt), 1200),
commands: taskId.length === 0 ? null : {
show: `bun scripts/cli.ts codex task ${taskId}`,
trace: `bun scripts/cli.ts codex task ${taskId} --trace --tail --limit ${defaultTraceLimit}`,
output: `bun scripts/cli.ts codex output ${taskId} --tail --limit ${defaultOutputLimit}`,
interrupt: `bun scripts/cli.ts codex interrupt ${taskId}`,
move: `bun scripts/cli.ts codex move ${taskId} --queue <queueId>`,
},
};
}
function compactQueueMutationSummary(value: unknown): Record<string, unknown> | null {
const record = asRecord(value);
if (record === null) return null;
return {
activeQueueIds: record.activeQueueIds ?? null,
activeTaskIds: record.activeTaskIds ?? null,
queuedTaskIds: record.queuedTaskIds ?? null,
counts: record.counts ?? null,
byQueue: Array.isArray(record.byQueue) ? record.byQueue : undefined,
};
}
function codexSubmitTask(args: string[]): unknown {
const options = parseSubmitOptions(args);
const payload = submitPayload(options);
if (options.dryRun) {
return {
ok: true,
dryRun: true,
request: {
...payload,
prompt: textPreview(options.prompt, 3000),
},
};
}
const response = unwrapCodexResponse(coreInternalFetch("/api/microservices/code-queue/proxy/api/tasks", { method: "POST", body: payload }));
return {
upstream: response.upstream,
tasks: asArray(response.body.tasks).map(compactTaskMutationResponse),
queue: compactQueueMutationSummary(response.body.queue),
};
}
function codexInterruptTask(taskId: string): unknown {
const response = unwrapCodexResponse(coreInternalFetch(`/api/microservices/k3sctl-adapter/proxy/api/services/code-queue-scheduler/proxy/api/tasks/${encodeURIComponent(taskId)}/interrupt`, { method: "POST" }));
return {
upstream: response.upstream,
task: compactTaskMutationResponse(response.body.task),
queue: compactQueueMutationSummary(response.body.queue),
};
}
export async function runCodeQueueCommand(_config: UniDeskConfig, args: string[]): Promise<unknown> {
const [action = "task", taskIdArg] = args;
if (action === "submit" || action === "enqueue") {
return codexSubmitTask(args.slice(1));
}
if (action === "task" || action === "summary" || action === "show") {
const taskId = requireTaskId(taskIdArg, `codex ${action}`);
return codexTaskQuery(taskId, args.slice(2));
@@ -693,5 +893,9 @@ export async function runCodeQueueCommand(_config: UniDeskConfig, args: string[]
const taskId = requireTaskId(taskIdArg, "codex move");
return codexMoveTask(taskId, requireQueueId(args.slice(2), "codex move"));
}
throw new Error("codex command must be one of: task, summary, show, output, judge, queues, queue list, queue create, queue merge, move");
if (action === "interrupt" || action === "cancel") {
const taskId = requireTaskId(taskIdArg, `codex ${action}`);
return codexInterruptTask(taskId);
}
throw new Error("codex command must be one of: submit, enqueue, task, summary, show, output, judge, queues, queue list, queue create, queue merge, move, interrupt, cancel");
}