Files
openclaw/extensions/codex/src/command-handler-args.ts
Peter Steinberger 6235ea55ac refactor(codex): split command handlers (#113335)
* refactor(codex): split command handlers

* refactor(codex): keep helper types private
2026-07-24 07:29:44 -07:00

462 lines
14 KiB
TypeScript

import { expectDefined } from "openclaw/plugin-sdk/expect-runtime";
import { parseStrictPositiveInteger } from "openclaw/plugin-sdk/number-runtime";
import type { PluginCommandResult } from "openclaw/plugin-sdk/plugin-entry";
import { normalizeOptionalString } from "openclaw/plugin-sdk/string-coerce-runtime";
import type { CodexComputerUseConfig } from "./app-server/config.js";
import {
buildCodexCommandPickerPresentation,
type CodexCommandPickerButton,
} from "./command-presentation.js";
type ParsedBindArgs = {
threadId?: string;
cwd?: string;
model?: string;
provider?: string;
help?: boolean;
};
type ParsedComputerUseArgs = {
action: "status" | "install";
overrides: Partial<CodexComputerUseConfig>;
hasOverrides: boolean;
persistentIdentity: Partial<Pick<CodexComputerUseConfig, "pluginName" | "mcpServerName">>;
help?: boolean;
};
type ParsedCodexCliSessionsArgs = {
host?: string;
filter: string;
limit?: number;
help?: boolean;
};
export type ParsedResumeArgs = {
threadId?: string;
host?: string;
bindHere?: boolean;
help?: boolean;
};
/** No-arg `/codex` picker. */
export function buildCodexSubcommandPickerReply(): PluginCommandResult {
const verbs: CodexCommandPickerButton[] = [
{ label: "plugins", command: "/codex plugins menu" },
{ label: "permissions", command: "/codex permissions menu" },
{ label: "fast", command: "/codex fast menu" },
{ label: "computer-use", command: "/codex computer-use menu" },
{ label: "account", command: "/codex account" },
{ label: "help", command: "/codex help" },
];
const fallbackTextLines = [
"Codex commands. Pick a category or type:",
"",
...verbs.map((v, i) => ` ${i + 1}. ${v.command}`),
"",
"Tap 'help' (or type /codex help) for the full list of typeable verbs",
"including threads, mcp, binding, detach, skills, resume, bind, steer,",
"model, diagnostics, compact, review, computer-use.",
"",
"Top-level shortcuts cover everyday operations: /status, /fast, /help, /stop, /models.",
];
return {
text: fallbackTextLines.join("\n"),
presentation: buildCodexCommandPickerPresentation(
"Codex commands",
"Pick a Codex subcommand:",
verbs,
),
};
}
export function buildCodexFastMenuReply(): PluginCommandResult {
const modes = ["on", "off", "status"] as const;
const buttons: CodexCommandPickerButton[] = [
...modes.map((mode) => ({ label: mode, command: `/codex fast ${mode}` })),
{ label: "back", command: "/codex" },
];
const fallbackTextLines = [
"Codex fast mode. Pick one or type /codex fast <mode>:",
"",
...modes.map((m, i) => ` ${i + 1}. /codex fast ${m}`),
"",
"Type '/codex' to go back to the main menu.",
];
return {
text: fallbackTextLines.join("\n"),
presentation: buildCodexCommandPickerPresentation(
"Codex fast mode",
"Pick a Codex fast mode:",
buttons,
),
};
}
export function buildCodexPermissionsMenuReply(): PluginCommandResult {
const modes = ["default", "yolo", "status"] as const;
const buttons: CodexCommandPickerButton[] = [
...modes.map((mode) => ({ label: mode, command: `/codex permissions ${mode}` })),
{ label: "back", command: "/codex" },
];
const fallbackTextLines = [
"Codex permissions. Pick one or type /codex permissions <mode>:",
"",
...modes.map((m, i) => ` ${i + 1}. /codex permissions ${m}`),
"",
"Type '/codex' to go back to the main menu.",
];
return {
text: fallbackTextLines.join("\n"),
presentation: buildCodexCommandPickerPresentation(
"Codex permissions",
"Pick a Codex permissions mode:",
buttons,
),
};
}
export function buildCodexComputerUseMenuReply(): PluginCommandResult {
const actions = ["status", "install"] as const;
const buttons: CodexCommandPickerButton[] = [
...actions.map((action) => ({
label: action,
command: `/codex computer-use ${action}`,
})),
{ label: "back", command: "/codex" },
];
const fallbackTextLines = [
"Codex computer-use. Pick one or type /codex computer-use <action>:",
"",
...actions.map((a, i) => ` ${i + 1}. /codex computer-use ${a}`),
"",
"Flag-driven invocations (--source, --marketplace-path, --marketplace) are not in the picker. Type '/codex computer-use' or read '/codex help' for the full surface.",
"",
"Type '/codex' to go back to the main menu.",
];
return {
text: fallbackTextLines.join("\n"),
presentation: buildCodexCommandPickerPresentation(
"Codex computer-use",
"Pick a Codex computer-use action:",
buttons,
),
};
}
export function isMenuVerb(rest: readonly string[]): boolean {
return rest.length === 1 && (rest[0] ?? "").trim().toLowerCase() === "menu";
}
export function splitArgs(value: string | undefined): string[] {
const input = value ?? "";
const args: string[] = [];
let current = "";
let quote: '"' | "'" | undefined;
let escaping = false;
let tokenStarted = false;
for (const char of input) {
if (escaping) {
current += char;
escaping = false;
tokenStarted = true;
continue;
}
if (char === "\\" && quote !== "'") {
escaping = true;
tokenStarted = true;
continue;
}
if (quote) {
if (char === quote) {
quote = undefined;
} else {
current += char;
}
tokenStarted = true;
continue;
}
if (char === '"' || char === "'") {
quote = char;
tokenStarted = true;
continue;
}
if (/\s/.test(char)) {
if (tokenStarted) {
args.push(current);
current = "";
tokenStarted = false;
}
continue;
}
current += char;
tokenStarted = true;
}
if (escaping) {
current += "\\";
}
if (tokenStarted) {
args.push(current);
}
return args;
}
export function parseBindArgs(args: string[]): ParsedBindArgs {
const parsed: ParsedBindArgs = {};
for (let index = 0; index < args.length; index += 1) {
const arg = expectDefined(args[index], "current Codex bind argument");
if (arg === "--help" || arg === "-h") {
parsed.help = true;
continue;
}
if (arg === "--cwd") {
const value = readRequiredOptionValue(args, index);
if (!value || parsed.cwd !== undefined) {
parsed.help = true;
continue;
}
parsed.cwd = value;
index += 1;
continue;
}
if (arg === "--model") {
const value = readRequiredOptionValue(args, index);
if (!value || parsed.model !== undefined) {
parsed.help = true;
continue;
}
parsed.model = value;
index += 1;
continue;
}
if (arg === "--provider" || arg === "--model-provider") {
const value = readRequiredOptionValue(args, index);
if (!value || parsed.provider !== undefined) {
parsed.help = true;
continue;
}
parsed.provider = value;
index += 1;
continue;
}
if (!arg.startsWith("-") && !parsed.threadId) {
parsed.threadId = arg;
continue;
}
parsed.help = true;
}
parsed.threadId = normalizeOptionalString(parsed.threadId);
parsed.cwd = normalizeOptionalString(parsed.cwd);
parsed.model = normalizeOptionalString(parsed.model);
parsed.provider = normalizeOptionalString(parsed.provider);
return parsed;
}
export function parseCodexCliSessionsArgs(args: string[]): ParsedCodexCliSessionsArgs {
const parsed: ParsedCodexCliSessionsArgs = { filter: "" };
const filter: string[] = [];
for (let index = 0; index < args.length; index += 1) {
const arg = expectDefined(args[index], "current Codex sessions argument");
if (arg === "--help" || arg === "-h") {
parsed.help = true;
continue;
}
if (arg === "--host" || arg === "--node") {
const value = readRequiredOptionValue(args, index);
if (!value || parsed.host !== undefined) {
parsed.help = true;
continue;
}
parsed.host = value;
index += 1;
continue;
}
if (arg === "--limit") {
const value = readRequiredOptionValue(args, index);
const parsedLimit = parseStrictPositiveInteger(value);
if (parsedLimit === undefined) {
parsed.help = true;
continue;
}
parsed.limit = parsedLimit;
index += 1;
continue;
}
if (arg.startsWith("-")) {
parsed.help = true;
continue;
}
filter.push(arg);
}
parsed.host = normalizeOptionalString(parsed.host);
parsed.filter = filter.join(" ").trim();
return parsed;
}
export function parseResumeArgs(args: string[]): ParsedResumeArgs {
const parsed: ParsedResumeArgs = {};
for (let index = 0; index < args.length; index += 1) {
const arg = expectDefined(args[index], "current Codex resume argument");
if (arg === "--help" || arg === "-h") {
parsed.help = true;
continue;
}
if (arg === "--host" || arg === "--node") {
const value = readRequiredOptionValue(args, index);
if (!value || parsed.host !== undefined) {
parsed.help = true;
continue;
}
parsed.host = value;
index += 1;
continue;
}
if (arg === "--bind") {
const value = readRequiredOptionValue(args, index);
if (value !== "here" || parsed.bindHere !== undefined) {
parsed.help = true;
continue;
}
parsed.bindHere = true;
index += 1;
continue;
}
if (!arg.startsWith("-") && !parsed.threadId) {
parsed.threadId = arg;
continue;
}
parsed.help = true;
}
parsed.threadId = normalizeOptionalString(parsed.threadId);
parsed.host = normalizeOptionalString(parsed.host);
return parsed;
}
export function parseComputerUseArgs(args: string[]): ParsedComputerUseArgs {
const parsed: ParsedComputerUseArgs = {
action: "status",
overrides: {},
hasOverrides: false,
persistentIdentity: {},
};
let sawAction = false;
for (let index = 0; index < args.length; index += 1) {
const arg = args[index];
if (arg === "--help" || arg === "-h") {
parsed.help = true;
continue;
}
if (arg === "status" || arg === "install") {
if (sawAction) {
parsed.help = true;
continue;
}
sawAction = true;
parsed.action = arg;
continue;
}
if (arg === "--source" || arg === "--marketplace-source") {
const value = readRequiredOptionValue(args, index);
if (!value || parsed.overrides.marketplaceSource !== undefined) {
parsed.help = true;
continue;
}
parsed.overrides.marketplaceSource = value;
index += 1;
continue;
}
if (arg === "--marketplace-path" || arg === "--path") {
const value = readRequiredOptionValue(args, index);
if (!value || parsed.overrides.marketplacePath !== undefined) {
parsed.help = true;
continue;
}
parsed.overrides.marketplacePath = value;
index += 1;
continue;
}
if (arg === "--marketplace") {
const value = readRequiredOptionValue(args, index);
if (!value || parsed.overrides.marketplaceName !== undefined) {
parsed.help = true;
continue;
}
parsed.overrides.marketplaceName = value;
index += 1;
continue;
}
if (arg === "--plugin" || arg === "--server" || arg === "--mcp-server") {
const value = readRequiredOptionValue(args, index);
const configKey = arg === "--plugin" ? "pluginName" : "mcpServerName";
if (!value || parsed.persistentIdentity[configKey] !== undefined) {
parsed.help = true;
continue;
}
parsed.persistentIdentity[configKey] = value.trim();
index += 1;
continue;
}
parsed.help = true;
}
parsed.overrides = normalizeComputerUseStringOverrides(parsed.overrides);
parsed.hasOverrides = Object.values(parsed.overrides).some(Boolean);
return parsed;
}
export function formatComputerUsePersistentIdentityMigration(
parsed: ParsedComputerUseArgs,
): string {
const configPrefix = "plugins.entries.codex.config.computerUse";
const settings = [
parsed.persistentIdentity.pluginName
? `${configPrefix}.pluginName = ${JSON.stringify(parsed.persistentIdentity.pluginName)}`
: undefined,
parsed.persistentIdentity.mcpServerName
? `${configPrefix}.mcpServerName = ${JSON.stringify(parsed.persistentIdentity.mcpServerName)}`
: undefined,
].filter((setting): setting is string => Boolean(setting));
const retryArgs = [
`/codex computer-use ${parsed.action}`,
parsed.overrides.marketplaceSource
? `--source ${JSON.stringify(parsed.overrides.marketplaceSource)}`
: undefined,
parsed.overrides.marketplacePath
? `--marketplace-path ${JSON.stringify(parsed.overrides.marketplacePath)}`
: undefined,
parsed.overrides.marketplaceName
? `--marketplace ${JSON.stringify(parsed.overrides.marketplaceName)}`
: undefined,
].filter((arg): arg is string => Boolean(arg));
return [
"One-off Computer Use plugin/server overrides are no longer supported.",
`Set ${settings.join(" and ")} persistently, then rerun ${retryArgs.join(" ")}.`,
].join(" ");
}
function readRequiredOptionValue(args: string[], index: number): string | undefined {
const value = args[index + 1];
const normalized = value?.trim();
if (!normalized || normalized.startsWith("-")) {
return undefined;
}
return value;
}
function normalizeComputerUseStringOverrides(
overrides: Partial<CodexComputerUseConfig>,
): Partial<CodexComputerUseConfig> {
const normalized: Partial<CodexComputerUseConfig> = {};
const marketplaceSource = normalizeOptionalString(overrides.marketplaceSource);
if (marketplaceSource) {
normalized.marketplaceSource = marketplaceSource;
}
const marketplacePath = normalizeOptionalString(overrides.marketplacePath);
if (marketplacePath) {
normalized.marketplacePath = marketplacePath;
}
const marketplaceName = normalizeOptionalString(overrides.marketplaceName);
if (marketplaceName) {
normalized.marketplaceName = marketplaceName;
}
return normalized;
}