openclaw
Version:
Multi-channel AI gateway with extensible messaging integrations
1,861 lines • 80.7 kB
JavaScript
import { c as normalizeOptionalString } from "./string-coerce-mnp54Vah.js";
import { b as parseStrictPositiveInteger } from "./number-coercion-CJQ8TR--.js";
import { p as normalizeUniqueStringEntries } from "./string-normalization-WNUDCpXX.js";
import { y as resolveSessionAgentIds } from "./agent-scope-MrLta7Pq.js";
import { a as resolveAgentDir, s as resolveDefaultAgentDir } from "./agent-scope-config-CgCYpZfK.js";
import { n as ensureAuthProfileStore } from "./store-C8spD0DG.js";
import { i as resolveAuthProfileOrder, r as resolveAuthProfileEligibility } from "./order-CdBVYd7c.js";
import { o as resolveProfileUnusableUntilForDisplay } from "./usage-K7-k2Oys.js";
import { n as findNormalizedProviderValue } from "./model-selection-normalize-roKDxQ9_.js";
import "./number-runtime-DBLVDypr.js";
import "./string-coerce-runtime-CEGJWkQ_.js";
import "./agent-runtime-CvpQRNVf.js";
import { d as resolveCodexAppServerRuntimeOptions, o as isCodexFastServiceTier } from "./config-i2AzQJy8.js";
import { n as listCodexAppServerModels, t as listAllCodexAppServerModels } from "./models-BlR_JQQS.js";
import { t as isJsonObject } from "./protocol-oeJQu4rs.js";
import { l as summarizeCodexAccountUsage } from "./provider-BQwJpj_6.js";
import { l as writeCodexAppServerBinding, n as clearCodexAppServerBinding, o as readCodexAppServerBinding } from "./session-binding-ZGWLAhUd.js";
import { n as describeControlFailure, t as CODEX_CONTROL_METHODS } from "./capabilities-5Jb_gIw6.js";
import { c as formatAccount, d as formatComputerUseStatus, f as formatList, g as readString$1, h as formatThreads, l as formatCodexDisplayText, m as formatSkills, p as formatModels, s as buildHelp, u as formatCodexStatus } from "./notification-correlation-x98jecIj.js";
import { n as resolveCodexNativeExecutionBlock, r as resolveCodexNativeSandboxBlock } from "./sandbox-guard-C8WKv-8n.js";
import { _ as steerCodexConversationTurn, b as readCodexConversationBindingData, d as parseCodexFastModeArg, f as parseCodexPermissionsModeArg, g as setCodexConversationPermissions, h as setCodexConversationModel, m as setCodexConversationFastMode, o as formatCodexCliSessions, p as readCodexConversationActiveTurn, r as startCodexConversationThread, u as formatPermissionsMode, v as stopCodexConversationTurn, x as resolveCodexDefaultWorkspaceDir, y as createCodexCliNodeConversationBindingData } from "./conversation-binding-WelcQgJk.js";
import { t as requestCodexAppServerJson } from "./request-D-miWXFr.js";
import { n as installCodexComputerUse, r as readCodexComputerUseStatus } from "./computer-use-BpJYn65K.js";
import { n as rememberCodexRateLimits } from "./rate-limit-cache-C7qmZ0Jh.js";
import crypto from "node:crypto";
//#region extensions/codex/src/command-account.ts
const OPENAI_PROVIDER_ID = "openai";
const OPENAI_CODEX_PROVIDER_ID = OPENAI_PROVIDER_ID;
async function readCodexAccountAuthOverview(params) {
const config = params.ctx.config;
const store = ensureAuthProfileStore(resolveDefaultAgentDir(config), {
allowKeychainPrompt: false,
config
});
const { order, explicit: explicitOrder } = resolveDisplayAuthOrder({
config,
store
});
if (order.length === 0) return;
const now = Date.now();
const activeProfileId = resolveActiveProfileId({
store,
order,
explicitOrder,
config,
account: params.account,
limits: params.limits,
now
});
const subscriptionProfileId = order.find((profileId) => isChatGptSubscriptionProfile(store.profiles[profileId]));
const activeIsSubscription = activeProfileId !== void 0 && isChatGptSubscriptionProfile(store.profiles[activeProfileId]);
const activeUsage = activeIsSubscription && params.limits.ok ? summarizeCodexAccountUsage(params.limits.value, now) : void 0;
const subscriptionUsage = subscriptionProfileId && (!activeIsSubscription || subscriptionProfileId !== activeProfileId) ? await readSubscriptionUsage({
...params,
config,
subscriptionProfileId,
now
}) : activeUsage;
if (!params.account.ok && !params.limits.ok && !subscriptionUsage) return;
const rows = order.map((profileId, index) => buildProfileRow({
store,
config,
profileId,
activeProfileId,
activeIndex: activeProfileId ? order.indexOf(activeProfileId) : -1,
index,
now,
usage: profileId === subscriptionProfileId ? subscriptionUsage : void 0
}));
const activeRow = rows.find((row) => row.active);
if (!activeRow) return {
currentLine: "OpenAI credentials: no working credential",
orderTitle: "Auth order",
rows
};
const activeIsApiKey = store.profiles[activeRow.profileId]?.type === "api_key";
const subscriptionLabel = subscriptionProfileId ? formatProfileLabel(subscriptionProfileId, store.profiles[subscriptionProfileId]) : activeIsSubscription ? activeRow.label : void 0;
const subscriptionUsageLine = formatSubscriptionUsageLine(subscriptionUsage);
return {
...activeIsApiKey ? { currentLine: buildApiKeyActiveLine(activeRow, subscriptionUsage) } : {},
...subscriptionLabel ? { subscriptionLabel } : {},
...subscriptionUsageLine ? { subscriptionUsage: subscriptionUsageLine } : {},
orderTitle: "Auth order",
rows
};
}
function resolveDisplayAuthOrder(params) {
const codexOrder = resolveOrder(params.store.order, OPENAI_CODEX_PROVIDER_ID) ?? resolveOrder(params.config?.auth?.order, OPENAI_CODEX_PROVIDER_ID);
if (codexOrder && codexOrder.length > 0) return {
order: normalizeUniqueStringEntries(codexOrder),
explicit: true
};
return {
order: resolveAuthProfileOrder({
cfg: params.config,
store: params.store,
provider: OPENAI_CODEX_PROVIDER_ID
}),
explicit: hasExplicitOpenAiAuthOrder(params)
};
}
function hasExplicitOpenAiAuthOrder(params) {
const sources = [params.store.order, params.config?.auth?.order];
for (const source of sources) {
const codex = resolveOrder(source, OPENAI_CODEX_PROVIDER_ID);
if (codex && codex.length > 0) return true;
const openai = resolveOrder(source, OPENAI_PROVIDER_ID);
if (openai && openai.length > 0) return true;
}
return false;
}
function resolveOrder(order, provider) {
return findNormalizedProviderValue(order, provider);
}
function resolveActiveProfileId(params) {
const liveProfileId = resolveLiveAccountProfileId({
account: params.account,
store: params.store,
order: params.order
});
if (liveProfileId) return liveProfileId;
if (params.explicitOrder) return params.order.find((profileId) => isActiveProfileCandidate(params, profileId) && resolveAuthProfileEligibility({
cfg: params.config,
store: params.store,
provider: OPENAI_CODEX_PROVIDER_ID,
profileId,
now: params.now
}).eligible);
const lastGood = [params.store.lastGood?.[OPENAI_PROVIDER_ID], params.store.lastGood?.[OPENAI_CODEX_PROVIDER_ID]].find((profileId) => typeof profileId === "string" && params.order.includes(profileId) && isActiveProfileCandidate(params, profileId));
if (lastGood) return lastGood;
const mostRecent = params.order.map((profileId) => ({
profileId,
lastUsed: params.store.usageStats?.[profileId]?.lastUsed ?? 0
})).filter((entry) => entry.lastUsed > 0 && isActiveProfileCandidate(params, entry.profileId)).toSorted((left, right) => right.lastUsed - left.lastUsed)[0]?.profileId;
if (mostRecent) return mostRecent;
if (shouldInferApiKeyActiveFromRateLimitProbe(params.limits)) {
const apiKeyProfile = params.order.find((profileId) => params.store.profiles[profileId]?.type === "api_key");
if (apiKeyProfile) return apiKeyProfile;
}
return resolveAuthProfileOrder({
cfg: params.config,
store: params.store,
provider: OPENAI_CODEX_PROVIDER_ID
})[0];
}
function isActiveProfileCandidate(params, profileId) {
return !isActiveUntil(resolveProfileUnusableUntilForDisplay(params.store, profileId) ?? void 0, params.now);
}
function resolveLiveAccountProfileId(params) {
if (!params.account.ok || !isJsonObject(params.account.value)) return;
const account = isJsonObject(params.account.value.account) ? params.account.value.account : params.account.value;
const type = readString(account, "type")?.toLowerCase();
if (type === "chatgpt") {
const email = readString(account, "email")?.toLowerCase();
const firstSubscription = params.order.find((profileId) => isChatGptSubscriptionProfile(params.store.profiles[profileId]));
if (!email) return firstSubscription;
return params.order.find((profileId) => {
const credential = params.store.profiles[profileId];
if (!isChatGptSubscriptionProfile(credential)) return false;
return (credential.email?.trim().toLowerCase() ?? extractEmailFromProfileId(profileId))?.toLowerCase() === email;
}) ?? firstSubscription;
}
if (type === "apikey" || type === "api_key") return params.order.find((profileId) => params.store.profiles[profileId]?.type === "api_key");
}
function shouldInferApiKeyActiveFromRateLimitProbe(limits) {
return !limits.ok && limits.error.toLowerCase().includes("chatgpt authentication required");
}
async function readSubscriptionUsage(params) {
const limits = await params.safeCodexControlRequest(params.pluginConfig, CODEX_CONTROL_METHODS.rateLimits, void 0, {
config: params.config,
authProfileId: params.subscriptionProfileId,
isolated: true
});
if (!limits.ok) return;
rememberCodexRateLimits(limits.value);
return summarizeCodexAccountUsage(limits.value, params.now);
}
function buildProfileRow(params) {
const credential = params.store.profiles[params.profileId];
const label = formatProfileLabel(params.profileId, credential);
const kind = formatProfileKind(credential);
const active = params.profileId === params.activeProfileId;
const status = active ? "active now" : params.usage?.blocked ? formatUsageBlockedStatus(params.usage) : describeInactiveProfileStatus({
store: params.store,
config: params.config,
profileId: params.profileId,
credential,
now: params.now,
afterActive: params.activeIndex >= 0 && params.index > params.activeIndex
});
return {
profileId: params.profileId,
label,
kind,
status,
active,
...credential?.type === "api_key" && active ? { billingNote: "billed per token" } : {},
...params.usage?.usageLine ? { usage: params.usage.usageLine } : {}
};
}
function formatUsageBlockedStatus(usage) {
return usage.blocked ? "rate-limited" : "available if needed";
}
function describeInactiveProfileStatus(params) {
const stats = params.store.usageStats?.[params.profileId];
const blockedUntil = stats?.blockedUntil;
if (isActiveUntil(blockedUntil, params.now)) return `rate-limited - resets ${formatRelativeReset(blockedUntil, params.now)}`;
if (isActiveUntil(resolveProfileUnusableUntilForDisplay(params.store, params.profileId) ?? void 0, params.now)) return describeFailureStatus(stats?.disabledReason ?? stats?.cooldownReason, params.credential);
const eligibility = resolveAuthProfileEligibility({
cfg: params.config,
store: params.store,
provider: OPENAI_CODEX_PROVIDER_ID,
profileId: params.profileId,
now: params.now
});
if (!eligibility.eligible) return describeEligibilityStatus(eligibility.reasonCode, params.credential);
return "available if needed";
}
function buildApiKeyActiveLine(activeRow, subscriptionUsage) {
if (subscriptionUsage?.blocked) {
const switchBack = subscriptionUsage.blockedResetRelative ? ` · switches back ${subscriptionUsage.blockedResetRelative}` : " · switches back automatically";
return `Now using: ${activeRow.label} - subscription rate-limited${switchBack}`;
}
return `Now using: ${activeRow.label} - subscription unavailable · switches back automatically`;
}
function formatSubscriptionUsageLine(usage) {
if (!usage) return;
const parts = usage.usageLine ? [formatUsageLineForDisplay(usage.usageLine)] : [];
if (usage.blockedResetRelative) parts.push(`Resets ${usage.blockedResetRelative}`);
return parts.length > 0 ? parts.join(" · ") : void 0;
}
function formatUsageLineForDisplay(value) {
return value.replace(/^weekly\b/u, "Weekly").replace(/\bshort-term\b/u, "Short-term");
}
function readString(record, key) {
const value = record[key];
return typeof value === "string" && value.trim() ? value.trim() : void 0;
}
function isChatGptSubscriptionProfile(credential) {
return credential?.type === "oauth" || credential?.type === "token";
}
function formatProfileKind(credential) {
if (!credential) return "credential";
if (isChatGptSubscriptionProfile(credential)) return "ChatGPT subscription";
if (credential.type === "api_key") return "API key";
return "credential";
}
function formatProfileLabel(profileId, credential) {
const tail = profileId.includes(":") ? profileId.slice(profileId.indexOf(":") + 1) : profileId;
const displayName = credential?.displayName?.trim();
if (displayName) return credential?.type === "api_key" ? simplifyApiKeyDisplayName(displayName, tail) : displayName;
const email = credential?.email?.trim() ?? extractEmailFromProfileId(profileId);
if (email) return email;
if (credential?.type === "api_key") return tail || "API key";
return humanizeProfileTail(tail);
}
function simplifyApiKeyDisplayName(value, tail) {
const stripped = value.replace(/^OpenAI\s+/iu, "").trim();
if (tail && stripped.toLowerCase() === humanizeApiKeyProfileTail(tail).toLowerCase()) return tail;
return stripped || value;
}
function humanizeApiKeyProfileTail(tail) {
const words = splitProfileTail(tail);
const hasBackup = words.includes("backup");
return [
words.filter((word) => word !== "api" && word !== "key" && word !== "backup").map(titleCase).join(" "),
"API key",
hasBackup ? "backup" : ""
].filter(Boolean).join(" ");
}
function humanizeProfileTail(tail) {
const words = splitProfileTail(tail);
return words.length > 0 ? words.map(titleCase).join(" ") : tail;
}
function splitProfileTail(tail) {
return tail.replace(/[_\s]+/gu, "-").split("-").map((word) => word.trim().toLowerCase()).filter(Boolean);
}
function titleCase(value) {
return value ? `${value[0]?.toUpperCase() ?? ""}${value.slice(1)}` : value;
}
function extractEmailFromProfileId(profileId) {
const tail = profileId.includes(":") ? profileId.slice(profileId.indexOf(":") + 1) : profileId;
return /^[^\s@<>()[\]`]+@[^\s@<>()[\]`]+\.[^\s@<>()[\]`]+$/.test(tail) ? tail : void 0;
}
function describeFailureStatus(reason, credential) {
if (reason === "auth" || reason === "auth_permanent" || reason === "session_expired") return credential?.type === "api_key" ? "auth failed - check key" : "sign-in expired";
if (reason === "billing") return "billing unavailable";
if (reason === "rate_limit") return "rate-limited";
return "temporarily unavailable";
}
function describeEligibilityStatus(reason, credential) {
if (reason === "profile_missing" || reason === "missing_credential") return credential?.type === "api_key" ? "not configured" : "sign-in required";
if (reason === "expired" || reason === "invalid_expires") return "sign-in expired";
if (reason === "unresolved_ref") return "credential unavailable";
if (reason === "provider_mismatch") return "wrong provider";
if (reason === "mode_mismatch") return "wrong credential type";
return "unavailable";
}
function isActiveUntil(value, now) {
return typeof value === "number" && Number.isFinite(value) && value > now;
}
function formatRelativeReset(untilMs, nowMs) {
const durationMs = Math.max(1e3, untilMs - nowMs);
const minuteMs = 6e4;
const hourMs = 60 * minuteMs;
const dayMs = 24 * hourMs;
if (durationMs < hourMs) {
const minutes = Math.ceil(durationMs / minuteMs);
return `in ${minutes} ${minutes === 1 ? "minute" : "minutes"}`;
}
if (durationMs < dayMs) {
const hours = Math.ceil(durationMs / hourMs);
return `in ${hours} ${hours === 1 ? "hour" : "hours"}`;
}
const days = Math.ceil(durationMs / dayMs);
return `in ${days} ${days === 1 ? "day" : "days"}`;
}
//#endregion
//#region extensions/codex/src/command-presentation.ts
function buildCodexCommandPickerPresentation(title, prompt, buttons) {
return {
title,
blocks: [{
type: "text",
text: prompt
}, {
type: "buttons",
buttons: buttons.map((button) => ({
label: button.label,
action: {
type: "command",
command: button.command
}
}))
}]
};
}
//#endregion
//#region extensions/codex/src/command-plugins-management.ts
const POLICY_REFRESH_HINT = "New Codex conversations pick this up automatically. Use /new or /reset to refresh the current one.";
async function handleCodexPluginsSubcommand(ctx, rest, io) {
const [verb = "list", ...args] = rest;
const normalized = verb.toLowerCase();
if (normalized === "menu") {
if (args.length > 0) return { text: "Usage: /codex plugins menu" };
return buildPluginsMenuReply();
}
if (normalized === "help") {
if (args.length > 0) return { text: "Usage: /codex plugins help" };
return { text: buildPluginsHelp() };
}
if (normalized === "list") {
if (args.length > 0) return { text: "Usage: /codex plugins list" };
const current = await io.readConfig();
return { text: formatPluginList(current.plugins ?? {}, { globalEnabled: current.enabled === true }) };
}
const target = args[0];
if (normalized === "enable" || normalized === "disable") {
if (args.length === 0) return buildPluginNamePickerReply(normalized, await io.readConfig());
if (!target || args.length > 1) return { text: `Usage: /codex plugins ${normalized} <name>` };
if (!canMutateCodexPlugins(ctx)) return { text: `Only an owner or operator.admin gateway client can run /codex plugins ${normalized}.` };
const wantEnabled = normalized === "enable";
if (!((await io.readConfig()).plugins ?? {})[target]) return { text: `Codex sub-plugin '${formatCodexDisplayText(target)}' is not configured. Run '/codex plugins list' to see configured plugins.` };
await io.mutate((block) => {
if (wantEnabled) block.enabled = true;
block.plugins ??= {};
block.plugins[target] = {
...block.plugins[target],
enabled: wantEnabled
};
});
return { text: `${formatCodexDisplayText(target)}: ${wantEnabled ? "enabled" : "disabled"} in openclaw.json. ${POLICY_REFRESH_HINT}` };
}
return { text: `Unknown /codex plugins subcommand: ${formatCodexDisplayText(verb)}\n\n${buildPluginsHelp()}` };
}
function buildPluginsMenuReply() {
return {
text: [
"Codex sub-plugins. Pick a sub-action or type:",
"",
" 1. /codex plugins list",
" 2. /codex plugins enable",
" 3. /codex plugins disable",
" 4. /codex plugins help",
"",
"Type '/codex' to go back to the main menu."
].join("\n"),
presentation: buildCodexCommandPickerPresentation("Codex sub-plugins", "Pick a Codex sub-plugin action:", [
{
label: "list",
command: "/codex plugins list"
},
{
label: "enable",
command: "/codex plugins enable"
},
{
label: "disable",
command: "/codex plugins disable"
},
{
label: "help",
command: "/codex plugins help"
},
{
label: "back",
command: "/codex"
}
])
};
}
function buildPluginNamePickerReply(verb, current) {
const globalEnabled = current.enabled === true;
const eligible = Object.entries(current.plugins ?? {}).toSorted(([left], [right]) => left.localeCompare(right)).filter(([, entry]) => {
const effectivelyEnabled = globalEnabled && entry.enabled !== false;
return verb === "disable" ? effectivelyEnabled : !effectivelyEnabled;
});
if (eligible.length === 0) return {
text: [
`No configured ${verb === "enable" ? "disabled" : "enabled"} Codex sub-plugins found.`,
"",
"Type '/codex plugins list' to inspect configured sub-plugins.",
"Type '/codex plugins menu' to go back to the plugins menu."
].join("\n"),
presentation: buildCodexCommandPickerPresentation("Codex sub-plugins", "Pick another Codex sub-plugin action:", [{
label: "list",
command: "/codex plugins list"
}, {
label: "back",
command: "/codex plugins menu"
}])
};
const buttons = [...eligible.map(([key]) => ({
label: formatCodexDisplayText(key),
command: `/codex plugins ${verb} ${key}`
})), {
label: "back",
command: "/codex plugins menu"
}];
return {
text: [
`Codex sub-plugins to ${verb}. Pick one or type:`,
"",
...eligible.map(([key], index) => ` ${index + 1}. /codex plugins ${verb} ${key}`),
"",
...verb === "enable" && !globalEnabled ? ["Global codexPlugins.enabled is off; enabling one configured sub-plugin turns it on.", ""] : [],
"Type '/codex plugins menu' to go back to the plugins menu."
].join("\n"),
presentation: buildCodexCommandPickerPresentation("Codex sub-plugins", `Pick a Codex sub-plugin to ${verb}:`, buttons)
};
}
function canMutateCodexPlugins(ctx) {
if (ctx.senderIsOwner === true) return true;
return ctx.gatewayClientScopes?.includes("operator.admin") === true;
}
function buildPluginsHelp() {
return [
"Codex sub-plugin management (writes only to ~/.openclaw/openclaw.json, never to ~/.codex/config.toml):",
"- /codex plugins (alias for list)",
"- /codex plugins list show all configured Codex sub-plugins",
"- /codex plugins enable <name> enable a configured sub-plugin",
"- /codex plugins disable <name> disable a configured sub-plugin"
].join("\n");
}
function formatPluginList(plugins, options = {}) {
const globalEnabled = options.globalEnabled === true;
const keys = Object.keys(plugins).toSorted();
if (keys.length === 0) return "No Codex sub-plugins configured under plugins.entries.codex.config.codexPlugins.plugins";
const rows = keys.map((key) => {
const entry = plugins[key] ?? {};
const state = globalEnabled && entry.enabled !== false ? "ON " : "OFF";
return {
displayKey: formatCodexDisplayText(key),
state,
pluginName: formatCodexDisplayText(entry.pluginName ?? key),
marketplace: formatCodexDisplayText(entry.marketplaceName ?? "?")
};
});
const keyW = Math.max(...rows.map((r) => r.displayKey.length));
const pluginW = Math.max(...rows.map((r) => r.pluginName.length));
return [
"Codex sub-plugins in Openclaw config (~/.openclaw/openclaw.json):",
"",
...rows.map((r) => ` ${r.state} ${r.displayKey.padEnd(keyW)} ${r.pluginName.padEnd(pluginW)} [${r.marketplace}]`),
"",
...globalEnabled ? [] : ["Global codexPlugins.enabled is off; configured sub-plugins are inactive.", ""],
"New Codex conversations pick up policy changes automatically; /new or /reset to refresh the current one."
].join("\n");
}
//#endregion
//#region extensions/codex/src/command-rpc.ts
function requestOptions(pluginConfig, limit, config) {
const runtime = resolveCodexAppServerRuntimeOptions({ pluginConfig });
return {
limit,
timeoutMs: runtime.requestTimeoutMs,
startOptions: runtime.start,
config
};
}
async function codexControlRequest(pluginConfig, method, requestParams, options = {}) {
const runtime = resolveCodexAppServerRuntimeOptions({ pluginConfig });
return await requestCodexAppServerJson({
method,
requestParams,
timeoutMs: runtime.requestTimeoutMs,
startOptions: runtime.start,
config: options.config,
sessionKey: options.sessionKey,
sessionId: options.sessionId,
authProfileId: options.authProfileId,
agentDir: options.agentDir,
isolated: options.isolated
});
}
async function safeCodexControlRequest(pluginConfig, method, requestParams, options = {}) {
return await safeValue(async () => await codexControlRequest(pluginConfig, method, requestParams, options));
}
async function safeCodexModelList(pluginConfig, limit, config) {
return await safeValue(async () => await listCodexAppServerModels(requestOptions(pluginConfig, limit, config)));
}
async function readCodexStatusProbes(pluginConfig, config) {
const [models, account, limits, mcps, skills] = await Promise.all([
safeCodexModelList(pluginConfig, 20, config),
safeCodexControlRequest(pluginConfig, CODEX_CONTROL_METHODS.account, { refreshToken: false }, { config }),
safeCodexControlRequest(pluginConfig, CODEX_CONTROL_METHODS.rateLimits, void 0, { config }),
safeCodexControlRequest(pluginConfig, CODEX_CONTROL_METHODS.listMcpServers, { limit: 100 }, { config }),
safeCodexControlRequest(pluginConfig, CODEX_CONTROL_METHODS.listSkills, {}, { config })
]);
return {
models,
account,
limits,
mcps,
skills
};
}
async function safeValue(read) {
try {
return {
ok: true,
value: await read()
};
} catch (error) {
return {
ok: false,
error: describeControlFailure(error)
};
}
}
//#endregion
//#region extensions/codex/src/command-handlers.ts
const defaultCodexCommandDeps = {
codexControlRequest,
listCodexAppServerModels: listAllCodexAppServerModels,
readCodexStatusProbes,
readCodexAppServerBinding,
requestOptions,
safeCodexControlRequest,
writeCodexAppServerBinding,
clearCodexAppServerBinding,
readCodexComputerUseStatus,
installCodexComputerUse,
resolveCodexDefaultWorkspaceDir,
startCodexConversationThread,
readCodexConversationActiveTurn,
setCodexConversationFastMode,
setCodexConversationModel,
setCodexConversationPermissions,
steerCodexConversationTurn,
stopCodexConversationTurn,
listCodexCliSessionsOnNode: async () => {
throw new Error("Codex CLI node sessions require Gateway node runtime.");
},
resolveCodexCliSessionForBindingOnNode: async () => {
throw new Error("Codex CLI node sessions require Gateway node runtime.");
}
};
const CODEX_DIAGNOSTICS_SOURCE = "openclaw-diagnostics";
const CODEX_DIAGNOSTICS_REASON_MAX_CHARS = 2048;
const CODEX_DIAGNOSTICS_COOLDOWN_MS = 6e4;
const CODEX_DIAGNOSTICS_ERROR_MAX_CHARS = 500;
const CODEX_DIAGNOSTICS_COOLDOWN_MAX_THREADS = 100;
const CODEX_DIAGNOSTICS_COOLDOWN_MAX_SCOPES = 100;
const CODEX_DIAGNOSTICS_CONFIRMATION_TTL_MS = 5 * 6e4;
const CODEX_DIAGNOSTICS_CONFIRMATION_MAX_REQUESTS_PER_SCOPE = 100;
const CODEX_DIAGNOSTICS_CONFIRMATION_MAX_SCOPES = 100;
const CODEX_DIAGNOSTICS_SCOPE_FIELD_MAX_CHARS = 128;
const CODEX_RESUME_SAFE_THREAD_ID_PATTERN = /^[A-Za-z0-9._:-]+$/;
const CODEX_NATIVE_EXECUTION_SUBCOMMANDS = new Set([
"bind",
"resume",
"steer",
"model",
"fast",
"permissions",
"compact",
"review"
]);
const lastCodexDiagnosticsUploadByThread = /* @__PURE__ */ new Map();
const lastCodexDiagnosticsUploadByScope = /* @__PURE__ */ new Map();
const pendingCodexDiagnosticsConfirmations = /* @__PURE__ */ new Map();
const pendingCodexDiagnosticsConfirmationTokensByScope = /* @__PURE__ */ new Map();
/**
* No-arg `/codex` picker. Codex owns the command tree; channels render the
* portable command actions as inline controls when their transport can.
*/
function buildCodexSubcommandPickerReply() {
const verbs = [
{
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"
}
];
return {
text: [
"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."
].join("\n"),
presentation: buildCodexCommandPickerPresentation("Codex commands", "Pick a Codex subcommand:", verbs)
};
}
/** Sub-picker for `/codex fast menu` (on / off / status). */
function buildCodexFastMenuReply() {
const modes = [
"on",
"off",
"status"
];
const buttons = [...modes.map((mode) => ({
label: mode,
command: `/codex fast ${mode}`
})), {
label: "back",
command: "/codex"
}];
return {
text: [
"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."
].join("\n"),
presentation: buildCodexCommandPickerPresentation("Codex fast mode", "Pick a Codex fast mode:", buttons)
};
}
/** Sub-picker for `/codex permissions menu` (default / yolo / status). */
function buildCodexPermissionsMenuReply() {
const modes = [
"default",
"yolo",
"status"
];
const buttons = [...modes.map((mode) => ({
label: mode,
command: `/codex permissions ${mode}`
})), {
label: "back",
command: "/codex"
}];
return {
text: [
"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."
].join("\n"),
presentation: buildCodexCommandPickerPresentation("Codex permissions", "Pick a Codex permissions mode:", buttons)
};
}
/** Sub-picker for `/codex computer-use menu` (status / install). */
function buildCodexComputerUseMenuReply() {
const actions = ["status", "install"];
const buttons = [...actions.map((action) => ({
label: action,
command: `/codex computer-use ${action}`
})), {
label: "back",
command: "/codex"
}];
return {
text: [
"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."
].join("\n"),
presentation: buildCodexCommandPickerPresentation("Codex computer-use", "Pick a Codex computer-use action:", buttons)
};
}
/** Returns true when the rest-args are exactly `["menu"]` (case-insensitive). */
function isMenuVerb(rest) {
return rest.length === 1 && (rest[0] ?? "").trim().toLowerCase() === "menu";
}
async function handleCodexSubcommand(ctx, options) {
const deps = {
...defaultCodexCommandDeps,
...options.deps
};
const args = splitArgs(ctx.args);
if (args.length === 0) return buildCodexSubcommandPickerReply();
const [subcommand = "status", ...rest] = args;
const normalized = subcommand.toLowerCase();
if (normalized === "help") return { text: buildHelp() };
const sandboxBlock = resolveCodexNativeCommandSandboxBlock(ctx, normalized, rest);
if (sandboxBlock) return { text: sandboxBlock };
if (normalized === "plugins") {
if (!deps.codexPluginsManagementIo) return { text: "Codex sub-plugin management is not wired up (codexPluginsManagementIo dep is undefined). Edit ~/.openclaw/openclaw.json or use `openclaw config patch` until the runtime exposes the IO." };
return await handleCodexPluginsSubcommand(ctx, rest, deps.codexPluginsManagementIo);
}
if (normalized === "status") {
if (rest.length > 0) return { text: "Usage: /codex status" };
return { text: formatCodexStatus(await deps.readCodexStatusProbes(options.pluginConfig, ctx.config)) };
}
if (normalized === "models") {
if (rest.length > 0) return { text: "Usage: /codex models" };
return { text: formatModels(await deps.listCodexAppServerModels(deps.requestOptions(options.pluginConfig, 100, ctx.config))) };
}
if (normalized === "threads") return { text: await buildThreads(deps, options.pluginConfig, rest.join(" ")) };
if (normalized === "sessions") return { text: await buildCodexCliSessions(deps, rest) };
if (normalized === "resume") return { text: await resumeThread(deps, ctx, options.pluginConfig, rest) };
if (normalized === "bind") return await bindConversation(deps, ctx, options.pluginConfig, rest);
if (normalized === "detach" || normalized === "unbind") {
if (rest.length > 0) return { text: "Usage: /codex detach" };
return { text: await detachConversation(deps, ctx) };
}
if (normalized === "binding") {
if (rest.length > 0) return { text: "Usage: /codex binding" };
return { text: await describeConversationBinding(deps, ctx) };
}
if (normalized === "stop") {
if (rest.length > 0) return { text: "Usage: /codex stop" };
return { text: await stopConversationTurn(deps, ctx, options.pluginConfig) };
}
if (normalized === "steer") return { text: await steerConversationTurn(deps, ctx, options.pluginConfig, rest.join(" ")) };
if (normalized === "model") return { text: await setConversationModel(deps, ctx, options.pluginConfig, rest) };
if (normalized === "fast") {
if (isMenuVerb(rest)) return buildCodexFastMenuReply();
return { text: await setConversationFastMode(deps, ctx, options.pluginConfig, rest) };
}
if (normalized === "permissions") {
if (isMenuVerb(rest)) return buildCodexPermissionsMenuReply();
return { text: await setConversationPermissions(deps, ctx, options.pluginConfig, rest) };
}
if (normalized === "compact") return { text: await startThreadAction(deps, ctx, options.pluginConfig, CODEX_CONTROL_METHODS.compact, "compaction", rest) };
if (normalized === "review") return { text: await startThreadAction(deps, ctx, options.pluginConfig, CODEX_CONTROL_METHODS.review, "review", rest) };
if (normalized === "diagnostics") return await handleCodexDiagnosticsFeedback(deps, ctx, options.pluginConfig, rest.join(" "), "/codex diagnostics");
if (normalized === "computer-use" || normalized === "computeruse") {
if (isMenuVerb(rest)) return buildCodexComputerUseMenuReply();
return { text: await handleComputerUseCommand(deps, options.pluginConfig, rest) };
}
if (normalized === "mcp") {
if (rest.length > 0) return { text: "Usage: /codex mcp" };
return { text: formatList(await deps.codexControlRequest(options.pluginConfig, CODEX_CONTROL_METHODS.listMcpServers, { limit: 100 }), "MCP servers") };
}
if (normalized === "skills") {
if (rest.length > 0) return { text: "Usage: /codex skills" };
return { text: formatSkills(await deps.codexControlRequest(options.pluginConfig, CODEX_CONTROL_METHODS.listSkills, {})) };
}
if (normalized === "account") {
if (rest.length > 0) return { text: "Usage: /codex account" };
const [account, limits] = await Promise.all([deps.safeCodexControlRequest(options.pluginConfig, CODEX_CONTROL_METHODS.account, { refreshToken: false }), deps.safeCodexControlRequest(options.pluginConfig, CODEX_CONTROL_METHODS.rateLimits, void 0)]);
if (limits.ok) rememberCodexRateLimits(limits.value);
return { text: formatAccount(account, limits, await readCodexAccountAuthOverview({
ctx,
pluginConfig: options.pluginConfig,
safeCodexControlRequest: deps.safeCodexControlRequest,
account,
limits
})) };
}
return { text: `Unknown Codex command: ${formatCodexDisplayText(subcommand)}\n\n${buildHelp()}` };
}
function resolveCodexNativeCommandSandboxBlock(ctx, subcommand, args) {
if (!CODEX_NATIVE_EXECUTION_SUBCOMMANDS.has(subcommand)) return;
if (returnsBeforeNativeCodexExecution(subcommand, args)) return;
if (isCodexCliNodeResumeBind(subcommand, args)) return resolveCodexNativeSandboxBlock({
config: ctx.config,
sessionKey: ctx.sessionKey,
sessionId: ctx.sessionId,
surface: `/${["codex", subcommand].join(" ")}`
});
return resolveCodexNativeExecutionBlock({
config: ctx.config,
sessionKey: ctx.sessionKey,
sessionId: ctx.sessionId,
surface: `/${["codex", subcommand].join(" ")}`
});
}
function returnsBeforeNativeCodexExecution(subcommand, args) {
switch (subcommand) {
case "bind": return parseBindArgs([...args]).help === true;
case "resume": return returnsBeforeNativeCodexResume(args);
case "steer": return args.join(" ").trim() === "";
case "model": return args.length === 0 || args.length > 1;
case "fast": return args.length === 0 || args.length > 1 || parseCodexFastModeArg(args[0]) === void 0;
case "permissions": return args.length === 0 || args.length > 1 || parseCodexPermissionsModeArg(args[0]) === void 0;
case "compact":
case "review":
case "stop": return args.length > 0;
default: return false;
}
}
function isCodexCliNodeResumeBind(subcommand, args) {
if (subcommand !== "resume") return false;
const parsed = parseResumeArgs([...args]);
return Boolean(parsed.host && parsed.threadId && parsed.bindHere === true && !parsed.help);
}
function returnsBeforeNativeCodexResume(args) {
const parsed = parseResumeArgs([...args]);
const normalizedThreadId = parsed.threadId?.trim();
if (parsed.help) return true;
if (parsed.host) return !normalizedThreadId || parsed.bindHere !== true;
return !normalizedThreadId || args.length !== 1;
}
async function handleComputerUseCommand(deps, pluginConfig, args) {
const parsed = parseComputerUseArgs(args);
if (parsed.help) return ["Usage: /codex computer-use [status|install] [--source <marketplace-source>] [--marketplace-path <path>] [--marketplace <name>]", "Checks or installs the configured Codex Computer Use plugin through app-server."].join("\n");
const params = {
pluginConfig,
forceEnable: parsed.action === "install" || parsed.hasOverrides,
...Object.keys(parsed.overrides).length > 0 ? { overrides: parsed.overrides } : {}
};
if (parsed.action === "install") return formatComputerUseStatus(await deps.installCodexComputerUse(params));
return formatComputerUseStatus(await deps.readCodexComputerUseStatus(params));
}
async function bindConversation(deps, ctx, pluginConfig, args) {
const parsed = parseBindArgs(args);
if (parsed.help) return { text: "Usage: /codex bind [thread-id] [--cwd <path>] [--model <model>] [--provider <provider>]" };
if (!ctx.sessionFile) return { text: "Cannot bind Codex because this command did not include an OpenClaw session file." };
const scope = resolveCodexConversationControlScope(ctx);
const workspaceDir = parsed.cwd ?? deps.resolveCodexDefaultWorkspaceDir(pluginConfig);
const authProfileId = (await deps.readCodexAppServerBinding(ctx.sessionFile, {
agentDir: scope.agentDir,
config: ctx.config
}))?.authProfileId;
const startParams = {
pluginConfig,
config: ctx.config,
sessionFile: ctx.sessionFile,
workspaceDir,
agentDir: scope.agentDir,
sessionKey: ctx.sessionKey,
threadId: parsed.threadId,
model: parsed.model,
modelProvider: parsed.provider
};
if (authProfileId) startParams.authProfileId = authProfileId;
const data = await deps.startCodexConversationThread(startParams);
const threadId = (await deps.readCodexAppServerBinding(ctx.sessionFile, {
agentDir: scope.agentDir,
config: ctx.config
}))?.threadId ?? parsed.threadId ?? "new thread";
const summary = `Codex app-server thread ${formatCodexDisplayText(threadId)} in ${formatCodexDisplayText(workspaceDir)}`;
let request;
try {
request = await ctx.requestConversationBinding({
summary,
detachHint: "/codex detach",
data
});
} catch (error) {
await deps.clearCodexAppServerBinding(ctx.sessionFile);
throw error;
}
if (request.status === "bound") return { text: `Bound this conversation to Codex thread ${formatCodexDisplayText(threadId)} in ${formatCodexDisplayText(workspaceDir)}.` };
if (request.status === "pending") return request.reply;
await deps.clearCodexAppServerBinding(ctx.sessionFile);
return { text: formatCodexDisplayText(request.message) };
}
async function detachConversation(deps, ctx) {
const data = readCodexConversationBindingData(await ctx.getCurrentConversationBinding());
const detached = await ctx.detachConversationBinding();
if (data?.kind === "codex-app-server-session") await deps.clearCodexAppServerBinding(data.sessionFile);
else if (ctx.sessionFile) await deps.clearCodexAppServerBinding(ctx.sessionFile);
return detached.removed ? "Detached this conversation from Codex." : "No Codex conversation binding was attached.";
}
async function describeConversationBinding(deps, ctx) {
const current = await ctx.getCurrentConversationBinding();
const data = readCodexConversationBindingData(current);
if (!current || !data) return "No Codex conversation binding is attached.";
if (data.kind === "codex-cli-node-session") return [
"Codex conversation binding:",
"- Mode: Codex CLI node session",
`- Node: ${formatCodexDisplayText(data.nodeId)}`,
`- Session: ${formatCodexDisplayText(data.sessionId)}`,
`- Workspace: ${formatCodexDisplayText(data.cwd ?? "unknown")}`,
"- Active run: not tracked"
].join("\n");
const threadBinding = await deps.readCodexAppServerBinding(data.sessionFile, {
agentDir: data.agentDir,
config: ctx.config
});
const active = deps.readCodexConversationActiveTurn(data.sessionFile);
return [
"Codex conversation binding:",
`- Thread: ${formatCodexDisplayText(threadBinding?.threadId ?? "unknown")}`,
`- Workspace: ${formatCodexDisplayText(data.workspaceDir)}`,
`- Model: ${formatCodexDisplayText(threadBinding?.model ?? "default")}`,
`- Fast: ${isCodexFastServiceTier(threadBinding?.serviceTier) ? "on" : "off"}`,
`- Permissions: ${threadBinding ? formatPermissionsMode(threadBinding) : "default"}`,
`- Active run: ${formatCodexDisplayText(active ? active.turnId : "none")}`,
`- Session: ${formatCodexDisplayText(data.sessionFile)}`
].join("\n");
}
async function buildThreads(deps, pluginConfig, filter) {
return formatThreads(await deps.codexControlRequest(pluginConfig, CODEX_CONTROL_METHODS.listThreads, {
limit: 10,
...filter.trim() ? { searchTerm: filter.trim() } : {}
}));
}
async function buildCodexCliSessions(deps, args) {
const parsed = parseCodexCliSessionsArgs(args);
if (parsed.help || !parsed.host) return "Usage: /codex sessions --host <node> [filter] [--limit <n>]";
return formatCodexCliSessions(await deps.listCodexCliSessionsOnNode({
requestedNode: parsed.host,
filter: parsed.filter,
limit: parsed.limit
}));
}
async function resumeThread(deps, ctx, pluginConfig, args) {
const parsed = parseResumeArgs(args);
const normalizedThreadId = parsed.threadId?.trim();
if (parsed.help) return args.includes("--help") || args.includes("-h") || parsed.host ? "Usage: /codex resume <thread-id>\nUsage: /codex resume <session-id> --host <node> --bind here" : "Usage: /codex resume <thread-id>";
if (parsed.host) return await bindCodexCliNodeSession(deps, ctx, parsed);
if (!normalizedThreadId || args.length !== 1) return "Usage: /codex resume <thread-id>";
if (!ctx.sessionFile) return "Cannot attach a Codex thread because this command did not include an OpenClaw session file.";
const response = await deps.codexControlRequest(pluginConfig, CODEX_CONTROL_METHODS.resumeThread, {
threadId: normalizedThreadId,
persistExtendedHistory: true
});
const thread = isJsonObject(response) && isJsonObject(response.thread) ? response.thread : {};
const effectiveThreadId = readString$1(thread, "id") ?? normalizedThreadId;
await deps.writeCodexAppServerBinding(ctx.sessionFile, {
threadId: effectiveThreadId,
cwd: readString$1(thread, "cwd") ?? "",
model: isJsonObject(response) ? readString$1(response, "model") : void 0,
modelProvider: isJsonObject(response) ? readString$1(response, "modelProvider") : void 0
});
return `Attached this OpenClaw session to Codex thread ${formatCodexDisplayText(effectiveThreadId)}.`;
}
async function bindCodexCliNodeSession(deps, ctx, parsed) {
if (!parsed.threadId || !parsed.host || parsed.bindHere !== true) return "Usage: /codex resume <session-id> --host <node> --bind here";
const resolved = await deps.resolveCodexCliSessionForBindingOnNode({
requestedNode: parsed.host,
sessionId: parsed.threadId
});
if (!resolved.session) return `No Codex CLI session ${formatCodexDisplayText(parsed.threadId)} was found on ${formatCodexDisplayText(parsed.host)}.`;
const nodeId = resolved.node.nodeId;
if (!nodeId) return "Cannot bind Codex CLI session because the selected node did not include a node id.";
const data = createCodexCliNodeConversationBindingData({
nodeId,
sessionId: parsed.threadId,
cwd: resolved.session?.cwd
});
const summary = `Codex CLI session ${formatCodexDisplayText(parsed.threadId)} on ${formatCodexDisplayText(nodeId)}`;
const request = await ctx.requestConversationBinding({
summary,
detachHint: "/codex detach",
data
});
if (request.status === "bound") return `Bound this conversation to Codex CLI session ${formatCodexDisplayText(parsed.threadId)} on ${formatCodexDisplayText(nodeId)}.`;
if (request.status === "pending") return request.reply.text ?? "Codex CLI session binding is pending approval.";
return formatCodexDisplayText(request.message);
}
async function stopConversationTurn(deps, ctx, pluginConfig) {
const target = await resolveControlTarget(ctx);
if (!target) return "Cannot stop Codex because this command did not include an OpenClaw session file.";
return (await deps.stopCodexConversationTurn({
sessionFile: target.sessionFile,
pluginConfig,
agentDir: target.agentDir,
config: ctx.config
})).message;
}
async function steerConversationTurn(deps, ctx, pluginConfig, message) {
const target = await resolveControlTarget(ctx);
if (!target) return "Cannot steer Codex because this command did not include an OpenClaw session file.";
return (await deps.steerCodexConversationTurn({
sessionFile: target.sessionFile,
pluginConfig,
message,
agentDir: target.agentDir,
config: ctx.config
})).message;
}
async function setConversationModel(deps, ctx, pluginConfig, args) {
if (args.length > 1) return "Usage: /codex model <model>";
const target = await resolveControlTarget(ctx);
if (!target) return "Cannot set Codex model because this command did not include an OpenClaw session file.";
const [model = ""] = args;
const normalized = model.trim();
if (!normalized) {
const binding = await deps.readCodexAppServerBinding(target.sessionFile, {
agentDir: target.agentDir,
config: ctx.config
});
return binding?.model ? `Codex model: ${formatCodexDisplayText(binding.model)}` : "Usage: /codex model <model>";
}
return await deps.setCodexConversationModel({
sessionFile: target.sessionFile,
pluginConfig,
model: normalized,
agentDir: target.agentDir,
config: ctx.config
});
}
async function setConversationFastMode(deps, ctx, pluginConfig, args) {
if (args.length > 1) return "Usage: /codex fast [on|off|status]";
const target = await resolveControlTarget(ctx);
if (!target) return "Cannot set Codex fast mode because this command did not include an OpenClaw session file.";
const value = args[0];
const parsed = parseCodexFastModeArg(value);
if (value && parsed == null && value.trim().toLowerCase() !== "status") return "Usage: /codex fast [on|off|status]";
return await deps.setCodexConversationFastMode({
sessionFile: target.sessionFile,
pluginConfig,
enabled: parsed,
agentDir: target.agentDir,
config: ctx.config
});
}
async function setConversationPermissions(deps, ctx, pluginConfig, args) {
if (args.length > 1) return "Usage: /codex permissions [default|yolo|status]";
const target = await resolveControlTarget(ctx);
if (!target) return "Cannot set Codex permissions because this command did not include an OpenClaw session file.";
const value = args[0];
const parsed = parseCodexPermissionsModeArg(value);
if (value && !parsed && value.trim().toLowerCase() !== "status") return "Usage: /codex permissions [default|yolo|status]";
return await deps.setCodexConversationPermissions({
sessionFile: target.sessionFile,
pluginConfig,
mode: parsed,
agentDir: target.agentDir,
config: ctx.config
});
}
async function resolveControlTarget(ctx) {
const data = readCodexConversationBindingData(await ctx.getCurrentConversationBinding());
const scope = resolveCodexConversationControlScope(ctx);
if (data?.kind === "codex-app-server-session") return {
sessionFile: data.sessionFile,
agentDir: data.agentDir ?? scope.agentDir
};
return ctx.sessionFile ? {
sessionFile: ctx.sessionFile,
agentDir: scope.agentDir
} : void 0;
}
async function resolveControlSessionFile(ctx) {
return (await resolveControlTarget(ctx))?.sessionFile;
}
function resolveCodexConversationControlScope(ctx) {
const { sessionAgentId } = resolveSessionAgentIds({
sessionKey: ctx.sessionKey,
config: ctx.config
});
return { agentDir: resolveAgentDir(ctx.config, sessionAgentId) };
}
async function handleCodexDiagnosticsFeedback(deps, ctx, pluginConfig, args, commandPrefix) {
if (ctx.senderIsOwner !== true) return { text: "Only an owner can send Codex diagnostics." };
const parsed = parseDiagnosticsArgs(args);
if (parsed.action === "usage") return { text: formatDiagnosticsUsage(commandPrefix) };
if (parsed.action === "confirm") return { text: await confirmCodexDiagnosticsFeedback(deps, ctx, pluginConfig, parsed.token) };
if (parsed.action === "cancel") return { text: cancelCodexDiagnosticsFeedback(ctx, parsed.token) };
if (ctx.diagnosticsUploadApproved === true) return { text: await sendCodexDiagnosticsFeedbackForContext(deps, ctx, pluginConfig, parsed.note) };
if (ctx.diagnosticsPreviewOnly === true) return { text: await previewCodexDiagnosticsFeedbackApproval(deps, ctx, parsed.note) };
return await requestCodexDiagnosticsFeedbackApproval(deps, ctx, parsed.note, commandPrefix);
}
async function requestCodexDiagnosticsFeedbackApproval(deps, ctx, note, commandPrefix) {
if (!await hasAnyCodexDiagnosticsSessionFile(ctx)) return { text: "Cannot send Codex diagnostics because this command did not include an OpenClaw session file." };
const targets = await resolveCodexDiagnosticsTargets(deps, ctx);
if (targets.length === 0) return { text: ["No Codex thread is attached to this OpenClaw session yet.", "Use /codex threads to find a thread, then /codex resume <thread-id> before sending diagnostics."].join("\n") };
const now = Date.now();
const cooldownMessage = readCodexDiagnosticsTargetsCooldownMessage(targets, ctx, now);
if (cooldownMessage) return { text: cooldownMessage };
if (!ctx.senderId) return { text: "Cannot send Codex diagnostics because this command did not include a sender identity." };
const reason = normalizeDiagnosticsReason(note);
const token = createCodexDiagnosticsConfirmation({
targets,
note: reason,
senderId: ctx.senderId,
channel: ctx.channel,
scopeKey: readCodexDiagnosticsCooldownScope(ctx),
privateRouted: ctx.diagnosticsPrivateRouted === true,
...readCodexDiagnosticsConfirmationScope(ctx),
now
});
const confirmCommand = `${commandPrefix} confirm ${token}`;
const cancelCommand = `${commandPrefix} cancel ${token}`;
const displayReason = reason ? escapeCodexChatText(formatCodexTextForDisplay(reason)) : void 0;
return {
text: [
targets.length === 1 ? "Codex runtime thread detected." : "Codex runtime threads detected.",
`Codex diagnostics can send ${targets.length === 1 ? "this thread's feedback bundle" : "these threads' feedback bundles"} to OpenAI servers.`,
"Codex sessions:",
...formatCodexDiagnosticsTargetLines(targets),
...displayReason ? [`Note: ${displayReason}`] : [],
"Included: Codex logs and spawned Codex subthreads when available.",
`To send: ${confirmCommand}`,
`To cancel: ${cancelCommand}`,
"This request expires in 5 minutes."
].join("\n"),
interactive: { blocks: [{
type: "buttons",
buttons: [{
label: "Send diagnostics",
action: {
type: "command",
command: confirmCommand
},
value: confirmCommand,
style: "danger"
}, {
label: "Cancel",
action: {
type: "command",
command: cancelCommand
},
value: cancelCommand,
style: "secondary"
}]
}] }
};
}
async function previewCodexDiagnosticsFeedbackApproval(deps, ctx, note) {
if (!await hasAnyCodexDiagnosticsSessionFile(ctx)) return "Cannot send Codex diagnostics because this command did not include an OpenClaw session file.";
const targets = await resolveCodexDiagnosticsTargets(deps, ctx);
if (targets.length === 0) return ["No Codex thread is attached to this OpenClaw session yet.", "Use /codex threads to find a thread, then /codex resume <thread-id> before sending diagnostics."].join("\n");
const cooldownMessage = readCodexDiagnosticsTargetsCooldownMessage(targets, ctx, Date.now(), { includeThreadId: false });
if (cooldownMessage) return cooldownMessage;
const reason = normalizeDiagnosticsReason(note);
const displayReason = reason ? escapeCodexChatText(formatCodexTextForDisplay(reason)) : void 0;
return [
targets.length === 1 ? "Codex runtime thread detected." : "Codex runtime threads detected.",
`Approving diagnostics will also send ${targets.length === 1 ? "this thread's feedback bundle" : "these threads' feedback bundles"} to OpenAI servers.`,
"The completed diagnostics reply will list the OpenClaw session ids and Codex thread ids that were sent.",
...displayReason ? [`Note: ${displayReason}`] : [],
"Included: Codex logs and spawned Codex subthreads when available."
].join("\n");
}
async function confirmCodexDiagnosticsFeedback(deps, ctx, pluginConfig, token) {
const pending = readPendingCodexDiagnosticsConfirmation(token, Date.now());
if (!pending) return "No pending Codex diagnostics confirmation was found. Run /diagnostics again to create a fresh request.";
if (!pending.senderId || !ctx.senderId) return "Cannot confirm Codex diagnostics because this command did not include the original sender identity.";
if (pending.senderId !== ctx.senderId) return "Only the user who requested these Codex diagnostics can confirm the upload.";
if (pending.channel !== ctx.channel) return "This Codex diagnostics confirmation belongs to a different channel.";
const scopeMismatch = readCodexDiagnosticsScopeMismatch(pending, ctx);
if (scopeMismatch) return scopeMismatch.confirmMessage;
deletePendingCodexDiagnosticsConfirmation(token);
if (!pending.privateRouted && !await hasAnyCodexDiagnosticsSessionFile(ctx)) return "Cannot send Codex diagnostics because this command did not include an OpenClaw session file.";
const currentTargets = pending.privateRouted ? await resolvePendingCodexDiagnosticsTargets(deps, pending.targets) : await resolveCodexDiagnosticsTargets(deps, ctx);
if (!codexDiagnosticsTargetsMatch(pending.targets, currentTargets)) return "The Codex diagnostics sessions changed before confirmation. Run /diagnostics again for the current threads.";
return await sendCodexDiagnosticsFeedbackForTargets(deps, ctx, pluginConfig, pending.note ?? "", pending.targets);
}
function cancelCodexDiagnosticsFeedback(ctx, token) {
const pending = readPendingCodexDiagnosticsConfirmation(token, Date.now());
if (!pending) return "No pending Codex diagnostics confirmation was found.";
if (!pending.senderId || !ctx.senderId) return "Cannot cancel Codex diagnostics because this command did not include the original sender identity.";
if (pending.senderId !== ctx.senderId) return "Only the user who requested these Codex diagnostics can cancel the upload.";
if (pending.channel !== ctx.channel) return "This Codex diagnostics confirmation belongs to a different channel.";
const scopeMismatch = readCodexDiagnosticsScopeMismatch(pending, ctx);
if (scopeMismatch) return scopeMismatch.cancelMessage;
deletePendingCodexDiagnosticsConfirmation(token);
return [
"Codex diagnostics upload canceled.",
"Codex sessions:",
...formatCodexDiagnosticsTargetLines(pending.targets)
].join("\n");
}
async function sendCodexDiagnosticsFeedbackForContext(deps, ctx, pluginConfig, note) {
if (!await hasAnyCodexDiagnosticsSessionFile(ctx)) return "Cannot send Codex diagnostics because this command did not include an OpenClaw session file.";
const targets = await resolveCodexDiagnosticsTargets(deps, ctx);
if (targets.length === 0) return ["No Codex thread is attached to this OpenClaw session yet.", "Use /codex threads to find a thread, then /codex resume <thread-id> before sending diagnostics."].join("\n");
return await sendCodexDiagnosticsFeedbackForTargets(deps, ctx, pluginConfig, note, targets);
}
async function sendCodexDiagnosticsFeedbackForTargets(deps, ctx, pluginConfig, note, targets) {
if (targets.length === 0) return ["No Codex thread is attached to this OpenClaw session yet.", "Use /codex threads to find a thread, then /codex resume <thread-id> before sending diagnostics."].join("\n");
const now = Date.now();
const cooldownMessage = readCodexDiagnosticsTargetsCooldownMessage(targets, ctx, now);
if (cooldownMessage) return cooldownMessage;
const reason = normalizeDiagnosticsReason(note);
const sent = [];
const failed = [];
for (const target of targets) {
const response = await deps.safeCodexControlRequest(pluginConfig, CODEX_CONTROL_METHODS.feedback, {
classification: "bug",
threadId: target.threadId,
includeLogs: true,
tags: buildDiagnosticsTags(ctx),
...reason ? { reason } : {}
});
if (!response.ok) {
failed.push({
target,
error: response.error
});
continue;
}
const responseThreadId = isJsonObject(response.value) ? readString$1(response.value, "threadId") : void 0;
sent.push({
...target,
threadId: responseThreadId ?? target.threadId
});
recordCodexDiagnosticsUpload(target.threadId, ctx, now);
}
return formatCodexDiagnosticsUploadResult(sent, failed);
}
async function hasAnyCodexDiagnosticsSessionFile(ctx) {
if (await resolveControlSessionFile(ctx)) return true;
return (ctx.diagnosticsSessions ?? []).some((session) => Boolean(session.sessionFile));
}
async function resolveCodexDiagnosticsTargets(deps, ctx) {
const activeSessionFile = await resolveControlSessionFile(ctx);
const candidates = [];
if (activeSessionFile) candidates.push({
threadId: "",
sessionFile: activeSessionFile,
sessionKey: ctx.sessionKey,
sessionId: ctx.sessionId,
channel: ctx.channel,
channelId: ctx.channelId,
accountId: ctx.accountId,
messageThreadId: ctx.messageThreadId,
threadParentId: ctx.threadParentId
});
for (const session of ctx.diagnosticsSessions ?? []) {
if (!session.sessionFile) continue;
candidates.push({
threadId: "",
sessionFile: session.sessionFile,
sessionKey: session.sessionKey,
sessionId: session.sessionId,
channel: session.channel,
channelId: session.channelId,
accountId: session.accountId,
messageThreadId: session.messageThreadId,
threadParentId: session.threadParentId
});
}
const seenSessionFiles = /* @__PURE__ */ new Set();
const seenThreadIds = /* @__PURE__ */ new Set();
const targets = [];
for (const candidate of candidates) {
if (seenSessionFiles.has(candidate.sessionFile)) continue;
seenSessionFiles.add(candidate.sessionFile);
const binding = await deps.readCodexAppServerBinding(candidate.sessionFile);
if (!binding?.threadId || seenThreadIds.has(binding.threadId)) continue;
seenThreadIds.add(binding.threadId);
targets.push({
...candidate,
threadId: binding.threadId
});
}
return targets;
}
async function resolvePendingCodexDiagnosticsTargets(deps, targets) {
const resolved = [];
for (const target of targets) {
const binding = await deps.readCodexAppServerBinding(target.sessionFile);
if (!binding?.threadId) continue;
resolved.push({
...target,
threadId: binding.threadId
});
}
return resolved;
}
function codexDiagnosticsTargetsMatch(expected, actual) {
const expectedThreadIds = expected.map((target) => target.threadId).toSorted();
const actualThreadIds = actual.map((target) => target.threadId).toSorted();
return expectedThreadIds.length === actualThreadIds.length && expectedThreadIds.every((threadId, index) => threadId === actualThreadIds[index]);
}
function formatCodexDiagnosticsUploadResult(sent, failed) {
const lines = [];
if (sent.length > 0) {
lines.push("Codex diagnostics sent to OpenAI servers:");
lines.push(...formatCodexDiagnosticsTargetLines(sent));
lines.push("Included Codex logs and spawned Codex subthreads when available.");
}
if (failed.length > 0) {
if (lines.length > 0) lines.push("");
lines.push("Could not send Codex diagnostics:");
lines.push(...failed.map(({ target, error }) => `${formatCodexDiagnosticsTargetLine(target)}: ${formatCodexErrorForDisplay(error)}`));
lines.push("Inspect locally:");
lines.push(...failed.map(({ target }) => `- ${formatCodexResumeCommandForDisplay(target.threadId)}`));
}
return lines.join("\n");
}
function formatCodexDiagnosticsTargetLines(targets) {
return targets.flatMap((target, index) => {
const lines = formatCodexDiagnosticsTargetBlock(target, index);
return index < targets.length - 1 ? [...lines, ""] : lines;
});
}
function formatCodexDiagnosticsTargetBlock(target, index) {
const lines = [`Session ${index + 1}`];
if (target.channel) lines.push(`Channel: ${formatCodexValueForDisplay(target.channel)}`);
if (target.sessionKey) lines.push(`OpenClaw session key: ${formatCodexCopyableValueForDisplay(target.sessionKey)}`);
if (target.sessionId) lines.push(`OpenClaw session id: ${formatCodexCopyableValueForDisplay(target.sessionId)}`);
lines.push(`Codex thread id: ${formatCodexCopyableValueForDisplay(target.threadId)}`);
lines.push(`Inspect locally: ${formatCodexResumeCommandForDisplay(target.threadId)}`);
return lines;
}
function formatCodexDiagnosticsTargetLine(target) {
const parts = [];
if (target.channel) parts.push(`channel ${formatCodexValueForDisplay(target.channel)}`);
const sessionLabel = target.sessionId || target.sessionKey;
if (sessionLabel) parts.push(`OpenClaw session ${formatCodexValueForDisplay(sessionLabel)}`);
parts.push(`Codex thread ${formatCodexThreadIdForDisplay(target.threadId)}`);
return `- ${parts.join(", ")}`;
}
function normalizeDiagnosticsReason(note) {
const normalized = normalizeOptionalString(note);
return normalized ? normalized.slice(0, CODEX_DIAGNOSTICS_REASON_MAX_CHARS) : void 0;
}
function parseDiagnosticsArgs(args) {
const [action, token, ...extra] = splitArgs(args);
const normalizedAction = action?.toLowerCase();
if ((normalizedAction === "confirm" || normalizedAction === "--confirm") && token && extra.length === 0) return {
action: "confirm",
token
};
if ((normalizedAction === "cancel" || normalizedAction === "--cancel") && token && extra.length === 0) return {
action: "cancel",
token
};
if (normalizedAction === "confirm" || normalizedAction === "--confirm" || normalizedAction === "cancel" || normalizedAction === "--cancel") return { action: "usage" };
return {
action: "request",
note: args
};
}
function formatDiagnosticsUsage(commandPrefix) {
return [
`Usage: ${commandPrefix} [note]`,
`Usage: ${commandPrefix} confirm <token>`,
`Usage: ${commandPrefix} cancel <token>`
].join("\n");
}
function createCodexDiagnosticsConfirmation(params) {
prunePendingCodexDiagnosticsConfirmations(params.now);
if (!pendingCodexDiagnosticsConfirmationTokensByScope.has(params.scopeKey) && pendingCodexDiagnosticsConfirmationTokensByScope.size >= CODEX_DIAGNOSTICS_CONFIRMATION_MAX_SCOPES) {
const oldestScopeKey = pendingCodexDiagnosticsConfirmationTokensByScope.keys().next().value;
if (typeof oldestScopeKey === "string") deletePendingCodexDiagnosticsConfirmationScope(oldestScopeKey);
}
const scopeTokens = pendingCodexDiagnosticsConfirmationTokensByScope.get(params.scopeKey) ?? [];
while (scopeTokens.length >= CODEX_DIAGNOSTICS_CONFIRMATION_MAX_REQUESTS_PER_SCOPE) {
const oldestToken = scopeTokens.shift();
if (!oldestToken) break;
pendingCodexDiagnosticsConfirmations.delete(oldestToken);
}
const token = crypto.randomBytes(6).toString("hex");
scopeTokens.push(token);
pendingCodexDiagnosticsConfirmationTokensByScope.set(params.scopeKey, scopeTokens);
pendingCodexDiagnosticsConfirmations.set(token, {
token,
targets: params.targets,
note: params.note,
senderId: params.senderId,
channel: params.channel,
accountId: params.accountId,
channelId: params.channelId,
messageThreadId: params.messageThreadId,
threadParentId: params.threadParentId,
sessionKey: params.sessionKey,
scopeKey: params.scopeKey,
...params.privateRouted === void 0 ? {} : { privateRouted: params.privateRouted },
createdAt: params.now
});
return token;
}
function readCodexDiagnosticsConfirmationScope(ctx) {
return {
accountId: normalizeCodexDiagnosticsScopeField(ctx.accountId),
channelId: normalizeCodexDiagnosticsScopeField(ctx.channelId),
messageThreadId: typeof ctx.messageThreadId === "string" || typeof ctx.messageThreadId === "number" ? normalizeCodexDiagnosticsScopeField(String(ctx.messageThreadId)) : void 0,
threadParentId: normalizeCodexDiagnosticsScopeField(ctx.threadParentId),
sessionKey: normalizeCodexDiagnosticsScopeField(ctx.sessionKey)
};
}
function readCodexDiagnosticsScopeMismatch(pending, ctx) {
const current = readCodexDiagnosticsConfirmationScope(ctx);
if (pending.accountId !== current.accountId) return {
confirmMessage: "This Codex diagnostics confirmation belongs to a different account.",
cancelMessage: "This Codex diagnostics confirmation belongs to a different account."
};
if (pending.privateRouted) return;
if (pending.channelId !== current.channelId) return {
confirmMessage: "This Codex diagnostics confirmation belongs to a different channel instance.",
cancelMessage: "This Codex diagnostics confirmation belongs to a different channel instance."
};
if (pending.messageThreadId !== current.messageThreadId) return {
confirmMessage: "This Codex diagnostics confirmation belongs to a different thread.",
cancelMessage: "This Codex diagnostics confirmation belongs to a different thread."
};
if (pending.threadParentId !== current.threadParentId) return {
confirmMessage: "This Codex diagnostics confirmation belongs to a different parent thread.",
cancelMessage: "This Codex diagnostics confirmation belongs to a different parent thread."
};
if (pending.sessionKey !== current.sessionKey) return {
confirmMessage: "This Codex diagnostics confirmation belongs to a different session.",
cancelMessage: "This Codex diagnostics confirmation belongs to a different session."
};
}
function readPendingCodexDiagnosticsConfirmation(token, now) {
prunePendingCodexDiagnosticsConfirmations(now);
return pendingCodexDiagnosticsConfirmations.get(token);
}
function prunePendingCodexDiagnosticsConfirmations(now) {
for (const [token, pending] of pendingCodexDiagnosticsConfirmations) if (now - pending.createdAt >= CODEX_DIAGNOSTICS_CONFIRMATION_TTL_MS) deletePendingCodexDiagnosticsConfirmation(token);
}
function deletePendingCodexDiagnosticsConfirmation(token) {
const pending = pendingCodexDiagnosticsConfirmations.get(token);
pendingCodexDiagnosticsConfirmations.delete(token);
if (!pending) return;
const scopeTokens = pendingCodexDiagnosticsConfirmationTokensByScope.get(pending.scopeKey);
if (!scopeTokens) return;
const tokenIndex = scopeTokens.indexOf(token);
if (tokenIndex >= 0) scopeTokens.splice(tokenIndex, 1);
if (scopeTokens.length === 0) pendingCodexDiagnosticsConfirmationTokensByScope.delete(pending.scopeKey);
}
function deletePendingCodexDiagnosticsConfirmationScope(scopeKey) {
const scopeTokens = pendingCodexDiagnosticsConfirmationTokensByScope.get(scopeKey) ?? [];
for (const token of scopeTokens) pendingCodexDiagnosticsConfirmations.delete(token);
pendingCodexDiagnosticsConfirmationTokensByScope.delete(scopeKey);
}
function buildDiagnosticsTags(ctx) {
const tags = { source: CODEX_DIAGNOSTICS_SOURCE };
addTag(tags, "channel", ctx.channel);
return tags;
}
function addTag(tags, key, value) {
if (typeof value === "string" && value.trim()) tags[key] = value.trim();
}
function formatCodexThreadIdForDisplay(threadId) {
return escapeCodexChatText(formatCodexTextForDisplay(threadId));
}
function formatCodexValueForDisplay(value) {
return escapeCodexChatText(formatCodexTextForDisplay(value));
}
function formatCodexCopyableValueForDisplay(value) {
const safe = formatCodexTextForDisplay(value);
if (CODEX_RESUME_SAFE_THREAD_ID_PATTERN.test(safe)) return `\`${safe}\``;
return escapeCodexChatText(safe);
}
function formatCodexTextForDisplay(value) {
let safe = "";
for (const character of value) {
const codePoint = character.codePointAt(0);
safe += codePoint != null && isUnsafeDisplayCodePoint(codePoint) ? "?" : character;
}
safe = safe.trim();
return safe || "<unknown>";
}
function escapeCodexChatText(value) {
return value.replaceAll("&", "&").replaceAll("<", "<").replaceAll(">", ">").replaceAll("@", "@").replaceAll("`", "`").replaceAll("[", "[").replaceAll("]", "]").replaceAll("(", "(").replaceAll(")", ")").replaceAll("*", "∗").replaceAll("_", "_").replaceAll("~", "~").replaceAll("|", "|");
}
function readCodexDiagnosticsCooldownMs(threadId, now) {
const lastSentAt = lastCodexDiagnosticsUploadByThread.get(threadId);
if (!lastSentAt) return 0;
const remainingMs = Math.max(0, CODEX_DIAGNOSTICS_COOLDOWN_MS - (now - lastSentAt));
if (remainingMs === 0) lastCodexDiagnosticsUploadByThread.delete(threadId);
return remainingMs;
}
function readCodexDiagnosticsTargetsCooldownMessage(targets, ctx, now, options = {}) {
for (const target of targets) {
const cooldownMs = readCodexDiagnosticsCooldownMs(target.threadId, now);
if (cooldownMs > 0) {
if (options.includeThreadId === false) return `Codex diagnostics were already sent for one of these Codex threads recently. Try again in ${Math.ceil(cooldownMs / 1e3)}s.`;
return `Codex diagnostics were already sent for thread ${formatCodexThreadIdForDisplay(target.threadId)} recently. Try again in ${Math.ceil(cooldownMs / 1e3)}s.`;
}
}
const scopeCooldownMs = readCodexDiagnosticsScopeCooldownMs(readCodexDiagnosticsCooldownScope(ctx), now);
if (scopeCooldownMs > 0) return `Codex diagnostics were already sent for this account or channel recently. Try again in ${Math.ceil(scopeCooldownMs / 1e3)}s.`;
}
function readCodexDiagnosticsScopeCooldownMs(scope, now) {
const lastSentAt = lastCodexDiagnosticsUploadByScope.get(scope);
if (!lastSentAt) return 0;
const remainingMs = Math.max(0, CODEX_DIAGNOSTICS_COOLDOWN_MS - (now - lastSentAt));
if (remainingMs === 0) lastCodexDiagnosticsUploadByScope.delete(scope);
return remainingMs;
}
function recordCodexDiagnosticsUpload(threadId, ctx, now) {
pruneCodexDiagnosticsCooldowns(now);
recordBoundedCodexDiagnosticsCooldown(lastCodexDiagnosticsUploadByScope, readCodexDiagnosticsCooldownScope(ctx), CODEX_DIAGNOSTICS_COOLDOWN_MAX_SCOPES, now);
recordBoundedCodexDiagnosticsCooldown(lastCodexDiagnosticsUploadByThread, threadId, CODEX_DIAGNOSTICS_COOLDOWN_MAX_THREADS, now);
}
function recordBoundedCodexDiagnosticsCooldown(map, key, maxSize, now) {
if (!map.has(key)) while (map.size >= maxSize) {
const oldestKey = map.keys().next().value;
if (typeof oldestKey !== "string") break;
map.delete(oldestKey);
}
map.set(key, now);
}
function readCodexDiagnosticsCooldownScope(ctx) {
const scope = readCodexDiagnosticsConfirmationScope(ctx);
const payload = JSON.stringify({
accountId: scope.accountId ?? null,
channelId: scope.channelId ?? null,
sessionKey: scope.sessionKey ?? null,
messageThreadId: scope.messageThreadId ?? null,
threadParentId: scope.threadParentId ?? null,
senderId: normalizeCodexDiagnosticsScopeField(ctx.senderId) ?? null,
channel: normalizeCodexDiagnosticsScopeField(ctx.channel) ?? ""
});
return crypto.createHash("sha256").update(payload).digest("hex");
}
function pruneCodexDiagnosticsCooldowns(now) {
pruneCodexDiagnosticsCooldownMap(lastCodexDiagnosticsUploadByThread, now);
pruneCodexDiagnosticsCooldownMap(lastCodexDiagnosticsUploadByScope, now);
}
function pruneCodexDiagnosticsCooldownMap(map, now) {
for (const [key, lastSentAt] of map) if (now - lastSentAt >= CODEX_DIAGNOSTICS_COOLDOWN_MS) map.delete(key);
}
function formatCodexErrorForDisplay(error) {
return escapeCodexChatText(formatCodexTextForDisplay(error).slice(0, CODEX_DIAGNOSTICS_ERROR_MAX_CHARS)) || "unknown error";
}
function formatCodexResumeCommandForDisplay(threadId) {
const safeThreadId = formatCodexTextForDisplay(threadId);
if (!CODEX_RESUME_SAFE_THREAD_ID_PATTERN.test(safeThreadId)) return "run codex resume and paste the thread id shown above";
return `\`codex resume ${safeThreadId}\``;
}
function isUnsafeDisplayCodePoint(codePoint) {
return codePoint <= 31 || codePoint >= 127 && codePoint <= 159 || codePoint === 173 || codePoint === 1564 || codePoint === 6158 || codePoint >= 8203 && codePoint <= 8207 || codePoint >= 8234 && codePoint <= 8238 || codePoint >= 8288 && codePoint <= 8303 || codePoint === 65279 || codePoint >= 65529 && codePoint <= 65531 || codePoint >= 917504 && codePoint <= 917631;
}
function normalizeCodexDiagnosticsScopeField(value) {
const normalized = normalizeOptionalString(value);
if (!normalized) return;
if (normalized.length <= CODEX_DIAGNOSTICS_SCOPE_FIELD_MAX_CHARS) return normalized;
return `sha256:${crypto.createHash("sha256").update(normalized).digest("hex")}`;
}
async function startThreadAction(deps, ctx, pluginConfig, method, label, args) {
if (args.length > 0) return `Usage: /codex ${label === "compaction" ? "compact" : label}`;
const target = await resolveControlTarget(ctx);
if (!target) return `Cannot start Codex ${label} because this command did not include an OpenClaw session file.`;
const binding = await deps.readCodexAppServerBinding(target.sessionFile, {
agentDir: target.agentDir,
config: ctx.config
});
if (!binding?.threadId) return `No Codex thread is attached to this OpenClaw session yet.`;
if (method === CODEX_CONTROL_METHODS.review) await deps.codexControlRequest(pluginConfig, method, {
threadId: binding.threadId,
target: { type: "uncommittedChanges" }
}, {
agentDir: target.agentDir,
authProfileId: binding.authProfileId,
config: ctx.config
});
else await deps.codexControlRequest(pluginConfig, method, { threadId: binding.threadId }, {
agentDir: target.agentDir,
authProfileId: binding.authProfileId,
config: ctx.config
});
return `Started Codex ${label} for thread ${formatCodexDisplayText(binding.threadId)}.`;
}
function splitArgs(value) {
const input = value ?? "";
const args = [];
let current = "";
let quote;
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 = void 0;
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;
}
function parseBindArgs(args) {
const parsed = {};
for (let index = 0; index < args.length; index += 1) {
const arg = args[index];
if (arg === "--help" || arg === "-h") {
parsed.help = true;
continue;
}
if (arg === "--cwd") {
const value = readRequiredOptionValue(args, index);
if (!value || parsed.cwd !== void 0) {
parsed.help = true;
continue;
}
parsed.cwd = value;
index += 1;
continue;
}
if (arg === "--model") {
const value = readRequiredOptionValue(args, index);
if (!value || parsed.model !== void 0) {
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 !== void 0) {
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;
}
function parseCodexCliSessionsArgs(args) {
const parsed = { filter: "" };
const filter = [];
for (let index = 0; index < args.length; index += 1) {
const arg = args[index];
if (arg === "--help" || arg === "-h") {
parsed.help = true;
continue;
}
if (arg === "--host" || arg === "--node") {
const value = readRequiredOptionValue(args, index);
if (!value || parsed.host !== void 0) {
parsed.help = true;
continue;
}
parsed.host = value;
index += 1;
continue;
}
if (arg === "--limit") {
const parsedLimit = parseStrictPositiveInteger(readRequiredOptionValue(args, index));
if (parsedLimit === void 0) {
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;
}
function parseResumeArgs(args) {
const parsed = {};
for (let index = 0; index < args.length; index += 1) {
const arg = args[index];
if (arg === "--help" || arg === "-h") {
parsed.help = true;
continue;
}
if (arg === "--host" || arg === "--node") {
const value = readRequiredOptionValue(args, index);
if (!value || parsed.host !== void 0) {
parsed.help = true;
continue;
}
parsed.host = value;
index += 1;
continue;
}
if (arg === "--bind") {
if (readRequiredOptionValue(args, index) !== "here" || parsed.bindHere !== void 0) {
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;
}
function parseComputerUseArgs(args) {
const parsed = {
action: "status",
overrides: {},
hasOverrides: false
};
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 !== void 0) {
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 !== void 0) {
parsed.help = true;
continue;
}
parsed.overrides.marketplacePath = value;
index += 1;
continue;
}
if (arg === "--marketplace") {
const value = readRequiredOptionValue(args, index);
if (!value || parsed.overrides.marketplaceName !== void 0) {
parsed.help = true;
continue;
}
parsed.overrides.marketplaceName = value;
index += 1;
continue;
}
if (arg === "--plugin") {
const value = readRequiredOptionValue(args, index);
if (!value || parsed.overrides.pluginName !== void 0) {
parsed.help = true;
continue;
}
parsed.overrides.pluginName = value;
index += 1;
continue;
}
if (arg === "--server" || arg === "--mcp-server") {
const value = readRequiredOptionValue(args, index);
if (!value || parsed.overrides.mcpServerName !== void 0) {
parsed.help = true;
continue;
}
parsed.overrides.mcpServerName = value;
index += 1;
continue;
}
parsed.help = true;
}
parsed.overrides = normalizeComputerUseStringOverrides(parsed.overrides);
parsed.hasOverrides = Object.values(parsed.overrides).some(Boolean);
return parsed;
}
function readRequiredOptionValue(args, index) {
const value = args[index + 1];
const normalized = value?.trim();
if (!normalized || normalized.startsWith("-")) return;
return value;
}
function normalizeComputerUseStringOverrides(overrides) {
const normalized = {};
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;
const pluginName = normalizeOptionalString(overrides.pluginName);
if (pluginName) normalized.pluginName = pluginName;
const mcpServerName = normalizeOptionalString(overrides.mcpServerName);
if (mcpServerName) normalized.mcpServerName = mcpServerName;
return normalized;
}
//#endregion
export { handleCodexSubcommand };