Skip to content
Merged
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
4 changes: 2 additions & 2 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -60,7 +60,7 @@ Unlike simple wrappers, Codex Mate acts as a **Local Agent Bridge**:
- **OpenCode Provider Control**: Manage OpenCode provider/model selection with a CodexMate-owned provider store under `~/.codexmate`, projecting only the active provider into native OpenCode config to avoid polluting or deleting user-owned settings.
- **Provider Health Cleanup**: Probe local Codex and Claude provider routes, surface failed configs in one modal, and bulk-clean selected broken providers without touching healthy or protected entries.
- **Skills Marketplace**: A local-first market to share and import skills between different agent apps.
- **Prompt File Editor**: Unified editor for global and project-level `CLAUDE.md` and `AGENTS.md` with auto-detection of project paths.
- **Prompt File Editor**: Unified editor for global and project-level `CLAUDE.md` and `AGENTS.md` with auto-detection of project paths, plus a shared preset pool for reusable prompts.
- **Task Orchestrator**: Plan and execute complex tasks with dependency tracking.

---
Expand All @@ -80,7 +80,7 @@ Unlike simple wrappers, Codex Mate acts as a **Local Agent Bridge**:
| **OpenCode Provider Store** | ✅ | Keep multiple OpenCode providers in `~/.codexmate` while projecting only the selected provider to native OpenCode config |
| **Provider Health Check** | ✅ | Probe local Codex/Claude provider routes, highlight failed configs, and bulk-remove selected broken providers safely |
| **Prompt Templates** | ✅ | Reusable prompt plugins with variables |
| **Prompt File Editor** | ✅ | Edit global and project-level CLAUDE.md / AGENTS.md with auto-detect and path switching |
| **Prompt File Editor** | ✅ | Edit global and project-level CLAUDE.md / AGENTS.md with auto-detect, path switching, and a shared preset pool. Applying a preset only updates the editor; save manually to write the file. |
| **MCP Integration** | ✅ | Expose local tools and resources via MCP stdio |
| **Auto Update** | ✅ | Quick update CLI via `codexmate update` |

Expand Down
1 change: 1 addition & 0 deletions README.vi.md
Original file line number Diff line number Diff line change
Expand Up @@ -67,6 +67,7 @@ Khác với các wrapper đơn giản, Codex Mate hoạt động như một **Ag
| **Bridge OpenAI** | ✅ | Chuyển đổi Codex Responses API sang định dạng OpenAI chuẩn |
| **Kiểm tra sức khỏe Provider** | ✅ | Kiểm tra route provider Codex/Claude, đánh dấu cấu hình lỗi và xóa hàng loạt các provider hỏng đã chọn một cách an toàn |
| **Mẫu Prompt** | ✅ | Plugin prompt tái sử dụng được với hỗ trợ biến |
| **Trình sửa tệp prompt** | ✅ | Chỉnh sửa CLAUDE.md / AGENTS.md và dùng kho preset chung. Áp dụng preset chỉ cập nhật editor; cần lưu thủ công để ghi vào tệp. |
| **Tích hợp MCP** | ✅ | Expose tool và resource cục bộ qua MCP stdio |
| **Tự động cập nhật** | ✅ | Cập nhật nhanh qua `codexmate update` |

Expand Down
4 changes: 2 additions & 2 deletions README.zh.md
Original file line number Diff line number Diff line change
Expand Up @@ -59,7 +59,7 @@
- **OpenCode Provider 控制**:在 `~/.codexmate` 下维护 CodexMate 自有的 OpenCode 多 provider 存储,只将当前选中的 provider 投影到 OpenCode 原生配置,避免污染或误删用户已有配置。
- **Provider 健康清理**:探测本地 Codex 与 Claude provider 路由,在同一个弹窗里集中展示失败配置,并批量清理已选坏配置,不影响正常或受保护项。
- **Skills 市场**:本地优先的市场,支持在不同的智能体应用之间共享和导入 Skills。
- **提示词文件编辑器**:统一编辑全局和项目级 `CLAUDE.md` 与 `AGENTS.md`,支持项目路径自动检测。
- **提示词文件编辑器**:统一编辑全局和项目级 `CLAUDE.md` 与 `AGENTS.md`,支持项目路径自动检测,并提供通用预设池复用 prompt
- **任务编排器**:支持带有依赖跟踪的复杂任务规划与执行。

---
Expand All @@ -79,7 +79,7 @@
| **OpenCode Provider 存储** | ✅ | 在 `~/.codexmate` 中保留多个 OpenCode provider,只将当前选中的 provider 投影到 OpenCode 原生配置 |
| **Provider 健康检查** | ✅ | 探测本地 Codex/Claude provider 路由,标记失败配置,并安全批量移除已选坏配置 |
| **提示词模板** | ✅ | 支持变量的可复用提示词插件 |
| **提示词文件编辑器** | ✅ | 编辑全局和项目级 CLAUDE.md / AGENTS.md,支持自动检测与路径切换 |
| **提示词文件编辑器** | ✅ | 编辑全局和项目级 CLAUDE.md / AGENTS.md,支持自动检测、路径切换与通用预设池。应用预设只更新编辑器,需手动保存才会写入文件。 |
| **MCP 集成** | ✅ | 通过 MCP stdio 暴露本地工具与资源 |
| **自动更新** | ✅ | 通过 `codexmate update` 快速更新 CLI |

Expand Down
122 changes: 7 additions & 115 deletions cli/openai-bridge.js
Original file line number Diff line number Diff line change
Expand Up @@ -425,54 +425,14 @@ function createOpenaiBridgeHttpHandler(options = {}) {
const acceptHeader = req && req.headers ? (req.headers.accept || req.headers.Accept || '') : '';
const wantsSse = /text\/event-stream/i.test(String(acceptHeader || ''));

if (streamRequested && wantsSse) {
const upstreamResponsesUrl = joinApiUrl(upstream.baseUrl, 'responses');
const skipResponsesProbe = isResponsesKnownUnsupported(upstream.baseUrl);
const streamedResponses = skipResponsesProbe
? { ok: false, status: 404, bodyText: '' }
: await retryTransientRequest(() => streamResponsesSse(upstreamResponsesUrl, {
method: 'POST',
body: normalizeResponsesPayloadForUpstream(responsesRequest, true),
headers: codexHeaders,
timeoutMs: streamTimeoutMs,
maxBytes: maxUpstreamBytes,
httpAgent,
httpsAgent,
res
}), { maxRetries: upstream.maxRetries });

if (streamedResponses.ok) {
clearResponsesUnsupported(upstream.baseUrl);
return;
}

const canFallbackToChat = streamedResponses.status
? shouldFallbackFromUpstreamResponses(streamedResponses.status, streamedResponses.bodyText)
: false;
if (!canFallbackToChat) {
if (res.writableEnded || res.destroyed) return;
const status = streamedResponses.status && streamedResponses.status >= 400 ? streamedResponses.status : 502;
if (!res.headersSent) {
res.writeHead(status, { 'Content-Type': 'application/json; charset=utf-8' });
res.end(streamedResponses.bodyText || JSON.stringify({ error: streamedResponses.error || 'Upstream request failed' }));
} else {
writeSse(res, 'response.failed', { type: 'response.failed', error: streamedResponses.error || streamedResponses.bodyText || 'Upstream request failed' });
writeSse(res, 'done', '[DONE]');
res.end();
}
return;
}

if (!skipResponsesProbe && isResponsesEndpointUnsupported(streamedResponses.status, streamedResponses.bodyText)) {
markResponsesUnsupported(upstream.baseUrl);
}
const converted = convertResponsesRequestToChatCompletions(responsesRequest);
if (converted.error) {
res.writeHead(400, { 'Content-Type': 'application/json; charset=utf-8' });
res.end(JSON.stringify({ error: converted.error }));
return;
}

const converted = convertResponsesRequestToChatCompletions(responsesRequest);
if (converted.error) {
res.writeHead(400, { 'Content-Type': 'application/json; charset=utf-8' });
res.end(JSON.stringify({ error: converted.error }));
return;
}
if (streamRequested && wantsSse) {
const upstreamUrl = joinApiUrl(upstream.baseUrl, 'chat/completions');
const chatBody = { ...converted.chat, stream: true };
const streamed = await retryTransientRequest(() => streamChatCompletionsAsResponsesSse(upstreamUrl, {
Expand Down Expand Up @@ -503,74 +463,6 @@ function createOpenaiBridgeHttpHandler(options = {}) {
return;
}

// Maxx-style behavior: prefer upstream /responses if supported.
// Fallback to /chat/completions conversion when upstream does not implement /responses (404/405).
// 已知不支持的上游:直接跳过探测,节省一次 round-trip。
const skipResponsesProbe = isResponsesKnownUnsupported(upstream.baseUrl);
const upstreamResponsesUrl = joinApiUrl(upstream.baseUrl, 'responses');
const upstreamResponsesResult = skipResponsesProbe
? { ok: true, status: 404, bodyText: '' }
: await retryTransientRequest(() => proxyRequestJson(upstreamResponsesUrl, {
method: 'POST',
body: toUpstreamNonStreamingResponsesPayload(responsesRequest),
headers: codexHeaders,
maxBytes: maxUpstreamBytes,
httpAgent,
httpsAgent
}), { maxRetries: upstream.maxRetries });

if (upstreamResponsesResult.ok && upstreamResponsesResult.status >= 200 && upstreamResponsesResult.status < 300) {
clearResponsesUnsupported(upstream.baseUrl);
const upstreamJson = parseJsonOrError(upstreamResponsesResult.bodyText);
if (upstreamJson.error) {
res.writeHead(502, { 'Content-Type': 'application/json; charset=utf-8' });
res.end(JSON.stringify({ error: `Upstream JSON parse failed: ${upstreamJson.error}` }));
return;
}
const upstreamPayload = upstreamJson.value;
if (streamRequested && wantsSse) {
res.writeHead(200, {
'Content-Type': 'text/event-stream; charset=utf-8',
'Cache-Control': 'no-cache',
'Connection': 'keep-alive',
'X-Accel-Buffering': 'no'
});
if (typeof res.flushHeaders === 'function') res.flushHeaders();
sendResponsesSse(res, upstreamPayload);
res.end();
return;
}

res.writeHead(200, { 'Content-Type': 'application/json; charset=utf-8' });
res.end(JSON.stringify(ensureResponseMetadata(upstreamPayload)));
return;
}

if (upstreamResponsesResult.ok && upstreamResponsesResult.status >= 400) {
if (!shouldFallbackFromUpstreamResponses(upstreamResponsesResult.status, upstreamResponsesResult.bodyText)) {
res.writeHead(upstreamResponsesResult.status, { 'Content-Type': 'application/json; charset=utf-8' });
res.end(upstreamResponsesResult.bodyText || JSON.stringify({ error: 'Upstream error' }));
return;
}
if (!skipResponsesProbe && isResponsesEndpointUnsupported(upstreamResponsesResult.status, upstreamResponsesResult.bodyText)) {
markResponsesUnsupported(upstream.baseUrl);
}
// fallthrough to chat/completions conversion
}

if (!upstreamResponsesResult.ok) {
res.writeHead(502, { 'Content-Type': 'application/json; charset=utf-8' });
res.end(JSON.stringify({ error: `Upstream request failed: ${upstreamResponsesResult.error}` }));
return;
}

const converted = convertResponsesRequestToChatCompletions(responsesRequest);
if (converted.error) {
res.writeHead(400, { 'Content-Type': 'application/json; charset=utf-8' });
res.end(JSON.stringify({ error: converted.error }));
return;
}

const upstreamUrl = joinApiUrl(upstream.baseUrl, 'chat/completions');
const upstreamResult = await retryTransientRequest(() => proxyRequestJson(upstreamUrl, {
method: 'POST',
Expand Down
4 changes: 2 additions & 2 deletions package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

2 changes: 1 addition & 1 deletion package.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{
"name": "codexmate",
"version": "0.1.0",
"version": "0.1.1",
"description": "Codex/Claude Code/OpenClaw 配置、会话与任务编排 CLI + Web 工具",
"main": "cli.js",
"bin": {
Expand Down
23 changes: 23 additions & 0 deletions tests/unit/config-tabs-ui.test.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -83,6 +83,29 @@ test('config template keeps expected config tabs in top and side navigation', ()
assert.match(webUiRedirectBlock, /url\.pathname\s*=\s*'\/'/);
assert.doesNotMatch(webUiRedirectBlock, /url\.search\s*=/);
assert.doesNotMatch(webUiRedirectBlock, /url\.hash\s*=/);
assert.match(html, /id="side-tab-prompts"/);
assert.doesNotMatch(html, /<div class="side-section-title">\{\{ t\('side\.prompts'\) \}\}<\/div>/);
assert.doesNotMatch(html, /<div class="side-section" role="navigation" :aria-label="t\('side\.prompts'\)">/);
assert.doesNotMatch(html, /id="side-tab-prompts-agents"/);
assert.doesNotMatch(html, /id="side-tab-prompts-project"/);
assert.doesNotMatch(html, /id="side-tab-prompts-presets"/);
assert.match(html, /@click="onMainTabClick\('prompts', \$event\)"/);
assert.match(html, /t\('side\.prompts'\)/);
assert.match(html, /t\('side\.prompts\.meta'\)/);
assert.doesNotMatch(html, /promptsSubTab === 'presets'/);
assert.doesNotMatch(html, /switchPromptsSubTab\('presets'\)/);
assert.match(html, /<details class="prompt-presets-panel">/);
assert.match(html, /class="form-input prompt-presets-select"/);
assert.match(html, /@change="applyPromptPresetSelection\(\$event\)"/);
assert.match(html, /t\('prompts\.presets\.selectPlaceholder'\)/);
assert.match(html, /v-model="promptPresetNameDraft"/);
assert.match(html, /@keydown.enter.prevent="saveCurrentPromptAsPreset"/);
assert.match(html, /@click="saveCurrentPromptAsPreset"/);
assert.match(html, /@click="applyPromptPresetToEditor\(preset\)"/);
assert.doesNotMatch(html, /@click="applyPromptPresetToEditor\(preset, 'codex'\)"/);
assert.doesNotMatch(html, /@click="applyPromptPresetToEditor\(preset, 'claude-project'\)"/);
assert.match(html, /class="form-group prompt-presets-body"/);
assert.match(html, /class="editor-frame prompt-presets-frame"/);
assert.match(html, /id="side-tab-orchestration"/);
assert.match(html, /id="tab-orchestration"/);
assert.match(html, /data-main-tab="orchestration"/);
Expand Down
11 changes: 10 additions & 1 deletion tests/unit/i18n-locales.test.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -96,8 +96,17 @@ test('Japanese orchestration template copy stays localized', () => {
}
});

test('provider cache and local Web preference settings are localized in every locale', () => {
test('provider cache, prompt sidebar, and local Web preference settings are localized in every locale', () => {
const keys = [
'side.prompts.meta',
'side.prompts.presets',
'side.prompts.presets.meta',
'prompts.presets.addCurrent',
'prompts.presets.defaultName.project',
'prompts.presets.confirm.addCurrentTitle',
'prompts.presets.confirm.addCurrentMessage',
'prompts.presets.toast.pasted',
'prompts.presets.selectPlaceholder',
'announcement.providerCache.open',
'announcement.project.eyebrow',
'announcement.project.title',
Expand Down
Loading
Loading