Update agent SDKs and add gated Codex goals support (#114)

* add gated codex goals support

* sync README SDK versions

* bump claude agent sdk

* add codex goals settings toggle
This commit is contained in:
Eyejoker
2026-05-02 01:42:04 +09:00
committed by GitHub
parent bb1998be29
commit 7576bcd3ff
20 changed files with 616 additions and 30 deletions

View File

@@ -1,8 +1,8 @@
# EJClaw # EJClaw
![Version](https://img.shields.io/badge/version-0.2.3-blue) ![Version](https://img.shields.io/badge/version-0.2.3-blue)
![Claude Agent SDK](https://img.shields.io/badge/Claude_Agent_SDK-0.2.101-blueviolet) ![Claude Agent SDK](https://img.shields.io/badge/Claude_Agent_SDK-0.2.126-blueviolet)
![Codex SDK](https://img.shields.io/badge/Codex_SDK-0.120.0-green) ![Codex SDK](https://img.shields.io/badge/Codex_SDK-0.128.0-green)
![Bun](https://img.shields.io/badge/Bun-1.3+-f9f1e1?logo=bun&logoColor=black) ![Bun](https://img.shields.io/badge/Bun-1.3+-f9f1e1?logo=bun&logoColor=black)
![Discord](https://img.shields.io/badge/Discord-Tribunal-5865F2?logo=discord&logoColor=white) ![Discord](https://img.shields.io/badge/Discord-Tribunal-5865F2?logo=discord&logoColor=white)
@@ -115,6 +115,11 @@ Discord ──► SQLite (WAL) ──► GroupQueue ──┬──► Owner (ho
- Codex CLI - Codex CLI
- Discord 봇 토큰 3개(owner / reviewer / arbiter) - Discord 봇 토큰 3개(owner / reviewer / arbiter)
현재 runner 번들 기준 버전:
- Claude Agent SDK: `@anthropic-ai/claude-agent-sdk@0.2.126`
- Codex SDK/CLI: `@openai/codex@0.128.0`
### 설치 ### 설치
```bash ```bash

View File

@@ -34,6 +34,7 @@ describe('SettingsPanel', () => {
expect(html).toContain('모델'); expect(html).toContain('모델');
expect(html).toContain('MoA 참조 모델'); expect(html).toContain('MoA 참조 모델');
expect(html).toContain('패스트 모드'); expect(html).toContain('패스트 모드');
expect(html).toContain('Codex 실험 기능');
expect(html).toContain('불러오는 중'); expect(html).toContain('불러오는 중');
expect(html).toContain('Claude'); expect(html).toContain('Claude');
expect(html).toContain('계정'); expect(html).toContain('계정');

View File

@@ -3,17 +3,20 @@ import { useEffect, useState } from 'react';
import { import {
type ClaudeAccountSummary, type ClaudeAccountSummary,
type CodexAccountSummary, type CodexAccountSummary,
type CodexFeatureSnapshot,
type FastModeSnapshot, type FastModeSnapshot,
type ModelConfigSnapshot, type ModelConfigSnapshot,
type ModelRoleConfig, type ModelRoleConfig,
addClaudeAccount, addClaudeAccount,
deleteAccount, deleteAccount,
fetchAccounts, fetchAccounts,
fetchCodexFeatures,
fetchFastMode, fetchFastMode,
fetchModelConfig, fetchModelConfig,
refreshAllCodexAccounts as refreshAllCodexAccountsApi, refreshAllCodexAccounts as refreshAllCodexAccountsApi,
refreshCodexAccount as refreshCodexAccountApi, refreshCodexAccount as refreshCodexAccountApi,
setCurrentCodexAccount as setCurrentCodexAccountApi, setCurrentCodexAccount as setCurrentCodexAccountApi,
updateCodexFeatures,
updateFastMode, updateFastMode,
updateModels, updateModels,
} from './api'; } from './api';
@@ -82,6 +85,8 @@ export function SettingsPanel({
<FastModeSettings /> <FastModeSettings />
<CodexFeatureSettings onRestartStack={onRestartStack} />
<AccountSettings onRestartStack={onRestartStack} /> <AccountSettings onRestartStack={onRestartStack} />
</div> </div>
); );
@@ -302,6 +307,104 @@ function FastModeSettings() {
); );
} }
function CodexFeatureSettings({
onRestartStack,
}: {
onRestartStack: () => void;
}) {
const [state, setState] = useState<CodexFeatureSnapshot | null>(null);
const [busy, setBusy] = useState(false);
const [error, setError] = useState<string | null>(null);
const [savedAt, setSavedAt] = useState<number | null>(null);
useEffect(() => {
let cancelled = false;
fetchCodexFeatures()
.then((snapshot) => {
if (cancelled) return;
setState(snapshot);
setError(null);
})
.catch((err) => {
if (cancelled) return;
setError(err instanceof Error ? err.message : String(err));
});
return () => {
cancelled = true;
};
}, []);
async function toggleGoals() {
if (!state) return;
const previous = state;
const optimistic = { ...state, goals: !state.goals };
setState(optimistic);
setBusy(true);
setError(null);
try {
const fresh = await updateCodexFeatures({ goals: optimistic.goals });
setState(fresh);
setSavedAt(Date.now());
} catch (err) {
setState(previous);
setError(err instanceof Error ? err.message : String(err));
} finally {
setBusy(false);
}
}
return (
<section className="settings-section">
<h3>Codex </h3>
{error ? <p className="settings-error">{error}</p> : null}
{!state ? (
<p className="settings-hint"> </p>
) : (
<>
<label className="settings-toggle-row">
<span className="settings-toggle-label">
<span className="settings-toggle-title">/goal</span>
<small className="settings-hint">
CODEX_GOALS=true Codex 0.128 under-development goals
. OFF이며, .
</small>
</span>
<input
checked={state.goals}
disabled={busy}
onChange={() => void toggleGoals()}
type="checkbox"
/>
</label>
<div className="settings-actions">
{savedAt ? (
<small className="settings-hint">
. .
</small>
) : null}
<button
className="settings-restart"
disabled={busy}
onClick={() => {
if (
window.confirm(
'스택을 재시작하면 진행 중인 모든 에이전트 작업이 중단됩니다. 진행할까요?',
)
) {
onRestartStack();
}
}}
type="button"
>
</button>
</div>
</>
)}
</section>
);
}
function formatExpiry( function formatExpiry(
iso: string | null, iso: string | null,
): { label: string; cls: string } | null { ): { label: string; cls: string } | null {

View File

@@ -388,6 +388,10 @@ export interface FastModeSnapshot {
claude: boolean; claude: boolean;
} }
export interface CodexFeatureSnapshot {
goals: boolean;
}
export interface MoaReferenceStatus { export interface MoaReferenceStatus {
model: string; model: string;
checkedAt: string; checkedAt: string;
@@ -538,6 +542,34 @@ export async function updateFastMode(
return (await response.json()) as FastModeSnapshot; return (await response.json()) as FastModeSnapshot;
} }
export async function fetchCodexFeatures(): Promise<CodexFeatureSnapshot> {
return fetchJson('/api/settings/codex-features');
}
export async function updateCodexFeatures(
input: Partial<CodexFeatureSnapshot>,
): Promise<CodexFeatureSnapshot> {
const response = await fetch('/api/settings/codex-features', {
method: 'PUT',
headers: {
accept: 'application/json',
'content-type': 'application/json',
},
body: JSON.stringify(input),
});
if (!response.ok) {
let msg = `update codex features failed: ${response.status}`;
try {
const payload = (await response.json()) as { error?: string };
if (payload.error) msg = payload.error;
} catch {
/* ignore */
}
throw new Error(msg);
}
return (await response.json()) as CodexFeatureSnapshot;
}
export async function fetchMoaSettings(): Promise<MoaSettingsSnapshot> { export async function fetchMoaSettings(): Promise<MoaSettingsSnapshot> {
return fetchJson('/api/settings/moa'); return fetchJson('/api/settings/moa');
} }

View File

@@ -5,7 +5,7 @@
"": { "": {
"name": "ejclaw-agent-runner", "name": "ejclaw-agent-runner",
"dependencies": { "dependencies": {
"@anthropic-ai/claude-agent-sdk": "0.2.114", "@anthropic-ai/claude-agent-sdk": "0.2.126",
"@modelcontextprotocol/sdk": "^1.12.1", "@modelcontextprotocol/sdk": "^1.12.1",
"cron-parser": "^5.0.0", "cron-parser": "^5.0.0",
"ejclaw-runners-shared": "file:../shared", "ejclaw-runners-shared": "file:../shared",
@@ -18,23 +18,23 @@
}, },
}, },
"packages": { "packages": {
"@anthropic-ai/claude-agent-sdk": ["@anthropic-ai/claude-agent-sdk@0.2.114", "", { "dependencies": { "@anthropic-ai/sdk": "^0.81.0", "@modelcontextprotocol/sdk": "^1.29.0" }, "optionalDependencies": { "@anthropic-ai/claude-agent-sdk-darwin-arm64": "0.2.114", "@anthropic-ai/claude-agent-sdk-darwin-x64": "0.2.114", "@anthropic-ai/claude-agent-sdk-linux-arm64": "0.2.114", "@anthropic-ai/claude-agent-sdk-linux-arm64-musl": "0.2.114", "@anthropic-ai/claude-agent-sdk-linux-x64": "0.2.114", "@anthropic-ai/claude-agent-sdk-linux-x64-musl": "0.2.114", "@anthropic-ai/claude-agent-sdk-win32-arm64": "0.2.114", "@anthropic-ai/claude-agent-sdk-win32-x64": "0.2.114" }, "peerDependencies": { "zod": "^4.0.0" } }, "sha512-plJ+j17jew9tDMHir/90hXrwoB8cZ9GrIyG19zIJcFyQ8pVhRXjZRJCtF2ElfPoiwkxMmNu1Klqyui4xP4shPg=="], "@anthropic-ai/claude-agent-sdk": ["@anthropic-ai/claude-agent-sdk@0.2.126", "", { "dependencies": { "@anthropic-ai/sdk": "^0.81.0", "@modelcontextprotocol/sdk": "^1.29.0" }, "optionalDependencies": { "@anthropic-ai/claude-agent-sdk-darwin-arm64": "0.2.126", "@anthropic-ai/claude-agent-sdk-darwin-x64": "0.2.126", "@anthropic-ai/claude-agent-sdk-linux-arm64": "0.2.126", "@anthropic-ai/claude-agent-sdk-linux-arm64-musl": "0.2.126", "@anthropic-ai/claude-agent-sdk-linux-x64": "0.2.126", "@anthropic-ai/claude-agent-sdk-linux-x64-musl": "0.2.126", "@anthropic-ai/claude-agent-sdk-win32-arm64": "0.2.126", "@anthropic-ai/claude-agent-sdk-win32-x64": "0.2.126" }, "peerDependencies": { "zod": "^4.0.0" } }, "sha512-4ZrVu0XUEwNG6wxvsLgppRAmSfAf3oeEMEUPhgazb0AXUUe/7W8MxwZKJWOffqSLWaNYzOt3ZCIL7NJY6toqWw=="],
"@anthropic-ai/claude-agent-sdk-darwin-arm64": ["@anthropic-ai/claude-agent-sdk-darwin-arm64@0.2.114", "", { "os": "darwin", "cpu": "arm64" }, "sha512-0/6LWrNilWpmiX6Xrj5plsBmCrCdKGERgAlKUZQEJZplnfuweFAJu7WXZB4KBaUpGlPO91zB/yqDh6kp5aZFbA=="], "@anthropic-ai/claude-agent-sdk-darwin-arm64": ["@anthropic-ai/claude-agent-sdk-darwin-arm64@0.2.126", "", { "os": "darwin", "cpu": "arm64" }, "sha512-JFlJBbeAlx7Ic5s4lGUN9SppobryXk/lIqPCvhp6KrJTQIerh3MIBzxsVIJ0MaDut7jVni/oYgsvDni7NIyqHA=="],
"@anthropic-ai/claude-agent-sdk-darwin-x64": ["@anthropic-ai/claude-agent-sdk-darwin-x64@0.2.114", "", { "os": "darwin", "cpu": "x64" }, "sha512-sOHxq1rEO/KZg2iEZILTPn62lMRRMPqtxKx41uGLi3xjVDrAej6Ury9dDZjYBKkK9n4kBylXV0Oom2CZ14dDYw=="], "@anthropic-ai/claude-agent-sdk-darwin-x64": ["@anthropic-ai/claude-agent-sdk-darwin-x64@0.2.126", "", { "os": "darwin", "cpu": "x64" }, "sha512-J8BpMj16NK9FUaG3HnHSivyp4Xww9DKWHiC8QSHT9oiT8pH5IG7nl0jxyjIq/lY79evlTY+ubgDVWlMUhUAN/g=="],
"@anthropic-ai/claude-agent-sdk-linux-arm64": ["@anthropic-ai/claude-agent-sdk-linux-arm64@0.2.114", "", { "os": "linux", "cpu": "arm64" }, "sha512-j/SfEoN6+fyEsp8EuPe+xKcGfsZtaBmdUUH+YSRk5H/lYgy38yNsDhdt+AJMQcdMKfHsiwZ3Y9Ajoe9G9wNwHQ=="], "@anthropic-ai/claude-agent-sdk-linux-arm64": ["@anthropic-ai/claude-agent-sdk-linux-arm64@0.2.126", "", { "os": "linux", "cpu": "arm64" }, "sha512-LM+mnfQsgI+1i5mYZwIPDDf14NGBu5wbhzm5U8P11dCa2p8sXmKoWpkbO16BFM2NxeW44I/RXCxE5qFsbz4zcg=="],
"@anthropic-ai/claude-agent-sdk-linux-arm64-musl": ["@anthropic-ai/claude-agent-sdk-linux-arm64-musl@0.2.114", "", { "os": "linux", "cpu": "arm64" }, "sha512-Mhd7bumTwWvkgjSJnYvCgyt8DfmLiUoK92mfvAKxHX7i5YSw+h5Kprqh2Cap+2SBbpwZvnwIoEYGCxhGwE5ddg=="], "@anthropic-ai/claude-agent-sdk-linux-arm64-musl": ["@anthropic-ai/claude-agent-sdk-linux-arm64-musl@0.2.126", "", { "os": "linux", "cpu": "arm64" }, "sha512-GO0BnIUw3LQ3XAy+nipAabkN0GwQGPhHB6ITI4XLoR99fLHB3TA6WfyvTf0fnpxd25A+c/+UsAoxz4zBQaHlhA=="],
"@anthropic-ai/claude-agent-sdk-linux-x64": ["@anthropic-ai/claude-agent-sdk-linux-x64@0.2.114", "", { "os": "linux", "cpu": "x64" }, "sha512-wbaExKDleLlm2zHEhb74GKMLVhtO0IUmFhdimQcdL6CdTkmDE8ZJi53tYWE9+jq+XWNRXoM2yEmKPzXoUmsJng=="], "@anthropic-ai/claude-agent-sdk-linux-x64": ["@anthropic-ai/claude-agent-sdk-linux-x64@0.2.126", "", { "os": "linux", "cpu": "x64" }, "sha512-yaOTDcYCdscxC0LKg9w8IwSa5g+993WggFZJBTZpqvflA2+WMQeTarDnKlsFTCw9XUZkL8XZeBALYJGx0HutuA=="],
"@anthropic-ai/claude-agent-sdk-linux-x64-musl": ["@anthropic-ai/claude-agent-sdk-linux-x64-musl@0.2.114", "", { "os": "linux", "cpu": "x64" }, "sha512-c1URsameGHAcghen+mY6jvr2oypiAPHXJIdP4huxR25zPdXWv2x+BCy+vcRVeajsq4VmFzAyQJwaM+BXkmXjAw=="], "@anthropic-ai/claude-agent-sdk-linux-x64-musl": ["@anthropic-ai/claude-agent-sdk-linux-x64-musl@0.2.126", "", { "os": "linux", "cpu": "x64" }, "sha512-ByJGO0+mu7EplxSFSCIHd7QWsXdrF3qgtzQ177o/j+oSppLoqR1ot5ktf8aw5oR3CC5lFHg4tqd6TnneQpEoIg=="],
"@anthropic-ai/claude-agent-sdk-win32-arm64": ["@anthropic-ai/claude-agent-sdk-win32-arm64@0.2.114", "", { "os": "win32", "cpu": "arm64" }, "sha512-qeWdUpQymcKCA92osPmffG4QogrOSvuffPvm6c2OlMDjCPYs8vKG7bSe1Vq5tP9tfBszKPVJWBDh+2ANkNissQ=="], "@anthropic-ai/claude-agent-sdk-win32-arm64": ["@anthropic-ai/claude-agent-sdk-win32-arm64@0.2.126", "", { "os": "win32", "cpu": "arm64" }, "sha512-gv3MOsOBkCx3LajOOIjD7AKsOtz/qNHsS2oshGt2GVoy7JA3XbCDeCetDjM6SorV4SE+7F/IH0UJdXe5ejI/Zg=="],
"@anthropic-ai/claude-agent-sdk-win32-x64": ["@anthropic-ai/claude-agent-sdk-win32-x64@0.2.114", "", { "os": "win32", "cpu": "x64" }, "sha512-nVr43WwsKvWA6rojw15qBS/f31srukdLxy1KwKzpftlpmkzQ9Lh8uhIafOmoIPzz67f8VJ8JqHE0caA5YrhX9A=="], "@anthropic-ai/claude-agent-sdk-win32-x64": ["@anthropic-ai/claude-agent-sdk-win32-x64@0.2.126", "", { "os": "win32", "cpu": "x64" }, "sha512-oRV75HwyoOd1/t5+kipAM2g62CaElpKGvSBx3Ys4lCwCiFUyOnmet/O+hRXENsY6ShDeQZEcJL2UWljr2d5NQw=="],
"@anthropic-ai/sdk": ["@anthropic-ai/sdk@0.81.0", "", { "dependencies": { "json-schema-to-ts": "^3.1.1" }, "peerDependencies": { "zod": "^3.25.0 || ^4.0.0" }, "optionalPeers": ["zod"], "bin": { "anthropic-ai-sdk": "bin/cli" } }, "sha512-D4K5PvEV6wPiRtVlVsJHIUhHAmOZ6IT/I9rKlTf84gR7GyyAurPJK7z9BOf/AZqC5d1DhYQGJNKRmV+q8dGhgw=="], "@anthropic-ai/sdk": ["@anthropic-ai/sdk@0.81.0", "", { "dependencies": { "json-schema-to-ts": "^3.1.1" }, "peerDependencies": { "zod": "^3.25.0 || ^4.0.0" }, "optionalPeers": ["zod"], "bin": { "anthropic-ai-sdk": "bin/cli" } }, "sha512-D4K5PvEV6wPiRtVlVsJHIUhHAmOZ6IT/I9rKlTf84gR7GyyAurPJK7z9BOf/AZqC5d1DhYQGJNKRmV+q8dGhgw=="],

View File

@@ -10,7 +10,7 @@
"start": "bun dist/index.js" "start": "bun dist/index.js"
}, },
"dependencies": { "dependencies": {
"@anthropic-ai/claude-agent-sdk": "0.2.114", "@anthropic-ai/claude-agent-sdk": "0.2.126",
"@modelcontextprotocol/sdk": "^1.12.1", "@modelcontextprotocol/sdk": "^1.12.1",
"cron-parser": "^5.0.0", "cron-parser": "^5.0.0",
"ejclaw-runners-shared": "file:../shared", "ejclaw-runners-shared": "file:../shared",

View File

@@ -5,7 +5,7 @@
"": { "": {
"name": "ejclaw-codex-runner", "name": "ejclaw-codex-runner",
"dependencies": { "dependencies": {
"@openai/codex": "^0.124.0", "@openai/codex": "0.128.0",
"ejclaw-runners-shared": "file:../shared", "ejclaw-runners-shared": "file:../shared",
}, },
"devDependencies": { "devDependencies": {
@@ -15,19 +15,19 @@
}, },
}, },
"packages": { "packages": {
"@openai/codex": ["@openai/codex@0.124.0", "", { "optionalDependencies": { "@openai/codex-darwin-arm64": "npm:@openai/codex@0.124.0-darwin-arm64", "@openai/codex-darwin-x64": "npm:@openai/codex@0.124.0-darwin-x64", "@openai/codex-linux-arm64": "npm:@openai/codex@0.124.0-linux-arm64", "@openai/codex-linux-x64": "npm:@openai/codex@0.124.0-linux-x64", "@openai/codex-win32-arm64": "npm:@openai/codex@0.124.0-win32-arm64", "@openai/codex-win32-x64": "npm:@openai/codex@0.124.0-win32-x64" }, "bin": { "codex": "bin/codex.js" } }, "sha512-1EVAuPyAQZ8zIVMw3bPJ6a4R8ifLAZ7LGsOyknj5c2he9AFXVRCmWx12WrdZJ25wcBvOEKt1n1Zx+QAj0EVGbQ=="], "@openai/codex": ["@openai/codex@0.128.0", "", { "optionalDependencies": { "@openai/codex-darwin-arm64": "npm:@openai/codex@0.128.0-darwin-arm64", "@openai/codex-darwin-x64": "npm:@openai/codex@0.128.0-darwin-x64", "@openai/codex-linux-arm64": "npm:@openai/codex@0.128.0-linux-arm64", "@openai/codex-linux-x64": "npm:@openai/codex@0.128.0-linux-x64", "@openai/codex-win32-arm64": "npm:@openai/codex@0.128.0-win32-arm64", "@openai/codex-win32-x64": "npm:@openai/codex@0.128.0-win32-x64" }, "bin": { "codex": "bin/codex.js" } }, "sha512-+xp6ODmFfBNnexIWRHApEaPXot2j6gyM8A5we/5IS/uY4eYHj4arETct4hQ5M4eO+MK7JY3ZU4xhuobhlysr0A=="],
"@openai/codex-darwin-arm64": ["@openai/codex@0.124.0-darwin-arm64", "", { "os": "darwin", "cpu": "arm64" }, "sha512-lnuqeAdl+RjPWsqVZ8rrPskRIOZmzlyr8e5q/wFVEnMPsm9dWgjRW1PKa84UmDSQVOuz9GObF28DEHFyC4A8NA=="], "@openai/codex-darwin-arm64": ["@openai/codex@0.128.0-darwin-arm64", "", { "os": "darwin", "cpu": "arm64" }, "sha512-w+6zohfHx/kHBdles/CyFKaY57u9I3nK8QI9+NrdwMliKA0b7xn13yblRNkMpe09j6vL1oAWoxYsMOQ/vjBGug=="],
"@openai/codex-darwin-x64": ["@openai/codex@0.124.0-darwin-x64", "", { "os": "darwin", "cpu": "x64" }, "sha512-Br+VlL83IOu96Urw+mkZ1PQXLsQXGAYEH6kXNt4ULuVt7qAdQY2FKYwIgLLVLl0vpMk8qWxdfdVMqhiu2uCHlg=="], "@openai/codex-darwin-x64": ["@openai/codex@0.128.0-darwin-x64", "", { "os": "darwin", "cpu": "x64" }, "sha512-SDbn6fO22Puy8xmMIbZi4f2znMrUEPwABApke4mo+4ihaauwuVjeqzXvW5SPJz5ty/bG11/mSupQgReT7T8BBw=="],
"@openai/codex-linux-arm64": ["@openai/codex@0.124.0-linux-arm64", "", { "os": "linux", "cpu": "arm64" }, "sha512-QXafFVQJxPfU1LSCI5afuHsF5evKAcbQpFuKou0Kl241/HG/zYHdwoWj546zH844gJgegIPAxPf36+RSkUsbbA=="], "@openai/codex-linux-arm64": ["@openai/codex@0.128.0-linux-arm64", "", { "os": "linux", "cpu": "arm64" }, "sha512-+SvH73H60qvCXFuQGP/EsmR//s1hHMBR22PvJkXvM/hdnTIGucx+JqRUjAWdmmQ1IU6j3kgwVvdLW/6ICB+M6w=="],
"@openai/codex-linux-x64": ["@openai/codex@0.124.0-linux-x64", "", { "os": "linux", "cpu": "x64" }, "sha512-cJ4QfQIrz2gkXVWephiGo9JDyD3qLKhvkTTLk7gI8rKd7MZpVXn9/1e7pZCQnJVA7Dk6ocA5G+sxnR78SoIejw=="], "@openai/codex-linux-x64": ["@openai/codex@0.128.0-linux-x64", "", { "os": "linux", "cpu": "x64" }, "sha512-2lnSPA05CRRuKAzFW8BCmmNCSieDcToLwfC2ALLbBYilGLgzhRibjlDglK9F1BkEzfohSSWJu4PBbRu/aG60lQ=="],
"@openai/codex-win32-arm64": ["@openai/codex@0.124.0-win32-arm64", "", { "os": "win32", "cpu": "arm64" }, "sha512-oDSI/CQh0kagBwWVPG9EiUBfHiY27ju3LPrjTVZg4VMpVJVNA31JTK8rHMZ6G/2gfNmOl6DMBA6+0ICxSkkshg=="], "@openai/codex-win32-arm64": ["@openai/codex@0.128.0-win32-arm64", "", { "os": "win32", "cpu": "arm64" }, "sha512-ECJvsqmYFdA9pn42xxK3Odp/G16AjmBW0BglX8L0PwPjqbstbmlew9bfHf7xvL+SNfNl4NmyotW0+RNo1phgaA=="],
"@openai/codex-win32-x64": ["@openai/codex@0.124.0-win32-x64", "", { "os": "win32", "cpu": "x64" }, "sha512-t+lAwmCWwIWQKk6dKaYetRhQsmA+uDmQy1NLka1xAAv3PlNOH8iNz9Lsisr3qYkBR8Tf7tbQlt+pl9tw/A5mfA=="], "@openai/codex-win32-x64": ["@openai/codex@0.128.0-win32-x64", "", { "os": "win32", "cpu": "x64" }, "sha512-k3jmUAFrzkUtvjGTXvSKjQqJLLlzjxp/VoHJDYedgmXUn6j70HxK38IwapzmnYfiBiTuzETvGwjXHzZgzKjhoQ=="],
"@types/node": ["@types/node@22.19.15", "", { "dependencies": { "undici-types": "~6.21.0" } }, "sha512-F0R/h2+dsy5wJAUe3tAU6oqa2qbWY5TpNfL/RGmo1y38hiyO1w3x2jPtt76wmuaJI4DQnOBu21cNXQ2STIUUWg=="], "@types/node": ["@types/node@22.19.15", "", { "dependencies": { "undici-types": "~6.21.0" } }, "sha512-F0R/h2+dsy5wJAUe3tAU6oqa2qbWY5TpNfL/RGmo1y38hiyO1w3x2jPtt76wmuaJI4DQnOBu21cNXQ2STIUUWg=="],

View File

@@ -10,8 +10,8 @@
"start": "bun dist/index.js" "start": "bun dist/index.js"
}, },
"dependencies": { "dependencies": {
"ejclaw-runners-shared": "file:../shared", "@openai/codex": "0.128.0",
"@openai/codex": "^0.124.0" "ejclaw-runners-shared": "file:../shared"
}, },
"devDependencies": { "devDependencies": {
"@types/node": "^22.10.7", "@types/node": "^22.10.7",

View File

@@ -42,6 +42,53 @@ export interface CodexAppServerTurnResult {
result: string | null; result: string | null;
} }
export interface CodexThreadGoal {
threadId: string;
objective: string;
status: string;
tokenBudget: number | null;
tokensUsed: number;
timeUsedSeconds: number;
createdAt: number;
updatedAt: number;
}
export function buildCodexAppServerArgs(args: {
codexBin: string;
enableGoals?: boolean;
}): string[] {
return args.enableGoals === true
? [args.codexBin, '--enable', 'goals', 'app-server']
: [args.codexBin, 'app-server'];
}
function isCodexThreadGoal(value: unknown): value is CodexThreadGoal {
if (!value || typeof value !== 'object') return false;
const goal = value as Partial<CodexThreadGoal>;
return (
typeof goal.threadId === 'string' &&
typeof goal.objective === 'string' &&
typeof goal.status === 'string'
);
}
function extractOptionalGoalResult(result: unknown): CodexThreadGoal | null {
const goal = (result as { goal?: unknown } | null)?.goal ?? null;
if (goal === null) return null;
if (!isCodexThreadGoal(goal)) {
throw new Error('Codex app-server returned an invalid goal payload.');
}
return goal;
}
function extractGoalResult(result: unknown, method: string): CodexThreadGoal {
const goal = extractOptionalGoalResult(result);
if (!goal) {
throw new Error(`Codex app-server ${method} did not return a goal.`);
}
return goal;
}
interface JsonRpcResponse { interface JsonRpcResponse {
id: number; id: number;
result?: unknown; result?: unknown;
@@ -79,12 +126,14 @@ export interface CodexAppServerClientOptions {
cwd: string; cwd: string;
env?: NodeJS.ProcessEnv; env?: NodeJS.ProcessEnv;
log: (message: string) => void; log: (message: string) => void;
enableGoals?: boolean;
} }
export class CodexAppServerClient { export class CodexAppServerClient {
private readonly cwd: string; private readonly cwd: string;
private readonly env: NodeJS.ProcessEnv; private readonly env: NodeJS.ProcessEnv;
private readonly log: (message: string) => void; private readonly log: (message: string) => void;
private readonly enableGoals: boolean;
private readonly pending = new Map<number, PendingRequest>(); private readonly pending = new Map<number, PendingRequest>();
private readonly require = createRequire(import.meta.url); private readonly require = createRequire(import.meta.url);
private nextId = 1; private nextId = 1;
@@ -96,6 +145,7 @@ export class CodexAppServerClient {
this.cwd = options.cwd; this.cwd = options.cwd;
this.env = options.env || process.env; this.env = options.env || process.env;
this.log = options.log; this.log = options.log;
this.enableGoals = options.enableGoals === true;
} }
async start(): Promise<void> { async start(): Promise<void> {
@@ -108,11 +158,18 @@ export class CodexAppServerClient {
'codex.js', 'codex.js',
); );
this.proc = spawn(process.execPath, [codexBin, 'app-server'], { this.proc = spawn(
process.execPath,
buildCodexAppServerArgs({
codexBin,
enableGoals: this.enableGoals,
}),
{
cwd: this.cwd, cwd: this.cwd,
env: this.env, env: this.env,
stdio: ['pipe', 'pipe', 'pipe'], stdio: ['pipe', 'pipe', 'pipe'],
}); },
);
this.proc.stdout.setEncoding('utf8'); this.proc.stdout.setEncoding('utf8');
this.proc.stdout.on('data', (chunk: string) => { this.proc.stdout.on('data', (chunk: string) => {
@@ -312,6 +369,27 @@ export class CodexAppServerClient {
return turnPromise; return turnPromise;
} }
async threadGoalSet(
threadId: string,
objective: string,
): Promise<CodexThreadGoal> {
const result = await this.request('thread/goal/set', {
threadId,
objective,
});
return extractGoalResult(result, 'thread/goal/set');
}
async threadGoalGet(threadId: string): Promise<CodexThreadGoal | null> {
const result = await this.request('thread/goal/get', { threadId });
return extractOptionalGoalResult(result);
}
async threadGoalClear(threadId: string): Promise<boolean> {
const result = await this.request('thread/goal/clear', { threadId });
return (result as { cleared?: boolean } | null)?.cleared === true;
}
private handleStdoutLine(line: string): void { private handleStdoutLine(line: string): void {
let message: JsonRpcResponse | JsonRpcNotification | JsonRpcServerRequest; let message: JsonRpcResponse | JsonRpcNotification | JsonRpcServerRequest;
try { try {

View File

@@ -52,6 +52,7 @@ interface RunnerInput {
isScheduledTask?: boolean; isScheduledTask?: boolean;
assistantName?: string; assistantName?: string;
agentType?: string; agentType?: string;
codexGoals?: boolean;
roomRoleContext?: RoomRoleContext; roomRoleContext?: RoomRoleContext;
} }
@@ -95,6 +96,10 @@ function log(message: string): void {
console.error(`[codex-runner] ${message}`); console.error(`[codex-runner] ${message}`);
} }
function isCodexGoalsEnabled(runnerInput: RunnerInput): boolean {
return runnerInput.codexGoals === true || process.env.CODEX_GOALS === 'true';
}
async function readStdin(): Promise<string> { async function readStdin(): Promise<string> {
return new Promise((resolve, reject) => { return new Promise((resolve, reject) => {
let data = ''; let data = '';
@@ -343,6 +348,7 @@ async function runAppServerSession(
cwd: EFFECTIVE_CWD, cwd: EFFECTIVE_CWD,
env: clientEnv, env: clientEnv,
log, log,
enableGoals: isCodexGoalsEnabled(runnerInput),
}); });
await client.start(); await client.start();

View File

@@ -0,0 +1,87 @@
import { describe, expect, it, vi } from 'vitest';
import {
CodexAppServerClient,
buildCodexAppServerArgs,
} from '../src/app-server-client.js';
describe('codex app-server client goals', () => {
it('does not enable goals by default when spawning app-server', () => {
expect(
buildCodexAppServerArgs({
codexBin: '/opt/codex/bin/codex.js',
}),
).toEqual(['/opt/codex/bin/codex.js', 'app-server']);
});
it('adds the under-development goals feature only when explicitly enabled', () => {
expect(
buildCodexAppServerArgs({
codexBin: '/opt/codex/bin/codex.js',
enableGoals: true,
}),
).toEqual(['/opt/codex/bin/codex.js', '--enable', 'goals', 'app-server']);
});
it('wraps thread goal JSON-RPC methods with the upstream objective field', async () => {
const client = new CodexAppServerClient({
cwd: '/repo',
log: () => undefined,
});
const request = vi
.fn()
.mockResolvedValueOnce({
goal: {
threadId: 'thread-1',
objective: 'ship the release',
status: 'active',
tokenBudget: null,
tokensUsed: 0,
timeUsedSeconds: 0,
createdAt: 1,
updatedAt: 1,
},
})
.mockResolvedValueOnce({
goal: {
threadId: 'thread-1',
objective: 'ship the release',
status: 'active',
tokenBudget: null,
tokensUsed: 0,
timeUsedSeconds: 0,
createdAt: 1,
updatedAt: 1,
},
})
.mockResolvedValueOnce({ cleared: true });
(
client as unknown as {
request: (method: string, params?: unknown) => Promise<unknown>;
}
).request = request;
await expect(
client.threadGoalSet('thread-1', 'ship the release'),
).resolves.toMatchObject({
threadId: 'thread-1',
objective: 'ship the release',
status: 'active',
});
await expect(client.threadGoalGet('thread-1')).resolves.toMatchObject({
objective: 'ship the release',
});
await expect(client.threadGoalClear('thread-1')).resolves.toBe(true);
expect(request).toHaveBeenNthCalledWith(1, 'thread/goal/set', {
threadId: 'thread-1',
objective: 'ship the release',
});
expect(request).toHaveBeenNthCalledWith(2, 'thread/goal/get', {
threadId: 'thread-1',
});
expect(request).toHaveBeenNthCalledWith(3, 'thread/goal/clear', {
threadId: 'thread-1',
});
});
});

View File

@@ -370,6 +370,79 @@ describe('prepareGroupEnvironment codex auth handling', () => {
}); });
}); });
describe('prepareGroupEnvironment Codex goals handling', () => {
let tempRoot: string;
let previousCwd: string;
let previousCodexGoals: string | undefined;
beforeEach(() => {
tempRoot = fs.mkdtempSync(path.join('/tmp', 'ejclaw-agent-env-goals-'));
previousCwd = process.cwd();
previousCodexGoals = process.env.CODEX_GOALS;
process.chdir(tempRoot);
process.env.EJ_TEST_ROOT = tempRoot;
process.env.EJ_TEST_HOME = path.join(tempRoot, 'home');
delete process.env.CODEX_GOALS;
fs.mkdirSync(path.join(process.env.EJ_TEST_HOME, '.codex'), {
recursive: true,
});
mockReadEnvFile.mockReset();
mockGetActiveCodexAuthPath.mockReset();
vi.mocked(config.isReviewService).mockReturnValue(false);
vi.mocked(serviceRouting.hasReviewerLease).mockReturnValue(false);
vi.mocked(serviceRouting.getEffectiveChannelLease).mockReturnValue({
chat_jid: 'dc:test',
owner_service_id: 'claude',
reviewer_service_id: 'codex-main',
arbiter_service_id: null,
owner_failover_active: false,
activated_at: null,
reason: null,
explicit: false,
});
});
afterEach(() => {
process.chdir(previousCwd);
delete process.env.EJ_TEST_ROOT;
delete process.env.EJ_TEST_HOME;
if (previousCodexGoals) process.env.CODEX_GOALS = previousCodexGoals;
else delete process.env.CODEX_GOALS;
fs.rmSync(tempRoot, { recursive: true, force: true });
});
it('keeps Codex goals disabled by default and enables them only via opt-in config', () => {
mockReadEnvFile.mockReturnValue({});
const defaultPrepared = prepareGroupEnvironment(group, false, 'dc:test');
expect(defaultPrepared.env.CODEX_GOALS).toBeUndefined();
const enabledPrepared = prepareGroupEnvironment(
{
...group,
agentConfig: {
codexGoals: true,
},
},
false,
'dc:test',
);
expect(enabledPrepared.env.CODEX_GOALS).toBe('true');
});
it('allows CODEX_GOALS env opt-in for Codex runner sessions', () => {
mockReadEnvFile.mockReturnValue({
CODEX_GOALS: 'true',
});
const prepared = prepareGroupEnvironment(group, false, 'dc:test');
expect(prepared.env.CODEX_GOALS).toBe('true');
});
});
describe('prepareReadonlySessionEnvironment codex compatibility', () => { describe('prepareReadonlySessionEnvironment codex compatibility', () => {
let tempRoot: string; let tempRoot: string;
let previousCwd: string; let previousCwd: string;

View File

@@ -319,6 +319,15 @@ function prepareCodexSessionEnvironment(args: {
process.env.CODEX_EFFORT; process.env.CODEX_EFFORT;
if (codexEffort) args.env.CODEX_EFFORT = codexEffort; if (codexEffort) args.env.CODEX_EFFORT = codexEffort;
const codexGoals =
args.group.agentConfig?.codexGoals ??
(args.envVars.CODEX_GOALS === 'true' || process.env.CODEX_GOALS === 'true');
if (codexGoals) {
args.env.CODEX_GOALS = 'true';
} else {
delete args.env.CODEX_GOALS;
}
const sessionCodexDir = path.join(args.sessionRootDir, '.codex'); const sessionCodexDir = path.join(args.sessionRootDir, '.codex');
syncHostCodexSessionFiles(sessionCodexDir); syncHostCodexSessionFiles(sessionCodexDir);
@@ -527,6 +536,7 @@ export function prepareGroupEnvironment(
'CLAUDE_EFFORT', 'CLAUDE_EFFORT',
'CODEX_MODEL', 'CODEX_MODEL',
'CODEX_EFFORT', 'CODEX_EFFORT',
'CODEX_GOALS',
]); ]);
const env = buildBaseRunnerEnv({ const env = buildBaseRunnerEnv({

View File

@@ -680,3 +680,59 @@ OUROBOROS_LLM_BACKEND = "codex"
); );
}); });
}); });
describe('agent-runner Codex goals', () => {
beforeEach(() => {
vi.useRealTimers();
vi.clearAllMocks();
fakeProc = createFakeProcess();
vi.mocked(fs.existsSync).mockImplementation((p: fs.PathLike) => {
const str = String(p);
return (
str.includes('dist/index.js') ||
str.includes('dist/ipc-mcp-stdio.js') ||
str.endsWith('/.codex/config.toml')
);
});
});
afterEach(() => {
vi.useRealTimers();
});
it('passes Codex goal opt-in config through env and runner stdin payload', async () => {
let stdinPayload = '';
fakeProc.stdin.on('data', (chunk) => {
stdinPayload += chunk.toString();
});
const codexGroup: RegisteredGroup = {
...testGroup,
agentType: 'codex',
agentConfig: {
codexGoals: true,
},
};
const resultPromise = runAgentProcess(
codexGroup,
testInput,
() => {},
async () => {},
);
fakeProc.emit('close', 0);
const result = await resultPromise;
expect(result.status).toBe('success');
expect(JSON.parse(stdinPayload)).toMatchObject({
prompt: 'Hello',
codexGoals: true,
});
const spawnEnv = vi.mocked(spawn).mock.calls.at(-1)?.[2]?.env as
| Record<string, string>
| undefined;
expect(spawnEnv?.CODEX_GOALS).toBe('true');
});
});

View File

@@ -34,6 +34,7 @@ export interface AgentInput {
useTaskScopedSession?: boolean; useTaskScopedSession?: boolean;
assistantName?: string; assistantName?: string;
agentType?: 'claude-code' | 'codex'; agentType?: 'claude-code' | 'codex';
codexGoals?: boolean;
roomRoleContext?: RoomRoleContext; roomRoleContext?: RoomRoleContext;
} }
@@ -150,13 +151,17 @@ export async function runAgentProcess(
onProcess(proc, processName, env.EJCLAW_IPC_DIR); onProcess(proc, processName, env.EJCLAW_IPC_DIR);
proc.stdin.write(JSON.stringify(input)); const runnerInput: AgentInput = {
...input,
...(group.agentConfig?.codexGoals === true ? { codexGoals: true } : {}),
};
proc.stdin.write(JSON.stringify(runnerInput));
proc.stdin.end(); proc.stdin.end();
runSpawnedAgentProcess({ runSpawnedAgentProcess({
proc, proc,
group, group,
input, input: runnerInput,
processName, processName,
logsDir, logsDir,
startTime, startTime,

View File

@@ -0,0 +1,45 @@
import fs from 'node:fs';
import os from 'node:os';
import path from 'node:path';
import { afterEach, beforeEach, describe, expect, it } from 'vitest';
import { getCodexFeatures, updateCodexFeatures } from './settings-store.js';
describe('settings-store Codex features', () => {
let tempDir: string;
let previousCwd: string;
let previousCodexGoals: string | undefined;
beforeEach(() => {
tempDir = fs.mkdtempSync(path.join(os.tmpdir(), 'ejclaw-settings-'));
previousCwd = process.cwd();
previousCodexGoals = process.env.CODEX_GOALS;
delete process.env.CODEX_GOALS;
process.chdir(tempDir);
});
afterEach(() => {
process.chdir(previousCwd);
if (previousCodexGoals === undefined) {
delete process.env.CODEX_GOALS;
} else {
process.env.CODEX_GOALS = previousCodexGoals;
}
fs.rmSync(tempDir, { recursive: true, force: true });
});
it('stores the Codex goals opt-in in the EJClaw .env file', () => {
expect(getCodexFeatures()).toEqual({ goals: false });
expect(updateCodexFeatures({ goals: true })).toEqual({ goals: true });
expect(fs.readFileSync(path.join(tempDir, '.env'), 'utf-8')).toContain(
'CODEX_GOALS=true',
);
expect(updateCodexFeatures({ goals: false })).toEqual({ goals: false });
expect(fs.readFileSync(path.join(tempDir, '.env'), 'utf-8')).toContain(
'CODEX_GOALS=false',
);
});
});

View File

@@ -56,6 +56,10 @@ export interface FastModeSnapshot {
claude: boolean; claude: boolean;
} }
export interface CodexFeatureSnapshot {
goals: boolean;
}
const ROLE_KEYS = ['OWNER', 'REVIEWER', 'ARBITER'] as const; const ROLE_KEYS = ['OWNER', 'REVIEWER', 'ARBITER'] as const;
function envFilePath(): string { function envFilePath(): string {
@@ -634,6 +638,36 @@ export function updateFastMode(
return getFastMode(); return getFastMode();
} }
function readCodexGoals(): boolean {
return readEnvOrProcess('CODEX_GOALS') === 'true';
}
function writeCodexGoals(value: boolean): void {
const file = envFilePath();
const content = fs.existsSync(file) ? fs.readFileSync(file, 'utf-8') : '';
const updated = setOrInsertEnvLine(
content,
'CODEX_GOALS',
value ? 'true' : 'false',
);
const tempPath = `${file}.tmp`;
fs.writeFileSync(tempPath, updated, { mode: 0o600 });
fs.renameSync(tempPath, file);
}
export function getCodexFeatures(): CodexFeatureSnapshot {
return {
goals: readCodexGoals(),
};
}
export function updateCodexFeatures(
input: Partial<CodexFeatureSnapshot>,
): CodexFeatureSnapshot {
if (typeof input.goals === 'boolean') writeCodexGoals(input.goals);
return getCodexFeatures();
}
export function removeAccountDirectory( export function removeAccountDirectory(
provider: 'claude' | 'codex', provider: 'claude' | 'codex',
index: number, index: number,

View File

@@ -5,6 +5,7 @@ export interface AgentConfig {
// Per-group model/effort overrides (take precedence over global env vars) // Per-group model/effort overrides (take precedence over global env vars)
codexModel?: string; codexModel?: string;
codexEffort?: string; codexEffort?: string;
codexGoals?: boolean;
claudeModel?: string; claudeModel?: string;
claudeEffort?: string; claudeEffort?: string;
claudeThinking?: 'adaptive' | 'enabled' | 'disabled'; claudeThinking?: 'adaptive' | 'enabled' | 'disabled';

View File

@@ -7,6 +7,7 @@ import {
import type { import type {
ClaudeAccountSummary, ClaudeAccountSummary,
CodexAccountSummary, CodexAccountSummary,
CodexFeatureSnapshot,
FastModeSnapshot, FastModeSnapshot,
ModelConfigSnapshot, ModelConfigSnapshot,
} from './settings-store.js'; } from './settings-store.js';
@@ -42,6 +43,7 @@ const modelConfig: ModelConfigSnapshot = {
}; };
const fastMode: FastModeSnapshot = { codex: true, claude: false }; const fastMode: FastModeSnapshot = { codex: true, claude: false };
const codexFeatures: CodexFeatureSnapshot = { goals: false };
const moaSettings: MoaSettingsSnapshot = { const moaSettings: MoaSettingsSnapshot = {
enabled: true, enabled: true,
@@ -107,6 +109,7 @@ function makeDeps(
responseLength: 2, responseLength: 2,
}), }),
getActiveCodexSettingsIndex: () => 1, getActiveCodexSettingsIndex: () => 1,
getCodexFeatures: () => codexFeatures,
getFastMode: () => fastMode, getFastMode: () => fastMode,
getModelConfig: () => modelConfig, getModelConfig: () => modelConfig,
getMoaSettings: () => moaSettings, getMoaSettings: () => moaSettings,
@@ -116,6 +119,7 @@ function makeDeps(
refreshCodexAccount: async () => makeCodexAccount(), refreshCodexAccount: async () => makeCodexAccount(),
removeAccountDirectory: () => undefined, removeAccountDirectory: () => undefined,
setActiveCodexSettingsIndex: () => undefined, setActiveCodexSettingsIndex: () => undefined,
updateCodexFeatures: () => codexFeatures,
updateFastMode: () => fastMode, updateFastMode: () => fastMode,
updateModelConfig: () => modelConfig, updateModelConfig: () => modelConfig,
updateMoaSettings: () => moaSettings, updateMoaSettings: () => moaSettings,
@@ -155,6 +159,10 @@ describe('web dashboard settings routes', () => {
expect(mode?.status).toBe(200); expect(mode?.status).toBe(200);
await expect(mode?.json()).resolves.toEqual(fastMode); await expect(mode?.json()).resolves.toEqual(fastMode);
const features = await route('/api/settings/codex-features');
expect(features?.status).toBe(200);
await expect(features?.json()).resolves.toEqual(codexFeatures);
const moa = await route('/api/settings/moa'); const moa = await route('/api/settings/moa');
expect(moa?.status).toBe(200); expect(moa?.status).toBe(200);
const moaJson = (await moa?.json()) as MoaSettingsSnapshot; const moaJson = (await moa?.json()) as MoaSettingsSnapshot;
@@ -187,6 +195,10 @@ describe('web dashboard settings routes', () => {
calls.push(`models:${JSON.stringify(input)}`); calls.push(`models:${JSON.stringify(input)}`);
return modelConfig; return modelConfig;
}, },
updateCodexFeatures: (input) => {
calls.push(`codex-features:${JSON.stringify(input)}`);
return { goals: input.goals === true };
},
updateMoaSettings: (input) => { updateMoaSettings: (input) => {
calls.push(`moa:${JSON.stringify(input)}`); calls.push(`moa:${JSON.stringify(input)}`);
return moaSettings; return moaSettings;
@@ -230,6 +242,15 @@ describe('web dashboard settings routes', () => {
codexCurrentIndex: 1, codexCurrentIndex: 1,
}); });
const goals = await route(
'/api/settings/codex-features',
'PATCH',
{ goals: true },
deps,
);
expect(goals?.status).toBe(200);
await expect(goals?.json()).resolves.toEqual({ goals: true });
const moa = await route( const moa = await route(
'/api/settings/moa', '/api/settings/moa',
'PATCH', 'PATCH',
@@ -255,6 +276,7 @@ describe('web dashboard settings routes', () => {
'add:claude-token', 'add:claude-token',
'delete:codex:4', 'delete:codex:4',
'current:4', 'current:4',
'codex-features:{"goals":true}',
'moa:{"enabled":false,"models":[{"name":"kimi","enabled":false}]}', 'moa:{"enabled":false,"models":[{"name":"kimi","enabled":false}]}',
]); ]);
}); });

View File

@@ -1,6 +1,7 @@
import { import {
addClaudeAccountFromToken, addClaudeAccountFromToken,
getActiveCodexSettingsIndex, getActiveCodexSettingsIndex,
getCodexFeatures,
getFastMode, getFastMode,
getModelConfig, getModelConfig,
listClaudeAccounts, listClaudeAccounts,
@@ -9,9 +10,11 @@ import {
refreshCodexAccount, refreshCodexAccount,
removeAccountDirectory, removeAccountDirectory,
setActiveCodexSettingsIndex, setActiveCodexSettingsIndex,
updateCodexFeatures,
updateFastMode, updateFastMode,
updateModelConfig, updateModelConfig,
type ClaudeAccountSummary, type ClaudeAccountSummary,
type CodexFeatureSnapshot,
type CodexAccountSummary, type CodexAccountSummary,
type FastModeSnapshot, type FastModeSnapshot,
type ModelConfigSnapshot, type ModelConfigSnapshot,
@@ -33,6 +36,7 @@ export interface SettingsRouteDependencies {
addClaudeAccountFromToken: typeof addClaudeAccountFromToken; addClaudeAccountFromToken: typeof addClaudeAccountFromToken;
checkMoaModel: typeof checkMoaModel; checkMoaModel: typeof checkMoaModel;
getActiveCodexSettingsIndex: typeof getActiveCodexSettingsIndex; getActiveCodexSettingsIndex: typeof getActiveCodexSettingsIndex;
getCodexFeatures: typeof getCodexFeatures;
getFastMode: typeof getFastMode; getFastMode: typeof getFastMode;
getModelConfig: typeof getModelConfig; getModelConfig: typeof getModelConfig;
getMoaSettings: typeof getMoaSettings; getMoaSettings: typeof getMoaSettings;
@@ -42,6 +46,7 @@ export interface SettingsRouteDependencies {
refreshCodexAccount: typeof refreshCodexAccount; refreshCodexAccount: typeof refreshCodexAccount;
removeAccountDirectory: typeof removeAccountDirectory; removeAccountDirectory: typeof removeAccountDirectory;
setActiveCodexSettingsIndex: typeof setActiveCodexSettingsIndex; setActiveCodexSettingsIndex: typeof setActiveCodexSettingsIndex;
updateCodexFeatures: typeof updateCodexFeatures;
updateFastMode: typeof updateFastMode; updateFastMode: typeof updateFastMode;
updateModelConfig: typeof updateModelConfig; updateModelConfig: typeof updateModelConfig;
updateMoaSettings: typeof updateMoaSettings; updateMoaSettings: typeof updateMoaSettings;
@@ -58,6 +63,7 @@ const defaultSettingsRouteDependencies: SettingsRouteDependencies = {
addClaudeAccountFromToken, addClaudeAccountFromToken,
checkMoaModel, checkMoaModel,
getActiveCodexSettingsIndex, getActiveCodexSettingsIndex,
getCodexFeatures,
getFastMode, getFastMode,
getModelConfig, getModelConfig,
getMoaSettings, getMoaSettings,
@@ -67,6 +73,7 @@ const defaultSettingsRouteDependencies: SettingsRouteDependencies = {
refreshCodexAccount, refreshCodexAccount,
removeAccountDirectory, removeAccountDirectory,
setActiveCodexSettingsIndex, setActiveCodexSettingsIndex,
updateCodexFeatures,
updateFastMode, updateFastMode,
updateModelConfig, updateModelConfig,
updateMoaSettings, updateMoaSettings,
@@ -133,6 +140,23 @@ async function handleFastModeRoute(
} }
} }
async function handleCodexFeaturesRoute(
request: Request,
jsonResponse: JsonResponse,
deps: SettingsRouteDependencies,
): Promise<Response | null> {
if (readMethod(request.method)) return jsonResponse(deps.getCodexFeatures());
if (request.method !== 'PUT' && request.method !== 'PATCH') return null;
const body = await readJsonObject(request, jsonResponse);
if (body instanceof Response) return body;
try {
return jsonResponse(deps.updateCodexFeatures(body));
} catch (err) {
return jsonResponse({ error: errorMessage(err) }, { status: 500 });
}
}
async function handleMoaSettingsRoute( async function handleMoaSettingsRoute(
request: Request, request: Request,
jsonResponse: JsonResponse, jsonResponse: JsonResponse,
@@ -299,6 +323,9 @@ export async function handleSettingsRoute({
if (url.pathname === '/api/settings/fast-mode') { if (url.pathname === '/api/settings/fast-mode') {
return handleFastModeRoute(request, jsonResponse, deps); return handleFastModeRoute(request, jsonResponse, deps);
} }
if (url.pathname === '/api/settings/codex-features') {
return handleCodexFeaturesRoute(request, jsonResponse, deps);
}
if (url.pathname === '/api/settings/moa') { if (url.pathname === '/api/settings/moa') {
return handleMoaSettingsRoute(request, jsonResponse, deps); return handleMoaSettingsRoute(request, jsonResponse, deps);
} }
@@ -335,6 +362,7 @@ export async function handleSettingsRoute({
export type { export type {
ClaudeAccountSummary, ClaudeAccountSummary,
CodexAccountSummary, CodexAccountSummary,
CodexFeatureSnapshot,
FastModeSnapshot, FastModeSnapshot,
ModelConfigSnapshot, ModelConfigSnapshot,
MoaSettingsSnapshot, MoaSettingsSnapshot,