feat(profiles): expose codex runtime across surfaces

- update CLI, API, route, and dashboard surfaces to recognize the Codex runtime target

- normalize persisted codex targets back to claude so runtime-only behavior stays truthful

- add regression coverage for help text, route parsing, and profile storage normalization

Refs #773
This commit is contained in:
Tam Nhu Tran committed 2026-03-29 13:14:15 -04:00
1 parent 8f60820f33
commit f9c1238483
19 files changed
+332 -26

No files matched your search

@@ -8,6 +8,7 @@ import * as fs from 'fs';
import * as path from 'path';
import type { Config, Settings } from '../../types';
import type { TargetType } from '../../targets/target-adapter';
import { getPersistedTargetChoices, isPersistedTargetType } from '../../targets/target-metadata';
import { getCcsDir, getConfigPath, loadConfigSafe } from '../../utils/config-manager';
import { ensureProfileHooksOrThrow } from '../../utils/websearch/profile-hook-injector';
import { isSensitiveKey } from '../../utils/sensitive-keys';
@@ -29,7 +30,7 @@ const SETTINGS_FILE_SUFFIX = '.settings.json';
const REDACTED_TOKEN_SENTINEL = '__CCS_REDACTED__';
function parseTargetValue(value: unknown): TargetType | null {
if (value === 'claude' || value === 'droid') {
if (isPersistedTargetType(value)) {
return value;
}
return null;
@@ -359,7 +360,7 @@ export function importApiProfileBundle(
if (input.profile.target !== undefined && bundleTarget === null) {
return {
success: false,
error: 'Invalid bundle profile target. Expected: claude or droid.',
error: `Invalid bundle profile target. Expected: ${getPersistedTargetChoices()}.`,
};
}
+2 -3
View File
@@ -10,14 +10,13 @@ import { loadConfigSafe } from '../../utils/config-manager';
import { loadOrCreateUnifiedConfig, isUnifiedMode } from '../../config/unified-config-loader';
import { expandPath } from '../../utils/helpers';
import type { TargetType } from '../../targets/target-adapter';
import { isPersistedTargetType } from '../../targets/target-metadata';
import type { Settings } from '../../types/config';
import type { ApiProfileInfo, CliproxyVariantInfo, ApiListResult } from './profile-types';
import { resolveCliproxyBridgeMetadata } from './cliproxy-profile-bridge';
const VALID_TARGETS: ReadonlySet<TargetType> = new Set<TargetType>(['claude', 'droid']);
function sanitizeTarget(target: unknown): TargetType {
if (typeof target === 'string' && VALID_TARGETS.has(target as TargetType)) {
if (isPersistedTargetType(target)) {
return target as TargetType;
}
return 'claude';
+1 -1
View File
@@ -292,7 +292,7 @@ export function getOfficialChannelsStateScopeMessage(): string {
}
export function getOfficialChannelsSupportMessage(): string {
return 'Works only for native Claude default/account sessions. It does not apply to API, OAuth, or Droid targets such as `ccs glm`, `ccs gemini`, `ccs codex`, or `ccs --target droid`.';
return 'Works only for native Claude default/account sessions. It does not apply to API, OAuth, or non-Claude targets such as `ccs glm`, `ccs gemini`, `ccs codex`, `ccs --target droid`, or `ccs --target codex`.';
}
export function getOfficialChannelsAccountStatusCaveat(): string {
@@ -417,6 +417,17 @@ export async function handleApiCreateCommand(args: string[]): Promise<void> {
` ${color(`ccs ${result.name} --target droid "your prompt"`, 'command')} ${dim('# target flag alternative')}`
);
}
if (cliproxyProvider === 'codex') {
console.log(
` ${color(`ccs ${result.name} --target codex "your prompt"`, 'command')} ${dim('# native Codex runtime')}`
);
console.log(
` ${color(`ccs-codex ${result.name} "your prompt"`, 'command')} ${dim('# explicit Codex alias')}`
);
console.log(
` ${color(`ccsx ${result.name} "your prompt"`, 'command')} ${dim('# short alias')}`
);
}
console.log('');
console.log(dim('Manage provider accounts, keys, and models in: ccs cliproxy'));
return;
+4
View File
@@ -107,6 +107,10 @@ export async function showApiCommandHelp(writeLine: HelpWriter = console.log): P
writeLine(
` ${color('ccs api create gemini-droid --cliproxy-provider gemini --target droid', 'command')}`
);
writeLine(` ${color('ccs api create codex-api --cliproxy-provider codex', 'command')}`);
writeLine(
` ${color('ccs codex-api --target codex', 'command')} ${dim('# runtime-only native Codex launch')}`
);
writeLine('');
writeLine(` ${dim('# Create with name')}`);
writeLine(` ${color('ccs api create myapi', 'command')}`);
+6 -3
View File
@@ -1,5 +1,6 @@
import type { ModelMapping } from '../../api/services';
import type { TargetType } from '../../targets/target-adapter';
import { getPersistedTargetChoices, isPersistedTargetType } from '../../targets/target-metadata';
import {
applyExtendedContextSuffix,
hasExtendedContextSuffix,
@@ -108,7 +109,7 @@ export function extractPositionalArgs(args: string[]): string[] {
function parseTargetValue(value: string): TargetType | null {
const normalized = value.trim().toLowerCase();
if (normalized === 'claude' || normalized === 'droid') {
if (isPersistedTargetType(normalized)) {
return normalized;
}
return null;
@@ -132,7 +133,7 @@ export function parseOptionalTargetFlag(
if (!target) {
return {
remainingArgs: extracted.remainingArgs,
errors: [`Invalid --target value "${extracted.value}". Use: claude or droid`],
errors: [`Invalid --target value "${extracted.value}". Use: ${getPersistedTargetChoices()}`],
};
}
@@ -251,7 +252,9 @@ export function parseApiCommandArgs(
(value) => {
const target = parseTargetValue(value);
if (!target) {
result.errors.push(`Invalid --target value "${value}". Use: claude or droid`);
result.errors.push(
`Invalid --target value "${value}". Use: ${getPersistedTargetChoices()}`
);
return;
}
result.target = target;
+8 -3
View File
@@ -13,6 +13,7 @@ import { CLIProxyProfileName, CLIPROXY_PROFILES } from '../../auth/profile-detec
import { supportsModelConfig, getProviderCatalog, ModelEntry } from '../../cliproxy/model-catalog';
import { CLIProxyProvider, CLIProxyBackend } from '../../cliproxy/types';
import type { TargetType } from '../../targets/target-adapter';
import { getPersistedTargetChoices, isPersistedTargetType } from '../../targets/target-metadata';
import { isUnifiedMode } from '../../config/unified-config-loader';
import { initUI, header, color, ok, fail, warn, info, infoBox, dim } from '../../utils/ui';
import { InteractivePrompt } from '../../utils/prompt';
@@ -42,7 +43,7 @@ interface CliproxyProfileArgs {
function parseTargetValue(rawValue: string): TargetType | null {
const normalized = rawValue.trim().toLowerCase();
if (normalized === 'claude' || normalized === 'droid') {
if (isPersistedTargetType(normalized)) {
return normalized;
}
return null;
@@ -73,7 +74,9 @@ export function parseProfileArgs(args: string[]): CliproxyProfileArgs {
i += 1;
const parsedTarget = parseTargetValue(rawValue);
if (!parsedTarget) {
result.errors.push(`Invalid --target value "${rawValue}". Use: claude or droid`);
result.errors.push(
`Invalid --target value "${rawValue}". Use: ${getPersistedTargetChoices()}`
);
} else {
result.target = parsedTarget;
}
@@ -82,7 +85,9 @@ export function parseProfileArgs(args: string[]): CliproxyProfileArgs {
const rawValue = arg.slice('--target='.length);
const parsedTarget = parseTargetValue(rawValue);
if (!parsedTarget) {
result.errors.push(`Invalid --target value "${rawValue}". Use: claude or droid`);
result.errors.push(
`Invalid --target value "${rawValue}". Use: ${getPersistedTargetChoices()}`
);
} else {
result.target = parsedTarget;
}
+12 -1
View File
@@ -391,7 +391,7 @@ Run ${color('ccs config', 'command')} for web dashboard`.trim();
'Flags',
[
['--config-dir <path>', 'Use custom CCS config directory'],
['--target <cli>', 'Target CLI: claude (default), droid'],
['--target <cli>', 'Target CLI: claude (default), droid, codex (runtime-only)'],
['-h, --help', 'Show this help message'],
['-v, --version', 'Show version and installation info'],
['-sc, --shell-completion', 'Install shell auto-completion'],
@@ -405,6 +405,8 @@ Run ${color('ccs config', 'command')} for web dashboard`.trim();
[
['ccs-droid <profile> [args]', 'Explicit Droid runtime alias'],
['ccsd <profile> [args]', 'Legacy shortcut for: ccs-droid <profile> [args]'],
['ccs-codex <profile> [args]', 'Explicit Codex runtime alias'],
['ccsx <profile> [args]', 'Short alias for: ccs-codex <profile> [args]'],
],
writeLine
);
@@ -416,6 +418,15 @@ Run ${color('ccs config', 'command')} for web dashboard`.trim();
['ccs glm --target droid', 'Run GLM profile on Droid CLI'],
['ccs-droid glm', 'Same as above (explicit alias)'],
['ccsd glm', 'Legacy shortcut for ccs-droid'],
['ccs --target codex', 'Open a native Codex session with your existing ~/.codex setup'],
['ccs-codex', 'Same as above (explicit Codex alias)'],
['ccsx', 'Short alias for ccs-codex'],
['ccs codex --target codex', 'Run built-in CLIProxy Codex on native Codex CLI'],
[
'ccs api create codex-api --cliproxy-provider codex',
'Create a routed API bridge that can also run on Codex',
],
['ccs codex-api --target codex', 'Run a Codex bridge profile on native Codex CLI'],
['ccs-droid codex', 'Run built-in CLIProxy Codex profile on Droid'],
['ccs-droid agy', 'Run built-in CLIProxy Antigravity profile on Droid'],
[
+10 -7
View File
@@ -23,6 +23,7 @@ import {
validateApiName,
} from '../../api/services';
import { normalizeDroidProvider } from '../../targets/droid-provider';
import { getPersistedTargetChoices } from '../../targets/target-metadata';
import { isCLIProxyProvider } from '../../cliproxy/provider-capabilities';
import { isAnthropicDirectProfile, updateSettingsFile, parseTarget } from './route-helpers';
@@ -100,7 +101,7 @@ router.post('/cliproxy-bridge', (req: Request, res: Response): void => {
const target = parseTarget(shape.payload.target);
if (shape.payload.target !== undefined && target === null) {
res.status(400).json({ error: 'Invalid target. Expected: claude or droid' });
res.status(400).json({ error: `Invalid target. Expected: ${getPersistedTargetChoices()}` });
return;
}
@@ -155,7 +156,7 @@ router.post('/', (req: Request, res: Response): void => {
const parsedTarget = parseTarget(target);
if (target !== undefined && parsedTarget === null) {
res.status(400).json({ error: 'Invalid target. Expected: claude or droid' });
res.status(400).json({ error: `Invalid target. Expected: ${getPersistedTargetChoices()}` });
return;
}
if (providerHint !== undefined && parsedProvider === null) {
@@ -265,7 +266,7 @@ router.post('/orphans/register', (req: Request, res: Response): void => {
const force = payload.force === true;
if (payload.target !== undefined && target === null) {
res.status(400).json({ error: 'Invalid target. Expected: claude or droid' });
res.status(400).json({ error: `Invalid target. Expected: ${getPersistedTargetChoices()}` });
return;
}
@@ -306,7 +307,7 @@ router.post('/:name/copy', (req: Request, res: Response): void => {
return;
}
if (shape.payload.target !== undefined && target === null) {
res.status(400).json({ error: 'Invalid target. Expected: claude or droid' });
res.status(400).json({ error: `Invalid target. Expected: ${getPersistedTargetChoices()}` });
return;
}
@@ -357,7 +358,7 @@ router.post('/import', (req: Request, res: Response): void => {
const target = parseTarget(shape.payload.target);
if (shape.payload.target !== undefined && target === null) {
res.status(400).json({ error: 'Invalid target. Expected: claude or droid' });
res.status(400).json({ error: `Invalid target. Expected: ${getPersistedTargetChoices()}` });
return;
}
@@ -368,7 +369,9 @@ router.post('/import', (req: Request, res: Response): void => {
}
const bundleTarget = (bundle as { profile?: { target?: unknown } }).profile?.target;
if (bundleTarget !== undefined && parseTarget(bundleTarget) === null) {
res.status(400).json({ error: 'Invalid bundle profile target. Expected: claude or droid' });
res.status(400).json({
error: `Invalid bundle profile target. Expected: ${getPersistedTargetChoices()}`,
});
return;
}
@@ -418,7 +421,7 @@ router.put('/:name', (req: Request, res: Response): void => {
const parsedTarget = parseTarget(target);
if (target !== undefined && parsedTarget === null) {
res.status(400).json({ error: 'Invalid target. Expected: claude or droid' });
res.status(400).json({ error: `Invalid target. Expected: ${getPersistedTargetChoices()}` });
return;
}
if (providerHint !== undefined && parsedProvider === null) {
+3 -2
View File
@@ -18,6 +18,7 @@ import {
import type { CLIProxyProvider } from '../../cliproxy/types';
import type { Config, Settings } from '../../types/config';
import type { TargetType } from '../../targets/target-adapter';
import { isPersistedTargetType } from '../../targets/target-metadata';
import { ValidationError } from '../../errors/error-types';
/** Model mapping for API profiles */
@@ -438,7 +439,7 @@ export function validateFilePath(filePath: string): {
}
/**
* Parse and validate a target param (claude/droid). Returns null if invalid/absent.
* Parse and validate a persisted target param. Returns null if invalid/absent.
* Shared by profile-routes and variant-routes.
*/
export function parseTarget(rawTarget: unknown): TargetType | null {
@@ -451,7 +452,7 @@ export function parseTarget(rawTarget: unknown): TargetType | null {
}
const normalized = rawTarget.trim().toLowerCase();
if (normalized === 'claude' || normalized === 'droid') {
if (isPersistedTargetType(normalized)) {
return normalized;
}
+3 -2
View File
@@ -7,6 +7,7 @@
import { Router, Request, Response } from 'express';
import { isReservedName, RESERVED_PROFILE_NAMES } from '../../config/reserved-names';
import type { CLIProxyProvider } from '../../cliproxy/types';
import { getPersistedTargetChoices } from '../../targets/target-metadata';
import { parseTarget } from './route-helpers';
import {
createVariant,
@@ -55,7 +56,7 @@ router.post('/', (req: Request, res: Response): void => {
const parsedTarget = parseTarget(req.body.target);
if (req.body.target !== undefined && parsedTarget === null) {
res.status(400).json({ error: 'Invalid target. Expected: claude or droid' });
res.status(400).json({ error: `Invalid target. Expected: ${getPersistedTargetChoices()}` });
return;
}
@@ -178,7 +179,7 @@ router.put('/:name', (req: Request, res: Response): void => {
const parsedTarget = parseTarget(req.body.target);
if (req.body.target !== undefined && parsedTarget === null) {
res.status(400).json({ error: 'Invalid target. Expected: claude or droid' });
res.status(400).json({ error: `Invalid target. Expected: ${getPersistedTargetChoices()}` });
return;
}