Skip to content

feat(cli): add multi-session management subcommands for Codex CLI #522

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Open
wants to merge 48 commits into
base: main
Choose a base branch
from
Open
Changes from all commits
Commits
Show all changes
48 commits
Select commit Hold shift + click to select a range
f73ed9c
feat(session): add multi-session management subcommands with custom n…
kshitizz36 Apr 22, 2025
672c679
Merge remote-tracking branch 'origin/main' into feature/multi-session…
kshitizz36 Apr 22, 2025
2258840
Merge branch 'openai:main' into feature/multi-session-cli
kshitizz36 Apr 22, 2025
dd9c165
Merge branch 'main' into feature/multi-session-cli
kshitizz36 Apr 22, 2025
e6ef157
Merge branch 'main' into feature/multi-session-cli
kshitizz36 Apr 22, 2025
0dd840e
fix(session-manager): Resolve type and test errors
kshitizz36 Apr 22, 2025
981a810
fix the conflicts
kshitizz36 Apr 22, 2025
986be04
Delete codex-cli/package-lock.json
kshitizz36 Apr 22, 2025
64465fd
Merge branch 'main' into feature/multi-session-cli
kshitizz36 Apr 22, 2025
0dea9ef
Merge branch 'main' into feature/multi-session-cli
kshitizz36 Apr 22, 2025
067a41a
Merge branch 'main' into feature/multi-session-cli
kshitizz36 Apr 22, 2025
f1aa3d2
Merge branch 'main' into feature/multi-session-cli
kshitizz36 Apr 22, 2025
0ddd166
Merge branch 'main' into feature/multi-session-cli
kshitizz36 Apr 22, 2025
1aa2770
Merge branch 'main' into feature/multi-session-cli
kshitizz36 Apr 23, 2025
2f82157
Merge branch 'main' into feature/multi-session-cli
kshitizz36 Apr 23, 2025
1c99039
Merge branch 'main' into feature/multi-session-cli
kshitizz36 Apr 23, 2025
3dd5701
Merge branch 'main' into feature/multi-session-cli
kshitizz36 Apr 23, 2025
0ebacc1
Merge branch 'main' into feature/multi-session-cli
kshitizz36 Apr 24, 2025
8b8a348
Merge branch 'main' into feature/multi-session-cli
kshitizz36 Apr 24, 2025
e02c6e3
Merge branch 'main' into feature/multi-session-cli
kshitizz36 Apr 24, 2025
043b4e1
Merge branch 'main' into feature/multi-session-cli
kshitizz36 Apr 24, 2025
e6af66f
Merge branch 'main' into feature/multi-session-cli
kshitizz36 Apr 25, 2025
4c4125f
Merge branch 'main' into feature/multi-session-cli
kshitizz36 Apr 25, 2025
a46b381
Merge branch 'main' into feature/multi-session-cli
kshitizz36 Apr 25, 2025
1c9063b
Merge branch 'main' into feature/multi-session-cli
kshitizz36 Apr 25, 2025
3c50d0e
Merge branch 'main' into feature/multi-session-cli
kshitizz36 Apr 25, 2025
570eef8
Merge branch 'main' into feature/multi-session-cli
kshitizz36 Apr 25, 2025
3e4a1ef
Merge branch 'main' into feature/multi-session-cli
kshitizz36 Apr 25, 2025
f43d079
Merge branch 'main' into feature/multi-session-cli
kshitizz36 Apr 25, 2025
f8d1f8f
Merge branch 'main' into feature/multi-session-cli
kshitizz36 Apr 26, 2025
d9ba950
Merge branch 'main' into feature/multi-session-cli
kshitizz36 Apr 26, 2025
6133e94
Merge branch 'main' into feature/multi-session-cli
kshitizz36 Apr 27, 2025
eb969ec
Merge branch 'main' into feature/multi-session-cli
kshitizz36 Apr 28, 2025
04a4c35
Merge branch 'main' into feature/multi-session-cli
kshitizz36 Apr 28, 2025
203f1b0
Merge branch 'main' into feature/multi-session-cli
kshitizz36 Apr 28, 2025
4ab866d
Merge branch 'main' into feature/multi-session-cli
kshitizz36 Apr 28, 2025
5497d85
Merge branch 'main' into feature/multi-session-cli
kshitizz36 Apr 29, 2025
f0c1712
Merge branch 'main' into feature/multi-session-cli
kshitizz36 Apr 29, 2025
938504a
Merge branch 'main' into feature/multi-session-cli
kshitizz36 Apr 30, 2025
9afb7f2
Merge branch 'main' into feature/multi-session-cli
kshitizz36 May 1, 2025
8bba6c8
Merge branch 'main' into feature/multi-session-cli
kshitizz36 May 1, 2025
1c6a10d
Merge branch 'main' into feature/multi-session-cli
kshitizz36 May 2, 2025
653e709
Merge branch 'main' into feature/multi-session-cli
kshitizz36 May 3, 2025
81677d8
Merge branch 'main' into feature/multi-session-cli
kshitizz36 May 4, 2025
8030244
Merge branch 'main' into feature/multi-session-cli
kshitizz36 May 9, 2025
2cbb7b3
Merge branch 'main' into feature/multi-session-cli
kshitizz36 May 9, 2025
0e58812
Merge branch 'main' into feature/multi-session-cli
kshitizz36 May 11, 2025
d569d7c
Merge branch 'main' into feature/multi-session-cli
kshitizz36 May 14, 2025
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions codex-cli/package.json
Original file line number Diff line number Diff line change
@@ -64,6 +64,7 @@
"@typescript-eslint/parser": "^7.18.0",
"boxen": "^8.0.1",
"esbuild": "^0.25.2",
"eslint": "^9.25.1",
"eslint-plugin-import": "^2.31.0",
"eslint-plugin-react": "^7.32.2",
"eslint-plugin-react-hooks": "^4.6.0",
125 changes: 124 additions & 1 deletion codex-cli/src/cli.tsx
Original file line number Diff line number Diff line change
@@ -28,6 +28,13 @@ import { createInputItem } from "./utils/input-utils";
import { initLogger } from "./utils/logger/log";
import { isModelSupportedForResponses } from "./utils/model-utils.js";
import { parseToolCall } from "./utils/parsers";
import { setSessionId, CLI_VERSION } from "./utils/session";
import {
listSessions,
saveSession,
loadSession,
deleteSession,
} from "./utils/session-manager";
import { onExit, setInkRenderer } from "./utils/terminal";
import chalk from "chalk";
import { spawnSync } from "child_process";
@@ -48,9 +55,10 @@ initLogger();

const cli = meow(
`
Usage
Usage
$ codex [options] <prompt>
$ codex completion <bash|zsh|fish>
$ codex session <list|save|load|delete> [id]

Options
--version Print version and exit
@@ -81,6 +89,12 @@ const cli = meow(

--reasoning <effort> Set the reasoning effort level (low, medium, high) (default: high)

Session Management Commands
$ codex session list List all saved sessions
$ codex session save [name] Save current session with optional name
$ codex session load <id> Load a previously saved session
$ codex session delete <id> Delete a saved session

Dangerous options
--dangerously-auto-approve-everything
Skip all confirmation prompts and execute commands without
@@ -95,6 +109,9 @@ const cli = meow(
$ codex "Write and run a python program that prints ASCII art"
$ codex -q "fix build issues"
$ codex completion bash
$ codex session list
$ codex session save my-feature-branch
$ codex session load session-1234567890
`,
{
importMeta: import.meta,
@@ -228,6 +245,112 @@ complete -c codex -a '(__fish_complete_path)' -d 'file path'`,
console.log(script);
process.exit(0);
}
// Handle 'session' subcommand for session management
if (cli.input[0] === "session") {
const action = cli.input[1];
const sessionArg = cli.input[2]; // may be session ID or name provided by the user

// Ensure configuration exists before handling session commands
let config;
try {
config = loadConfig(undefined, undefined, {
cwd: process.cwd(),
disableProjectDoc: Boolean(cli.flags.noProjectDoc),
projectDocPath: cli.flags.projectDoc as string | undefined,
isFullContext: false,
});
} catch (e) {
// If config couldn't be loaded, create minimal config for session management
config = {
model: cli.flags.model || "o4-mini",
provider: cli.flags.provider || "openai",
};
}

switch (action) {
case "list": {
const sessions = listSessions();
if (sessions.length === 0) {
// eslint-disable-next-line no-console
console.log("No sessions found.");
} else {
// eslint-disable-next-line no-console
console.log("Available Sessions:");
sessions.forEach((s) => {
// eslint-disable-next-line no-console
console.log(
`ID: ${s.id}, User: ${s.user || "unknown"}, Model: ${
s.model
}, Timestamp: ${s.timestamp}`,
);
});
}
process.exit(0);
break;
}
case "save": {
// Create a new session from the current configuration context
const newSession = {
id: sessionArg || Date.now().toString(),
user: process.env["USER"] || "unknown",
version: CLI_VERSION,
model: config.model,
timestamp: new Date().toISOString(),
instructions: config.instructions || "",
cwd: process.cwd(),
firstPrompt: cli.input[3] || "No prompt provided",
};
saveSession(newSession);
// eslint-disable-next-line no-console
console.log(`Session saved with ID: ${newSession.id}`);
process.exit(0);
break;
}
case "load": {
if (!sessionArg) {
// eslint-disable-next-line no-console
console.error("Please provide a session ID to load.");
process.exit(1);
}
const sess = loadSession(sessionArg);
if (sess) {
setSessionId(sess.id); // update the global session id
// eslint-disable-next-line no-console
console.log(`Loaded session ${sess.id} (${sess.model})`);
} else {
// eslint-disable-next-line no-console
console.error(`Session with ID ${sessionArg} not found.`);
process.exit(1);
}
process.exit(0);
break;
}
case "delete": {
if (!sessionArg) {
// eslint-disable-next-line no-console
console.error("Please provide a session ID to delete.");
process.exit(1);
}
const success = deleteSession(sessionArg);
if (success) {
// eslint-disable-next-line no-console
console.log(`Deleted session ${sessionArg}`);
} else {
// eslint-disable-next-line no-console
console.error(`Session with ID ${sessionArg} not found.`);
process.exit(1);
}
process.exit(0);
break;
}
default:
// eslint-disable-next-line no-console
console.error(
"Invalid session action. Use one of: list, save [name], load <id>, delete <id>.",
);
process.exit(1);
}
}

// For --help, show help and exit.
if (cli.flags.help) {
151 changes: 151 additions & 0 deletions codex-cli/src/utils/session-manager.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,151 @@
import type { TerminalChatSession } from "./session";

import fs from "fs"; // Then built-in modules like fs, os, path
import os from "os";
import path from "path";

// Constants for session storage
const sessionsDir = path.join(os.homedir(), ".codex");
const sessionsFile = path.join(sessionsDir, "sessions.json");
const BACKUP_SESSION_FILE = path.join(sessionsDir, "sessions.backup.json");

/**
* Ensure the sessions directory and file exist.
* Creates the directory and empty sessions file if they don't exist.
*/
function ensureSessionsFileExists(): void {
if (!fs.existsSync(sessionsDir)) {
fs.mkdirSync(sessionsDir, { recursive: true });
}

if (!fs.existsSync(sessionsFile)) {
fs.writeFileSync(sessionsFile, JSON.stringify([]), "utf-8");
}
}

/**
* Read sessions from the sessions file
* @returns Array of TerminalChatSession objects
*/
function readSessions(): Array<TerminalChatSession> {
ensureSessionsFileExists();

try {
const raw = fs.readFileSync(sessionsFile, "utf-8");
const sessions = JSON.parse(raw);
return Array.isArray(sessions) ? sessions : [];
} catch (error) {
// If there's an error reading the main file, try to recover from backup
if (fs.existsSync(BACKUP_SESSION_FILE)) {
try {
const backupRaw = fs.readFileSync(BACKUP_SESSION_FILE, "utf-8");
const backupSessions = JSON.parse(backupRaw);
return Array.isArray(backupSessions) ? backupSessions : [];
} catch {
// If backup also fails, return empty array
return [];
}
}
return [];
}
}

/**
* Write sessions to the sessions file with backup
* @param sessions Array of sessions to write
*/
function writeSessions(sessions: Array<TerminalChatSession>): void {
ensureSessionsFileExists();

// First create a backup of the current file if it exists and has content
if (fs.existsSync(sessionsFile)) {
try {
const currentContent = fs.readFileSync(sessionsFile, "utf-8");
if (currentContent.trim()) {
fs.writeFileSync(BACKUP_SESSION_FILE, currentContent, "utf-8");
}
} catch {
// Ignore backup creation errors
}
}

// Now write the new content
try {
fs.writeFileSync(sessionsFile, JSON.stringify(sessions, null, 2), "utf-8");
} catch (error) {
// Remove console.error statement
}
}

/**
* List all available sessions
* @returns Array of TerminalChatSession objects
*/
export function listSessions(): Array<TerminalChatSession> {
return readSessions();
}

/**
* Save a session, replacing an existing one with the same ID or adding a new one
* @param newSession The session to save
*/
export function saveSession(newSession: TerminalChatSession): void {
const sessions = readSessions();

// Replace an existing session if its id matches; otherwise, add new
const index = sessions.findIndex((session) => session.id === newSession.id);
if (index >= 0) {
sessions[index] = newSession;
} else {
sessions.push(newSession);
}

writeSessions(sessions);
}

/**
* Load a session by ID
* @param sessionId The ID of the session to load
* @returns The session if found, null otherwise
*/
export function loadSession(sessionId: string): TerminalChatSession | null {
const sessions = readSessions();
return sessions.find((s) => s.id === sessionId) || null;
}

/**
* Delete a session by ID
* @param sessionId The ID of the session to delete
* @returns true if the session was deleted, false if it wasn't found
*/
export function deleteSession(sessionId: string): boolean {
let sessions = readSessions();
const initialLength = sessions.length;
sessions = sessions.filter((s) => s.id !== sessionId);

// Only write if something was actually removed
if (sessions.length < initialLength) {
writeSessions(sessions);
return true;
}

return false;
}

/**
* Create a session backup
* Useful for emergency recovery situations
*/
export function createSessionBackup(): boolean {
try {
const sessions = readSessions();
const backupPath = path.join(
sessionsDir,
`sessions.backup.${Date.now()}.json`,
);
fs.writeFileSync(backupPath, JSON.stringify(sessions, null, 2), "utf-8");
return true;
} catch {
return false;
}
}
4 changes: 4 additions & 0 deletions codex-cli/src/utils/session.ts
Original file line number Diff line number Diff line change
@@ -13,6 +13,10 @@ export type TerminalChatSession = {
timestamp: string;
/** Optional custom instructions that were active for the run */
instructions: string;
/** The working directory where the session was saved */
cwd: string;
/** The content of the first prompt in the session */
firstPrompt: string;
};

let sessionId = "";
Loading