























@@ -36,6 +36,7 @@ import {
3636type SafeValue,
3737} from "./command-rpc.js";
3838import {
39+createCodexCliNodeConversationBindingData,
3940readCodexConversationBindingData,
4041resolveCodexDefaultWorkspaceDir,
4142startCodexConversationThread,
@@ -51,6 +52,11 @@ import {
5152steerCodexConversationTurn,
5253stopCodexConversationTurn,
5354} from "./conversation-control.js";
55+import {
56+formatCodexCliSessions,
57+listCodexCliSessionsOnNode,
58+resolveCodexCliSessionForBindingOnNode,
59+} from "./node-cli-sessions.js";
54605561export type CodexCommandDeps = {
5662codexControlRequest: CodexControlRequestFn;
@@ -71,6 +77,8 @@ export type CodexCommandDeps = {
7177setCodexConversationPermissions: typeof setCodexConversationPermissions;
7278steerCodexConversationTurn: typeof steerCodexConversationTurn;
7379stopCodexConversationTurn: typeof stopCodexConversationTurn;
80+listCodexCliSessionsOnNode: ListCodexCliSessionsOnNodeFn;
81+resolveCodexCliSessionForBindingOnNode: ResolveCodexCliSessionForBindingOnNodeFn;
7482};
75837684type CodexControlRequestFn = (
@@ -87,6 +95,14 @@ type SafeCodexControlRequestFn = (
8795options?: CodexControlRequestOptions,
8896) => Promise<SafeValue<JsonValue | undefined>>;
899798+type ListCodexCliSessionsOnNodeFn = (
99+params: Omit<Parameters<typeof listCodexCliSessionsOnNode>[0], "runtime">,
100+) => ReturnType<typeof listCodexCliSessionsOnNode>;
101+102+type ResolveCodexCliSessionForBindingOnNodeFn = (
103+params: Omit<Parameters<typeof resolveCodexCliSessionForBindingOnNode>[0], "runtime">,
104+) => ReturnType<typeof resolveCodexCliSessionForBindingOnNode>;
105+90106const defaultCodexCommandDeps: CodexCommandDeps = {
91107 codexControlRequest,
92108listCodexAppServerModels: listAllCodexAppServerModels,
@@ -106,6 +122,12 @@ const defaultCodexCommandDeps: CodexCommandDeps = {
106122 setCodexConversationPermissions,
107123 steerCodexConversationTurn,
108124 stopCodexConversationTurn,
125+listCodexCliSessionsOnNode: async () => {
126+throw new Error("Codex CLI node sessions require Gateway node runtime.");
127+},
128+resolveCodexCliSessionForBindingOnNode: async () => {
129+throw new Error("Codex CLI node sessions require Gateway node runtime.");
130+},
109131};
110132111133type ParsedBindArgs = {
@@ -123,6 +145,20 @@ type ParsedComputerUseArgs = {
123145help?: boolean;
124146};
125147148+type ParsedCodexCliSessionsArgs = {
149+host?: string;
150+filter: string;
151+limit?: number;
152+help?: boolean;
153+};
154+155+type ParsedResumeArgs = {
156+threadId?: string;
157+host?: string;
158+bindHere?: boolean;
159+help?: boolean;
160+};
161+126162type ParsedDiagnosticsArgs =
127163| { action: "request"; note: string }
128164| { action: "confirm"; token: string }
@@ -214,6 +250,9 @@ export async function handleCodexSubcommand(
214250if (normalized === "threads") {
215251return { text: await buildThreads(deps, options.pluginConfig, rest.join(" ")) };
216252}
253+if (normalized === "sessions") {
254+return { text: await buildCodexCliSessions(deps, rest) };
255+}
217256if (normalized === "resume") {
218257return { text: await resumeThread(deps, ctx, options.pluginConfig, rest) };
219258}
@@ -437,7 +476,7 @@ async function detachConversation(
437476const current = await ctx.getCurrentConversationBinding();
438477const data = readCodexConversationBindingData(current);
439478const detached = await ctx.detachConversationBinding();
440-if (data) {
479+if (data?.kind === "codex-app-server-session") {
441480await deps.clearCodexAppServerBinding(data.sessionFile);
442481} else if (ctx.sessionFile) {
443482await deps.clearCodexAppServerBinding(ctx.sessionFile);
@@ -456,6 +495,16 @@ async function describeConversationBinding(
456495if (!current || !data) {
457496return "No Codex conversation binding is attached.";
458497}
498+if (data.kind === "codex-cli-node-session") {
499+return [
500+"Codex conversation binding:",
501+"- Mode: Codex CLI node session",
502+`- Node: ${formatCodexDisplayText(data.nodeId)}`,
503+`- Session: ${formatCodexDisplayText(data.sessionId)}`,
504+`- Workspace: ${formatCodexDisplayText(data.cwd ?? "unknown")}`,
505+"- Active run: not tracked",
506+].join("\n");
507+}
459508const threadBinding = await deps.readCodexAppServerBinding(data.sessionFile);
460509const active = deps.readCodexConversationActiveTurn(data.sessionFile);
461510return [
@@ -482,14 +531,36 @@ async function buildThreads(
482531return formatThreads(response);
483532}
484533534+async function buildCodexCliSessions(deps: CodexCommandDeps, args: string[]): Promise<string> {
535+const parsed = parseCodexCliSessionsArgs(args);
536+if (parsed.help || !parsed.host) {
537+return "Usage: /codex sessions --host <node> [filter] [--limit <n>]";
538+}
539+return formatCodexCliSessions(
540+await deps.listCodexCliSessionsOnNode({
541+requestedNode: parsed.host,
542+filter: parsed.filter,
543+limit: parsed.limit,
544+}),
545+);
546+}
547+485548async function resumeThread(
486549deps: CodexCommandDeps,
487550ctx: PluginCommandContext,
488551pluginConfig: unknown,
489552args: string[],
490553): Promise<string> {
491-const [threadId] = args;
492-const normalizedThreadId = threadId?.trim();
554+const parsed = parseResumeArgs(args);
555+const normalizedThreadId = parsed.threadId?.trim();
556+if (parsed.help) {
557+return args.includes("--help") || args.includes("-h") || parsed.host
558+ ? "Usage: /codex resume <thread-id>\nUsage: /codex resume <session-id> --host <node> --bind here"
559+ : "Usage: /codex resume <thread-id>";
560+}
561+if (parsed.host) {
562+return await bindCodexCliNodeSession(deps, ctx, parsed);
563+}
493564if (!normalizedThreadId || args.length !== 1) {
494565return "Usage: /codex resume <thread-id>";
495566}
@@ -517,6 +588,47 @@ async function resumeThread(
517588 )}.`;
518589}
519590591+async function bindCodexCliNodeSession(
592+deps: CodexCommandDeps,
593+ctx: PluginCommandContext,
594+parsed: ParsedResumeArgs,
595+): Promise<string> {
596+if (!parsed.threadId || !parsed.host || parsed.bindHere !== true) {
597+return "Usage: /codex resume <session-id> --host <node> --bind here";
598+}
599+const resolved = await deps.resolveCodexCliSessionForBindingOnNode({
600+requestedNode: parsed.host,
601+sessionId: parsed.threadId,
602+});
603+if (!resolved.session) {
604+return `No Codex CLI session ${formatCodexDisplayText(parsed.threadId)} was found on ${formatCodexDisplayText(parsed.host)}.`;
605+}
606+const nodeId = resolved.node.nodeId;
607+if (!nodeId) {
608+return "Cannot bind Codex CLI session because the selected node did not include a node id.";
609+}
610+const data = createCodexCliNodeConversationBindingData({
611+ nodeId,
612+sessionId: parsed.threadId,
613+cwd: resolved.session?.cwd,
614+});
615+const summary = `Codex CLI session ${formatCodexDisplayText(parsed.threadId)} on ${formatCodexDisplayText(nodeId)}`;
616+const request = await ctx.requestConversationBinding({
617+ summary,
618+detachHint: "/codex detach",
619+ data,
620+});
621+if (request.status === "bound") {
622+return `Bound this conversation to Codex CLI session ${formatCodexDisplayText(
623+ parsed.threadId,
624+ )} on ${formatCodexDisplayText(nodeId)}.`;
625+}
626+if (request.status === "pending") {
627+return request.reply.text ?? "Codex CLI session binding is pending approval.";
628+}
629+return formatCodexDisplayText(request.message);
630+}
631+520632async function stopConversationTurn(
521633deps: CodexCommandDeps,
522634ctx: PluginCommandContext,
@@ -628,7 +740,8 @@ async function setConversationPermissions(
628740629741async function resolveControlSessionFile(ctx: PluginCommandContext): Promise<string | undefined> {
630742const binding = await ctx.getCurrentConversationBinding();
631-return readCodexConversationBindingData(binding)?.sessionFile ?? ctx.sessionFile;
743+const data = readCodexConversationBindingData(binding);
744+return data?.kind === "codex-app-server-session" ? data.sessionFile : ctx.sessionFile;
632745}
633746634747async function handleCodexDiagnosticsFeedback(
@@ -1613,6 +1726,86 @@ function parseBindArgs(args: string[]): ParsedBindArgs {
16131726return parsed;
16141727}
161517281729+function parseCodexCliSessionsArgs(args: string[]): ParsedCodexCliSessionsArgs {
1730+const parsed: ParsedCodexCliSessionsArgs = { filter: "" };
1731+const filter: string[] = [];
1732+for (let index = 0; index < args.length; index += 1) {
1733+const arg = args[index];
1734+if (arg === "--help" || arg === "-h") {
1735+parsed.help = true;
1736+continue;
1737+}
1738+if (arg === "--host" || arg === "--node") {
1739+const value = readRequiredOptionValue(args, index);
1740+if (!value || parsed.host !== undefined) {
1741+parsed.help = true;
1742+continue;
1743+}
1744+parsed.host = value;
1745+index += 1;
1746+continue;
1747+}
1748+if (arg === "--limit") {
1749+const value = readRequiredOptionValue(args, index);
1750+const parsedLimit = value ? Number.parseInt(value, 10) : Number.NaN;
1751+if (!Number.isFinite(parsedLimit) || parsedLimit <= 0) {
1752+parsed.help = true;
1753+continue;
1754+}
1755+parsed.limit = parsedLimit;
1756+index += 1;
1757+continue;
1758+}
1759+if (arg.startsWith("-")) {
1760+parsed.help = true;
1761+continue;
1762+}
1763+filter.push(arg);
1764+}
1765+parsed.host = normalizeOptionalString(parsed.host);
1766+parsed.filter = filter.join(" ").trim();
1767+return parsed;
1768+}
1769+1770+function parseResumeArgs(args: string[]): ParsedResumeArgs {
1771+const parsed: ParsedResumeArgs = {};
1772+for (let index = 0; index < args.length; index += 1) {
1773+const arg = args[index];
1774+if (arg === "--help" || arg === "-h") {
1775+parsed.help = true;
1776+continue;
1777+}
1778+if (arg === "--host" || arg === "--node") {
1779+const value = readRequiredOptionValue(args, index);
1780+if (!value || parsed.host !== undefined) {
1781+parsed.help = true;
1782+continue;
1783+}
1784+parsed.host = value;
1785+index += 1;
1786+continue;
1787+}
1788+if (arg === "--bind") {
1789+const value = readRequiredOptionValue(args, index);
1790+if (value !== "here" || parsed.bindHere !== undefined) {
1791+parsed.help = true;
1792+continue;
1793+}
1794+parsed.bindHere = true;
1795+index += 1;
1796+continue;
1797+}
1798+if (!arg.startsWith("-") && !parsed.threadId) {
1799+parsed.threadId = arg;
1800+continue;
1801+}
1802+parsed.help = true;
1803+}
1804+parsed.threadId = normalizeOptionalString(parsed.threadId);
1805+parsed.host = normalizeOptionalString(parsed.host);
1806+return parsed;
1807+}
1808+16161809function parseComputerUseArgs(args: string[]): ParsedComputerUseArgs {
16171810const parsed: ParsedComputerUseArgs = {
16181811action: "status",
此内容由惯性聚合(RSS阅读器)自动聚合整理,仅供阅读参考。 原文来自 — 版权归原作者所有。