Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
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
3 changes: 3 additions & 0 deletions src/mux-interface.ts
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@ import type {
OpenCodeConfig,
CodexConfig,
EffortLevel,
GeminiConfig,
} from './types.js';

/**
Expand Down Expand Up @@ -64,6 +65,7 @@ export interface CreateSessionOptions {
allowedTools?: string;
openCodeConfig?: OpenCodeConfig;
codexConfig?: CodexConfig;
geminiConfig?: GeminiConfig;
/** When restoring after reboot, resume a previous Claude conversation by its session ID */
resumeSessionId?: string;
/** Extra env vars exported before launching the CLI (e.g., CLAUDE_CODE_EXPERIMENTAL_AGENT_TEAMS). Ephemeral — not written to disk. */
Expand All @@ -83,6 +85,7 @@ export interface RespawnPaneOptions {
allowedTools?: string;
openCodeConfig?: OpenCodeConfig;
codexConfig?: CodexConfig;
geminiConfig?: GeminiConfig;
/** Resume a previous Claude conversation when respawning */
resumeSessionId?: string;
/** Extra env vars exported before launching the CLI (preserved across respawns). */
Expand Down
36 changes: 34 additions & 2 deletions src/session.ts
Original file line number Diff line number Diff line change
Expand Up @@ -48,6 +48,7 @@ import {
type OpenCodeConfig,
type CodexConfig,
type EffortLevel,
type GeminiConfig,
} from './types.js';
import type { TerminalMultiplexer, MuxSession } from './mux-interface.js';
import { TaskTracker, type BackgroundTask } from './task-tracker.js';
Expand Down Expand Up @@ -133,7 +134,22 @@ const NEWLINE_SPLIT_PATTERN = /\r?\n/;

/** True for external-CLI run modes (non-Claude) that use their own TUI and output format. */
export function isExternalCliMode(mode: SessionMode): boolean {
return mode === 'opencode' || mode === 'codex';
return mode === 'opencode' || mode === 'codex' || mode === 'gemini';
}

function getModeLabel(mode: SessionMode): string {
switch (mode) {
case 'opencode':
return 'OpenCode';
case 'codex':
return 'Codex';
case 'gemini':
return 'Gemini';
case 'shell':
return 'Shell';
case 'claude':
return 'Claude';
}
}

/**
Expand Down Expand Up @@ -351,6 +367,8 @@ export class Session extends EventEmitter {
private _openCodeConfig: OpenCodeConfig | undefined;
// Codex configuration (only for mode === 'codex')
private _codexConfig: CodexConfig | undefined;
// Gemini configuration (only for mode === 'gemini')
private _geminiConfig: GeminiConfig | undefined;
private _resumeSessionId: string | undefined;

// Ephemeral env overrides (e.g., CLAUDE_CODE_EXPERIMENTAL_AGENT_TEAMS). Exported by tmux
Expand Down Expand Up @@ -421,6 +439,8 @@ export class Session extends EventEmitter {
openCodeConfig?: OpenCodeConfig;
/** Codex configuration (only for mode === 'codex') */
codexConfig?: CodexConfig;
/** Gemini configuration (only for mode === 'gemini') */
geminiConfig?: GeminiConfig;
/** Resume a previous Claude conversation (used after server reboot) */
resumeSessionId?: string;
/** Extra env vars exported to the CLI at spawn time (no disk persistence) */
Expand Down Expand Up @@ -481,6 +501,11 @@ export class Session extends EventEmitter {
this._codexConfig = config.codexConfig;
}

// Apply Gemini configuration
if (config.geminiConfig) {
this._geminiConfig = config.geminiConfig;
}

// Apply env overrides (exported at spawn, not persisted to disk).
// Legacy migration: pre-0.7.2 carried effort as the CLAUDE_CODE_EFFORT_LEVEL env var,
// which hard-locks /effort switching. Extract it into _effort (--settings soft default)
Expand Down Expand Up @@ -985,6 +1010,7 @@ export class Session extends EventEmitter {
cliLatestVersion: this._cliLatestVersion || undefined,
openCodeConfig: this._openCodeConfig,
codexConfig: this._codexConfig,
geminiConfig: this._geminiConfig,
resumeSessionId: this._resumeSessionId,
effort: this._effort,
attachmentHistory: this.attachmentHistory.length > 0 ? this.attachmentHistory : undefined,
Expand Down Expand Up @@ -1224,7 +1250,7 @@ export class Session extends EventEmitter {

this._resetBuffers();

const modeLabel = this.mode === 'opencode' ? 'OpenCode' : this.mode === 'codex' ? 'Codex' : 'Claude';
const modeLabel = getModeLabel(this.mode);
console.log(
`[Session] Starting interactive ${modeLabel} session` + (this._useMux ? ` (with ${this._mux!.backend})` : '')
);
Expand All @@ -1243,6 +1269,7 @@ export class Session extends EventEmitter {
allowedTools: this._allowedTools,
openCodeConfig: this._openCodeConfig,
codexConfig: this._codexConfig,
geminiConfig: this._geminiConfig,
resumeSessionId: this._resumeSessionId,
envOverrides: this._envOverrides,
effort: this._effort,
Expand All @@ -1258,6 +1285,7 @@ export class Session extends EventEmitter {
allowedTools: this._allowedTools,
openCodeConfig: this._openCodeConfig,
codexConfig: this._codexConfig,
geminiConfig: this._geminiConfig,
resumeSessionId: this._resumeSessionId,
envOverrides: this._envOverrides,
effort: this._effort,
Expand Down Expand Up @@ -1331,6 +1359,10 @@ export class Session extends EventEmitter {
if (this.mode === 'codex') {
throw new Error('Codex sessions require tmux. Direct PTY fallback is not supported.');
}
// Gemini sessions require tmux for Gemini/Google auth env injection via setenv
if (this.mode === 'gemini') {
throw new Error('Gemini sessions require tmux. Direct PTY fallback is not supported.');
}
try {
// Pass --session-id to use the SAME ID as the Codeman session
// This ensures subagents can be directly matched to the correct tab
Expand Down
104 changes: 103 additions & 1 deletion src/tmux-manager.ts
Original file line number Diff line number Diff line change
Expand Up @@ -41,9 +41,17 @@ import {
type OpenCodeConfig,
type CodexConfig,
type EffortLevel,
type GeminiConfig,
} from './types.js';
import { buildEffortCliArgs } from './session-cli-builder.js';
import { wrapWithNice, SAFE_PATH_PATTERN, findClaudeDir, resolveOpenCodeDir, resolveCodexDir } from './utils/index.js';
import {
wrapWithNice,
SAFE_PATH_PATTERN,
findClaudeDir,
resolveOpenCodeDir,
resolveCodexDir,
resolveGeminiDir,
} from './utils/index.js';
import type {
TerminalMultiplexer,
MuxSession,
Expand Down Expand Up @@ -571,6 +579,35 @@ export function buildCodexCommand(config?: CodexConfig): string {
return parts.join(' ');
}

/**
* Build the Gemini CLI command with appropriate flags.
*
* `--skip-trust` avoids a first-run workspace trust prompt inside Codeman.
* Approval mode defaults to `yolo` for parity with Codeman's Claude default
* of `--dangerously-skip-permissions`; users can override it later through
* Gemini config once Codeman exposes richer Gemini settings.
*/
function buildGeminiCommand(config?: GeminiConfig): string {
const parts = ['gemini', '--skip-trust'];

const approvalMode = config?.approvalMode || 'yolo';
if (['default', 'auto_edit', 'yolo', 'plan'].includes(approvalMode)) {
parts.push('--approval-mode', approvalMode);
}

if (config?.model) {
const safeModel = /^[a-zA-Z0-9._\-/]+$/.test(config.model) ? config.model : undefined;
if (safeModel) parts.push('--model', safeModel);
}

if (config?.resumeSession) {
const safeId = /^[a-zA-Z0-9._-]+$/.test(config.resumeSession) ? config.resumeSession : undefined;
if (safeId) parts.push('--resume', safeId);
}

return parts.join(' ');
}

/**
* Build the spawn command for any session mode.
* Shared by createSession() and respawnPane() to avoid duplication.
Expand All @@ -597,6 +634,7 @@ function buildSpawnCommand(options: {
allowedTools?: string;
openCodeConfig?: OpenCodeConfig;
codexConfig?: CodexConfig;
geminiConfig?: GeminiConfig;
resumeSessionId?: string;
effort?: EffortLevel;
}): string {
Expand Down Expand Up @@ -624,6 +662,9 @@ function buildSpawnCommand(options: {
if (options.mode === 'codex') {
return buildCodexCommand(options.codexConfig);
}
if (options.mode === 'gemini') {
return buildGeminiCommand(options.geminiConfig);
}
return '$SHELL';
}

Expand Down Expand Up @@ -674,6 +715,38 @@ function setCodexEnvVars(tmuxCmd: string, muxName: string): void {
}
}

/**
* Set sensitive environment variables for Gemini on a tmux session via setenv.
* Gemini Pro/Ultra users usually authenticate via cached Google login; these
* variables cover API-key and Vertex AI paths without putting secrets in ps.
*/
function setGeminiEnvVars(muxName: string): void {
const sensitiveVars = [
'GEMINI_API_KEY',
'GEMINI_MODEL',
'GOOGLE_API_KEY',
'GOOGLE_CLOUD_PROJECT',
'GOOGLE_CLOUD_LOCATION',
'GOOGLE_APPLICATION_CREDENTIALS',
'GOOGLE_GENAI_USE_VERTEXAI',
];
for (const key of sensitiveVars) {
const val = process.env[key];
if (val) {
const escaped = val.replace(/'/g, "'\\''");
try {
execSync(`tmux setenv -t '${muxName}' ${key} '${escaped}'`, {
encoding: 'utf8',
timeout: EXEC_TIMEOUT_MS,
stdio: ['pipe', 'pipe', 'pipe'],
});
} catch {
/* Non-critical — key may not be needed */
}
}
}
}

/**
* Set OPENCODE_CONFIG_CONTENT on a tmux session via setenv.
* Uses tmux setenv to avoid shell metacharacter injection from user-supplied JSON.
Expand Down Expand Up @@ -930,6 +1003,10 @@ export class TmuxManager extends EventEmitter implements TerminalMultiplexer {
const dir = resolveCodexDir();
return { pathExport: dir ? `export PATH="${dir}:$PATH" && ` : '', dir };
}
if (mode === 'gemini') {
const dir = resolveGeminiDir();
return { pathExport: dir ? `export PATH="${dir}:$PATH" && ` : '', dir };
}
return { pathExport: '', dir: null };
}

Expand All @@ -953,6 +1030,13 @@ export class TmuxManager extends EventEmitter implements TerminalMultiplexer {
setCodexEnvVars(this.tmux(), muxName);
}

/**
* Configure Gemini-specific environment on a tmux session.
*/
private _configureGemini(muxName: string): void {
setGeminiEnvVars(muxName);
}

/**
* Creates a new tmux session wrapping Claude CLI or a shell.
* In test mode: creates an in-memory session only (no real tmux session).
Expand All @@ -969,6 +1053,7 @@ export class TmuxManager extends EventEmitter implements TerminalMultiplexer {
allowedTools,
openCodeConfig,
codexConfig,
geminiConfig,
resumeSessionId,
envOverrides,
effort,
Expand Down Expand Up @@ -1007,6 +1092,12 @@ export class TmuxManager extends EventEmitter implements TerminalMultiplexer {
if (mode === 'opencode' && !cliDir) {
throw new Error('OpenCode CLI not found. Install with: curl -fsSL https://opencode.ai/install | bash');
}
if (mode === 'codex' && !cliDir) {
throw new Error('Codex CLI not found. Install with: npm install -g @openai/codex');
}
if (mode === 'gemini' && !cliDir) {
throw new Error('Gemini CLI not found. Install with: npm install -g @google/gemini-cli');
}

const envExportsStr = this.buildEnvExports(sessionId, muxName, mode).join(' && ');

Expand All @@ -1018,6 +1109,7 @@ export class TmuxManager extends EventEmitter implements TerminalMultiplexer {
allowedTools,
openCodeConfig,
codexConfig,
geminiConfig,
resumeSessionId,
effort,
});
Expand Down Expand Up @@ -1067,6 +1159,10 @@ export class TmuxManager extends EventEmitter implements TerminalMultiplexer {
} else if (mode === 'codex') {
this._configureCodex(muxName);
}
// For Gemini: set Gemini/Google auth env vars via tmux setenv
if (mode === 'gemini') {
this._configureGemini(muxName);
}

// Apply user-supplied env overrides (e.g., CLAUDE_CODE_EFFORT_LEVEL) via tmux setenv
// so secret values stay off the bash command line. Must run before respawn-pane.
Expand Down Expand Up @@ -1224,6 +1320,7 @@ export class TmuxManager extends EventEmitter implements TerminalMultiplexer {
allowedTools,
openCodeConfig,
codexConfig,
geminiConfig,
resumeSessionId,
envOverrides,
effort,
Expand All @@ -1247,6 +1344,7 @@ export class TmuxManager extends EventEmitter implements TerminalMultiplexer {
allowedTools,
openCodeConfig,
codexConfig,
geminiConfig,
resumeSessionId,
effort,
});
Expand All @@ -1261,6 +1359,10 @@ export class TmuxManager extends EventEmitter implements TerminalMultiplexer {
} else if (mode === 'codex') {
this._configureCodex(muxName);
}
// For Gemini: set Gemini/Google auth env vars via tmux setenv before respawn
if (mode === 'gemini') {
this._configureGemini(muxName);
}

// Re-apply user env overrides before respawn so the new shell inherits them.
this.applyEnvOverrides(muxName, envOverrides);
Expand Down
18 changes: 16 additions & 2 deletions src/types/session.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,10 +8,12 @@
* - SessionConfig — creation-time config (id, workingDir, createdAt)
* - SessionOutput — captured stdout/stderr/exitCode
* - SessionStatus — 'idle' | 'busy' | 'stopped' | 'error'
* - SessionMode — 'claude' | 'shell' | 'opencode' | 'codex' (which CLI backend)
* - SessionMode — 'claude' | 'shell' | 'opencode' | 'codex' | 'gemini' (which CLI backend)
* - ClaudeMode — CLI permission mode ('dangerously-skip-permissions' | 'normal' | 'allowedTools')
* - SessionColor — visual differentiation color
* - OpenCodeConfig — OpenCode-specific settings (model, autoAllowTools, continueSession)
* - CodexConfig — Codex (OpenAI CLI)-specific settings (model, resumeSessionId)
* - GeminiConfig — Gemini CLI-specific settings (model, approvalMode, resumeSession)
*
* Cross-domain relationships:
* - SessionState.respawnConfig embeds RespawnConfig (respawn domain)
Expand Down Expand Up @@ -39,7 +41,7 @@ export type SessionStatus = 'idle' | 'busy' | 'stopped' | 'error';
export type ClaudeMode = 'dangerously-skip-permissions' | 'normal' | 'allowedTools';

/** Session mode: which CLI backend a session runs */
export type SessionMode = 'claude' | 'shell' | 'opencode' | 'codex';
export type SessionMode = 'claude' | 'shell' | 'opencode' | 'codex' | 'gemini';

/**
* Valid Claude CLI effort levels (claude >= 2.1.154).
Expand Down Expand Up @@ -85,6 +87,16 @@ export interface CodexConfig {
renderMode?: CodexRenderMode;
}

/** Gemini CLI session configuration */
export interface GeminiConfig {
/** Model identifier (e.g., "gemini-2.5-pro"). Passed via --model. */
model?: string;
/** Gemini approval mode for tool calls. */
approvalMode?: 'default' | 'auto_edit' | 'yolo' | 'plan';
/** Resume a previous Gemini session ("latest", index, or session id). */
resumeSession?: string;
}

/**
* Configuration for creating a new session
*/
Expand Down Expand Up @@ -214,6 +226,8 @@ export interface SessionState {
openCodeConfig?: OpenCodeConfig;
/** Codex-specific configuration (only for mode === 'codex') */
codexConfig?: CodexConfig;
/** Gemini-specific configuration (only for mode === 'gemini') */
geminiConfig?: GeminiConfig;
/** Claude conversation session ID to resume after reboot (set by restore script) */
resumeSessionId?: string;
/** Claude CLI effort level (soft default via --settings, switchable in-session via /effort) */
Expand Down
Loading