@@ -36,6 +36,7 @@ import {
3636 type SafeValue ,
3737} from "./command-rpc.js" ;
3838import {
39+ createCodexCliNodeConversationBindingData ,
3940 readCodexConversationBindingData ,
4041 resolveCodexDefaultWorkspaceDir ,
4142 startCodexConversationThread ,
@@ -51,6 +52,11 @@ import {
5152 steerCodexConversationTurn ,
5253 stopCodexConversationTurn ,
5354} from "./conversation-control.js" ;
55+ import {
56+ formatCodexCliSessions ,
57+ listCodexCliSessionsOnNode ,
58+ resolveCodexCliSessionForBindingOnNode ,
59+ } from "./node-cli-sessions.js" ;
5460
5561export type CodexCommandDeps = {
5662 codexControlRequest : CodexControlRequestFn ;
@@ -71,6 +77,8 @@ export type CodexCommandDeps = {
7177 setCodexConversationPermissions : typeof setCodexConversationPermissions ;
7278 steerCodexConversationTurn : typeof steerCodexConversationTurn ;
7379 stopCodexConversationTurn : typeof stopCodexConversationTurn ;
80+ listCodexCliSessionsOnNode : ListCodexCliSessionsOnNodeFn ;
81+ resolveCodexCliSessionForBindingOnNode : ResolveCodexCliSessionForBindingOnNodeFn ;
7482} ;
7583
7684type CodexControlRequestFn = (
@@ -87,6 +95,14 @@ type SafeCodexControlRequestFn = (
8795 options ?: CodexControlRequestOptions ,
8896) => Promise < SafeValue < JsonValue | undefined > > ;
8997
98+ 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,
92108 listCodexAppServerModels : 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} ;
110132
111133type ParsedBindArgs = {
@@ -123,6 +145,20 @@ type ParsedComputerUseArgs = {
123145 help ?: boolean ;
124146} ;
125147
148+ 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(
214250 if ( normalized === "threads" ) {
215251 return { text : await buildThreads ( deps , options . pluginConfig , rest . join ( " " ) ) } ;
216252 }
253+ if ( normalized === "sessions" ) {
254+ return { text : await buildCodexCliSessions ( deps , rest ) } ;
255+ }
217256 if ( normalized === "resume" ) {
218257 return { text : await resumeThread ( deps , ctx , options . pluginConfig , rest ) } ;
219258 }
@@ -437,7 +476,7 @@ async function detachConversation(
437476 const current = await ctx . getCurrentConversationBinding ( ) ;
438477 const data = readCodexConversationBindingData ( current ) ;
439478 const detached = await ctx . detachConversationBinding ( ) ;
440- if ( data ) {
479+ if ( data ?. kind === "codex-app-server-session" ) {
441480 await deps . clearCodexAppServerBinding ( data . sessionFile ) ;
442481 } else if ( ctx . sessionFile ) {
443482 await deps . clearCodexAppServerBinding ( ctx . sessionFile ) ;
@@ -456,6 +495,16 @@ async function describeConversationBinding(
456495 if ( ! current || ! data ) {
457496 return "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+ }
459508 const threadBinding = await deps . readCodexAppServerBinding ( data . sessionFile ) ;
460509 const active = deps . readCodexConversationActiveTurn ( data . sessionFile ) ;
461510 return [
@@ -482,14 +531,36 @@ async function buildThreads(
482531 return formatThreads ( response ) ;
483532}
484533
534+ 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 (
486549 deps : CodexCommandDeps ,
487550 ctx : PluginCommandContext ,
488551 pluginConfig : unknown ,
489552 args : 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+ }
493564 if ( ! normalizedThreadId || args . length !== 1 ) {
494565 return "Usage: /codex resume <thread-id>" ;
495566 }
@@ -517,6 +588,47 @@ async function resumeThread(
517588 ) } .`;
518589}
519590
591+ 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 (
521633 deps : CodexCommandDeps ,
522634 ctx : PluginCommandContext ,
@@ -628,7 +740,8 @@ async function setConversationPermissions(
628740
629741async function resolveControlSessionFile ( ctx : PluginCommandContext ) : Promise < string | undefined > {
630742 const 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}
633746
634747async function handleCodexDiagnosticsFeedback (
@@ -1613,6 +1726,86 @@ function parseBindArgs(args: string[]): ParsedBindArgs {
16131726 return parsed ;
16141727}
16151728
1729+ 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 {
16171810 const parsed : ParsedComputerUseArgs = {
16181811 action : "status" ,
0 commit comments