diff --git a/cli/src/api/apiMachine.ts b/cli/src/api/apiMachine.ts index 61bffd48..1bb6f285 100644 --- a/cli/src/api/apiMachine.ts +++ b/cli/src/api/apiMachine.ts @@ -70,13 +70,21 @@ export class ApiMachineClient { setRPCHandlers({ spawnSession, stopSession, requestShutdown }: MachineRpcHandlers): void { this.rpcHandlerManager.registerHandler('spawn-happy-session', async (params: any) => { - const { directory, sessionId, machineId, approvedNewDirectoryCreation, agent, token } = params || {} + const { directory, sessionId, machineId, approvedNewDirectoryCreation, agent, yolo, token } = params || {} if (!directory) { throw new Error('Directory is required') } - const result = await spawnSession({ directory, sessionId, machineId, approvedNewDirectoryCreation, agent, token }) + const result = await spawnSession({ + directory, + sessionId, + machineId, + approvedNewDirectoryCreation, + agent, + yolo, + token + }) switch (result.type) { case 'success': diff --git a/cli/src/codex/utils/codexCliOverrides.test.ts b/cli/src/codex/utils/codexCliOverrides.test.ts index ad349515..bc2ee40a 100644 --- a/cli/src/codex/utils/codexCliOverrides.test.ts +++ b/cli/src/codex/utils/codexCliOverrides.test.ts @@ -22,6 +22,11 @@ describe('parseCodexCliOverrides', () => { approvalPolicy: 'on-request' }); + expect(parseCodexCliOverrides(['--yolo'])).toEqual({ + sandbox: 'danger-full-access', + approvalPolicy: 'never' + }); + expect(parseCodexCliOverrides(['--dangerously-bypass-approvals-and-sandbox'])).toEqual({ sandbox: 'danger-full-access', approvalPolicy: 'never' diff --git a/cli/src/codex/utils/codexCliOverrides.ts b/cli/src/codex/utils/codexCliOverrides.ts index 61ed4d1d..53a1cfc6 100644 --- a/cli/src/codex/utils/codexCliOverrides.ts +++ b/cli/src/codex/utils/codexCliOverrides.ts @@ -34,6 +34,12 @@ export function parseCodexCliOverrides(args?: string[]): CodexCliOverrides { continue; } + if (arg === '--yolo') { + overrides.approvalPolicy = 'never'; + overrides.sandbox = 'danger-full-access'; + continue; + } + if (arg === '--dangerously-bypass-approvals-and-sandbox') { overrides.approvalPolicy = 'never'; overrides.sandbox = 'danger-full-access'; diff --git a/cli/src/daemon/run.ts b/cli/src/daemon/run.ts index 500370d5..1660901e 100644 --- a/cli/src/daemon/run.ts +++ b/cli/src/daemon/run.ts @@ -186,6 +186,8 @@ export async function startDaemon(): Promise { logger.debugLargeJson('[DAEMON RUN] Spawning session', options); const { directory, sessionId, machineId, approvedNewDirectoryCreation = true } = options; + const agent = options.agent ?? 'claude'; + const yolo = options.yolo === true; let directoryCreated = false; try { @@ -256,9 +258,9 @@ export async function startDaemon(): Promise { } // Construct arguments for the CLI - const agentCommand = options.agent === 'codex' + const agentCommand = agent === 'codex' ? 'codex' - : options.agent === 'gemini' + : agent === 'gemini' ? 'gemini' : 'claude'; const args = [ @@ -266,6 +268,9 @@ export async function startDaemon(): Promise { '--hapi-starting-mode', 'remote', '--started-by', 'daemon' ]; + if (yolo) { + args.push('--yolo'); + } // TODO: In future, sessionId could be used with --resume to continue existing sessions // For now, we ignore it - each spawn creates a new session diff --git a/cli/src/index.ts b/cli/src/index.ts index 3400cc16..0a838ec8 100755 --- a/cli/src/index.ts +++ b/cli/src/index.ts @@ -147,16 +147,29 @@ import { getCliArgs } from './utils/cliArgs' } else if (subcommand === 'gemini') { // Handle gemini command try { - await import('./agent/runners/gemini'); - const { runAgentSession } = await import('./agent/runners/runAgentSession'); - let startedBy: 'daemon' | 'terminal' | undefined = undefined; + let yolo = false; for (let i = 1; i < args.length; i++) { if (args[i] === '--started-by') { startedBy = args[++i] as 'daemon' | 'terminal'; + } else if (args[i] === '--yolo') { + yolo = true; } } + if (yolo) { + const existingArgs = process.env.HAPPY_GEMINI_ARGS ?? process.env.GEMINI_ACP_ARGS ?? ''; + if (!existingArgs.includes('--yolo')) { + const nextArgs = existingArgs.trim().length > 0 + ? `${existingArgs} --yolo` + : '--yolo'; + process.env.HAPPY_GEMINI_ARGS = nextArgs; + } + } + + await import('./agent/runners/gemini'); + const { runAgentSession } = await import('./agent/runners/runAgentSession'); + await initializeToken(); await authAndSetupMachineIfNeeded(); await runAgentSession({ agentType: 'gemini', startedBy }); diff --git a/cli/src/modules/common/registerCommonHandlers.ts b/cli/src/modules/common/registerCommonHandlers.ts index 4e733336..c3796dca 100644 --- a/cli/src/modules/common/registerCommonHandlers.ts +++ b/cli/src/modules/common/registerCommonHandlers.ts @@ -122,6 +122,7 @@ export interface SpawnSessionOptions { sessionId?: string; approvedNewDirectoryCreation?: boolean; agent?: 'claude' | 'codex' | 'gemini'; + yolo?: boolean; token?: string; } diff --git a/cli/src/ui/ink/useSwitchControls.ts b/cli/src/ui/ink/useSwitchControls.ts index 71c9d3b3..5bcde5bf 100644 --- a/cli/src/ui/ink/useSwitchControls.ts +++ b/cli/src/ui/ink/useSwitchControls.ts @@ -42,6 +42,13 @@ export function useSwitchControls(opts: { }, confirmationTimeoutMs); }, [confirmationTimeoutMs, resetConfirmation]); + const readKeyString = useCallback((keyLike: unknown, prop: 'name' | 'sequence'): string | undefined => { + if (!keyLike || typeof keyLike !== 'object') return undefined; + if (!(prop in keyLike)) return undefined; + const value = (keyLike as Record)[prop]; + return typeof value === 'string' ? value : undefined; + }, []); + useEffect(() => { return () => { if (confirmationTimeoutRef.current) { @@ -78,14 +85,18 @@ export function useSwitchControls(opts: { return; } - const sequence = typeof key.sequence === 'string' ? key.sequence : input; - const isKeyRelease = typeof sequence === 'string' && /^\u001b\[[0-9;]*:3u$/.test(sequence); - const csiUMatch = typeof sequence === 'string' - ? sequence.match(/^\u001b\[(\d+)(?:;(\d+))?u$/) + const keySequence = readKeyString(key, 'sequence'); + const keyName = readKeyString(key, 'name'); + const sequence = keySequence ?? input; + const sequenceString = typeof sequence === 'string' ? sequence : ''; + const isKeyRelease = sequenceString.length > 0 + && /^\u001b\[[0-9;]*:3u$/.test(sequenceString); + const csiUMatch = sequenceString.length > 0 + ? sequenceString.match(/^\u001b\[(\d+)(?:;(\d+))?u$/) : null; const csiUCodepoint = csiUMatch ? Number(csiUMatch[1]) : null; const isCsiUSpace = csiUCodepoint === 32; - const isSpace = Boolean(onSwitch) && !isKeyRelease && (input === ' ' || key.name === 'space' || isCsiUSpace); + const isSpace = Boolean(onSwitch) && !isKeyRelease && (input === ' ' || keyName === 'space' || isCsiUSpace); const hasPrintableInput = typeof input === 'string' && input.length > 0; if (isSpace) { diff --git a/server/src/sync/syncEngine.ts b/server/src/sync/syncEngine.ts index 268e2ba5..a3462e3a 100644 --- a/server/src/sync/syncEngine.ts +++ b/server/src/sync/syncEngine.ts @@ -652,10 +652,15 @@ export class SyncEngine { async spawnSession( machineId: string, directory: string, - agent: 'claude' | 'codex' | 'gemini' = 'claude' + agent: 'claude' | 'codex' | 'gemini' = 'claude', + yolo?: boolean ): Promise<{ type: 'success'; sessionId: string } | { type: 'error'; message: string }> { try { - const result = await this.machineRpc(machineId, 'spawn-happy-session', { type: 'spawn-in-directory', directory, agent }) + const result = await this.machineRpc( + machineId, + 'spawn-happy-session', + { type: 'spawn-in-directory', directory, agent, yolo } + ) if (result && typeof result === 'object') { const obj = result as Record if (obj.type === 'success' && typeof obj.sessionId === 'string') { diff --git a/server/src/web/routes/machines.ts b/server/src/web/routes/machines.ts index 8ec71711..e1b79b93 100644 --- a/server/src/web/routes/machines.ts +++ b/server/src/web/routes/machines.ts @@ -5,7 +5,8 @@ import type { WebAppEnv } from '../middleware/auth' const spawnBodySchema = z.object({ directory: z.string().min(1), - agent: z.enum(['claude', 'codex', 'gemini']).optional() + agent: z.enum(['claude', 'codex', 'gemini']).optional(), + yolo: z.boolean().optional() }) export function createMachinesRoutes(getSyncEngine: () => SyncEngine | null): Hono { @@ -39,7 +40,12 @@ export function createMachinesRoutes(getSyncEngine: () => SyncEngine | null): Ho return c.json({ error: 'Invalid body' }, 400) } - const result = await engine.spawnSession(machineId, parsed.data.directory, parsed.data.agent) + const result = await engine.spawnSession( + machineId, + parsed.data.directory, + parsed.data.agent, + parsed.data.yolo + ) return c.json(result) }) diff --git a/web/src/api/client.ts b/web/src/api/client.ts index 98139fbe..d518706d 100644 --- a/web/src/api/client.ts +++ b/web/src/api/client.ts @@ -227,10 +227,15 @@ export class ApiClient { return await this.request('/api/machines') } - async spawnSession(machineId: string, directory: string, agent?: 'claude' | 'codex' | 'gemini'): Promise { + async spawnSession( + machineId: string, + directory: string, + agent?: 'claude' | 'codex' | 'gemini', + yolo?: boolean + ): Promise { return await this.request(`/api/machines/${encodeURIComponent(machineId)}/spawn`, { method: 'POST', - body: JSON.stringify({ directory, agent }) + body: JSON.stringify({ directory, agent, yolo }) }) } } diff --git a/web/src/components/NewSession.tsx b/web/src/components/NewSession.tsx index 1777b7df..10a6f7cb 100644 --- a/web/src/components/NewSession.tsx +++ b/web/src/components/NewSession.tsx @@ -30,6 +30,7 @@ export function NewSession(props: { const [machineId, setMachineId] = useState(null) const [directory, setDirectory] = useState('') const [agent, setAgent] = useState('claude') + const [yoloMode, setYoloMode] = useState(false) const [error, setError] = useState(null) // Initialize with last used machine or first available @@ -83,6 +84,7 @@ export function NewSession(props: { machineId, directory: directory.trim(), agent, + yolo: yoloMode }) if (result.type === 'success') { @@ -194,6 +196,34 @@ export function NewSession(props: { + {/* YOLO Mode */} +
+ +
+
+ + Bypass approvals and sandbox + + + Uses dangerous agent flags when spawning. + +
+ +
+
+ {/* Error Message */} {(error ?? spawnError) ? (
diff --git a/web/src/hooks/mutations/useSpawnSession.ts b/web/src/hooks/mutations/useSpawnSession.ts index 9c3da862..db8fd96f 100644 --- a/web/src/hooks/mutations/useSpawnSession.ts +++ b/web/src/hooks/mutations/useSpawnSession.ts @@ -7,6 +7,7 @@ type SpawnInput = { machineId: string directory: string agent?: 'claude' | 'codex' | 'gemini' + yolo?: boolean } export function useSpawnSession(api: ApiClient | null): { @@ -21,7 +22,7 @@ export function useSpawnSession(api: ApiClient | null): { if (!api) { throw new Error('API unavailable') } - return await api.spawnSession(input.machineId, input.directory, input.agent) + return await api.spawnSession(input.machineId, input.directory, input.agent, input.yolo) }, onSuccess: () => { void queryClient.invalidateQueries({ queryKey: queryKeys.sessions })