Files
hapi/cli/src/commands/codex.ts
T
07db10f86d fix(web): expose Codex Fast and Plan on Create Session (#1017)
* fix(web): expose Codex Fast and Plan on Create Session

Wire serviceTier and collaborationMode through spawn so Create can set
the same Codex options chat Settings already supports (#1015).

Co-authored-by: Cursor <cursoragent@cursor.com>

* fix(cli): forward collaborationMode through machine spawn RPC

Create Session Plan was accepted by the hub but dropped in apiMachine
before buildCliArgs; also preserve collaborationMode on resume spawn.

Co-authored-by: Cursor <cursoragent@cursor.com>

* fix(cli): correct stopSession mock type in spawn RPC test

Co-authored-by: Cursor <cursoragent@cursor.com>

* fix(web): keep Fast mode across Create draft restore while models load

Co-authored-by: Cursor <cursoragent@cursor.com>

* fix(web): preserve pending Fast selection

* fix: apply Fast and Plan to imported Codex sessions

* test: narrow imported Codex session id

* fix: forward explicit Standard service tier

* fix: integrate create-session controls with current main

* test: close Codex RPC suite

* fix: preserve existing session spawn field

* fix(web): integrate Codex controls with current New Session form

* fix(web): reconcile draft types and submit state

* fix(hub): integrate spawn arguments with current resume flow

* test(cli): isolate spawn RPC suite

---------

Co-authored-by: Cursor <cursoragent@cursor.com>
2026-07-28 12:13:29 +08:00

136 lines
5.8 KiB
TypeScript

import chalk from 'chalk'
import { authAndSetupMachineIfNeeded } from '@/ui/auth'
import { initializeToken } from '@/ui/tokenInit'
import { maybeAutoStartServer } from '@/utils/autoStartServer'
import type { CommandDefinition } from './types'
import { CODEX_PERMISSION_MODES } from '@hapi/protocol/modes'
import type { CodexPermissionMode } from '@hapi/protocol/types'
import { CodexCollaborationModeSchema } from '@hapi/protocol/schemas'
import type { ReasoningEffort } from '@/codex/appServerTypes'
import { assertCodexLocalSupported } from '@/codex/utils/codexVersion'
import { parseReasoningEffortValue } from '@/codex/utils/reasoningEffort'
// Mirror the web /service-tier endpoint's enum so the internal resume spawn
// path can never seed/persist an unsupported tier string.
function parseServiceTier(value: string): 'fast' | 'standard' {
const normalized = value.trim().toLowerCase()
if (normalized === 'fast' || normalized === 'standard') {
return normalized
}
throw new Error('Invalid --service-tier value')
}
function parseCollaborationMode(value: string): 'default' | 'plan' {
const parsed = CodexCollaborationModeSchema.safeParse(value.trim().toLowerCase())
if (!parsed.success) {
throw new Error('Invalid --collaboration-mode value')
}
return parsed.data
}
export const codexCommand: CommandDefinition = {
name: 'codex',
requiresRuntimeAssets: true,
run: async ({ commandArgs }) => {
try {
const { runCodex } = await import('@/codex/runCodex')
const options: {
startedBy?: 'runner' | 'terminal'
codexArgs?: string[]
permissionMode?: CodexPermissionMode
resumeSessionId?: string
existingSessionId?: string
model?: string
modelReasoningEffort?: ReasoningEffort
serviceTier?: string
collaborationMode?: 'default' | 'plan'
} = {}
const unknownArgs: string[] = []
let hasExplicitPermissionMode = false
for (let i = 0; i < commandArgs.length; i++) {
const arg = commandArgs[i]
if (i === 0 && arg === 'resume') {
const candidate = commandArgs[i + 1]
if (!candidate || candidate.startsWith('-')) {
unknownArgs.push(arg)
continue
}
options.resumeSessionId = candidate
i += 1
continue
}
if (arg === '--started-by') {
options.startedBy = commandArgs[++i] as 'runner' | 'terminal'
} else if (arg === '--existing-session-id') {
const sessionId = commandArgs[++i]
if (!sessionId) {
throw new Error('Missing --existing-session-id value')
}
options.existingSessionId = sessionId
} else if (arg === '--permission-mode') {
const mode = commandArgs[++i]
if (!mode || !(CODEX_PERMISSION_MODES as readonly string[]).includes(mode)) {
throw new Error(`Invalid --permission-mode value: ${mode ?? '(missing)'}`)
}
options.permissionMode = mode as CodexPermissionMode
hasExplicitPermissionMode = true
} else if ((arg === '--yolo' || arg === '--dangerously-bypass-approvals-and-sandbox') && !hasExplicitPermissionMode) {
options.permissionMode = 'yolo'
unknownArgs.push(arg)
} else if (arg === '--model') {
const model = commandArgs[++i]
if (!model) {
throw new Error('Missing --model value')
}
options.model = model
unknownArgs.push('--model', model)
} else if (arg === '--model-reasoning-effort') {
const effort = commandArgs[++i]
if (!effort) {
throw new Error('Missing --model-reasoning-effort value')
}
options.modelReasoningEffort = parseReasoningEffortValue(effort)
} else if (arg === '--service-tier') {
const tier = commandArgs[++i]
if (!tier) {
throw new Error('Missing --service-tier value')
}
options.serviceTier = parseServiceTier(tier)
} else if (arg === '--collaboration-mode') {
const mode = commandArgs[++i]
if (!mode) {
throw new Error('Missing --collaboration-mode value')
}
options.collaborationMode = parseCollaborationMode(mode)
} else {
unknownArgs.push(arg)
}
}
if (unknownArgs.length > 0) {
options.codexArgs = unknownArgs
}
if (options.startedBy !== 'runner') {
assertCodexLocalSupported()
}
await initializeToken()
if (options.startedBy === 'runner') {
await maybeAutoStartServer()
} else {
void maybeAutoStartServer({ waitForReady: false, quiet: true })
}
await authAndSetupMachineIfNeeded()
await runCodex(options)
} catch (error) {
console.error(chalk.red('Error:'), error instanceof Error ? error.message : 'Unknown error')
if (process.env.DEBUG) {
console.error(error)
}
process.exit(1)
}
}
}