codexmate 0.1.1 → 0.1.3
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +6 -3
- package/cli/openai-bridge-retry.js +17 -9
- package/cli/openai-bridge-runtime.js +14 -1
- package/cli/openai-bridge.js +20 -10
- package/cli.js +375 -4
- package/package.json +1 -1
- package/web-ui/app.js +18 -4
- package/web-ui/index.html +1 -0
- package/web-ui/modules/app.computed.dashboard.mjs +14 -0
- package/web-ui/modules/app.methods.agents.mjs +2 -2
- package/web-ui/modules/app.methods.index.mjs +2 -0
- package/web-ui/modules/app.methods.install.mjs +14 -0
- package/web-ui/modules/app.methods.kilocode-config.mjs +164 -0
- package/web-ui/modules/app.methods.navigation.mjs +12 -0
- package/web-ui/modules/app.methods.openclaw-core.mjs +87 -0
- package/web-ui/modules/app.methods.providers.mjs +5 -32
- package/web-ui/modules/app.methods.startup-claude.mjs +2 -1
- package/web-ui/modules/app.methods.tool-config-permissions.mjs +3 -2
- package/web-ui/modules/app.methods.web-ui-preferences.mjs +2 -1
- package/web-ui/modules/config-mode.computed.mjs +11 -1
- package/web-ui/modules/i18n/locales/en.mjs +31 -4
- package/web-ui/modules/i18n/locales/ja.mjs +34 -8
- package/web-ui/modules/i18n/locales/vi.mjs +35 -9
- package/web-ui/modules/i18n/locales/zh-tw.mjs +36 -9
- package/web-ui/modules/i18n/locales/zh.mjs +40 -13
- package/web-ui/partials/index/layout-header.html +32 -21
- package/web-ui/partials/index/modal-config-template-agents.html +8 -4
- package/web-ui/partials/index/modals-basic.html +0 -26
- package/web-ui/partials/index/panel-config-kilocode.html +101 -0
- package/web-ui/partials/index/panel-config-openclaw.html +42 -14
- package/web-ui/partials/index/panel-config-opencode.html +1 -0
- package/web-ui/partials/index/panel-docs.html +17 -1
- package/web-ui/res/web-ui-render.precompiled.js +400 -123
- package/web-ui/styles/controls-forms.css +10 -0
- package/web-ui/styles/docs-panel.css +51 -0
- package/web-ui/styles/layout-shell.css +12 -0
- package/web-ui/styles/modals-core.css +79 -0
- package/web-ui/styles/openclaw-structured.css +117 -71
package/README.md
CHANGED
|
@@ -4,7 +4,7 @@
|
|
|
4
4
|
|
|
5
5
|
# Codex Mate
|
|
6
6
|
|
|
7
|
-
**One dashboard for all your local AI coding agents. Switch providers, manage sessions, and orchestrate tasks across Codex, Claude Code, OpenCode, and OpenClaw. Zero cloud, local-first control plane.**
|
|
7
|
+
**One dashboard for all your local AI coding agents. Switch providers, manage sessions, and orchestrate tasks across Codex, Claude Code, OpenCode, KiloCode, and OpenClaw. Zero cloud, local-first control plane.**
|
|
8
8
|
|
|
9
9
|
<p>
|
|
10
10
|
<a href="https://sakurabytecore.github.io/codexmate/">[Documentation]</a>
|
|
@@ -49,7 +49,7 @@
|
|
|
49
49
|
|
|
50
50
|
Have you ever felt overwhelmed by managing multiple local AI agents? Each has its own config format, session storage, and skills directory.
|
|
51
51
|
|
|
52
|
-
**Codex Mate** offers a unified control plane to bring order to the chaos. It's a local-first CLI + Web UI designed to manage [Codex](https://github.com/openai/codex), [Claude Code](https://github.com/anthropic-ai/claude-code), [OpenCode](https://opencode.ai/), and [OpenClaw](https://github.com/moeru-ai/openclaw) seamlessly.
|
|
52
|
+
**Codex Mate** offers a unified control plane to bring order to the chaos. It's a local-first CLI + Web UI designed to manage [Codex](https://github.com/openai/codex), [Claude Code](https://github.com/anthropic-ai/claude-code), [OpenCode](https://opencode.ai/), KiloCode, and [OpenClaw](https://github.com/moeru-ai/openclaw) seamlessly.
|
|
53
53
|
|
|
54
54
|
### What's So Special?
|
|
55
55
|
|
|
@@ -58,6 +58,7 @@ Unlike simple wrappers, Codex Mate acts as a **Local Agent Bridge**:
|
|
|
58
58
|
- **OpenAI-Compatible Bridge**: Use Codex with any OpenAI-compatible UI by normalizing the Responses API; the built-in Codex conversion also fills and normalizes Codex fingerprint headers such as `User-Agent`, `Version`, `OpenAI-Beta`, and `Originator` so upstream providers see an official Codex CLI-shaped request.
|
|
59
59
|
- **Claude Provider Bridge**: Connect Claude Code to OpenAI Chat Completions-compatible providers and Ollama through the built-in local Claude-compatible proxy.
|
|
60
60
|
- **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.
|
|
61
|
+
- **KiloCode Config Bridge**: Configure KiloCode providers from the CLI or Web UI, writing to `~/.config/kilo/kilo.jsonc` while preserving stored API keys.
|
|
61
62
|
- **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.
|
|
62
63
|
- **Skills Marketplace**: A local-first market to share and import skills between different agent apps.
|
|
63
64
|
- **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.
|
|
@@ -69,7 +70,7 @@ Unlike simple wrappers, Codex Mate acts as a **Local Agent Bridge**:
|
|
|
69
70
|
|
|
70
71
|
| Feature | Status | Description |
|
|
71
72
|
| --- | --- | --- |
|
|
72
|
-
| **Provider Management** | ✅ | Switch providers/models for Codex, Claude, OpenCode, and OpenClaw |
|
|
73
|
+
| **Provider Management** | ✅ | Switch providers/models for Codex, Claude, OpenCode, KiloCode, and OpenClaw |
|
|
73
74
|
| **Live Agent Sync** | ✅ | Real-time monitoring of Codex/Claude config & status |
|
|
74
75
|
| **Session Browser** | ✅ | Search, preview, filter, and export sessions across Codex, Claude Code, Gemini CLI, and CodeBuddy Code |
|
|
75
76
|
| **Usage Analytics** | ✅ | Visualize message trends and top projects |
|
|
@@ -78,6 +79,7 @@ Unlike simple wrappers, Codex Mate acts as a **Local Agent Bridge**:
|
|
|
78
79
|
| **OpenAI Bridge** | ✅ | Convert Codex Responses API to standard OpenAI format and attach/normalize Codex fingerprints in the built-in conversion |
|
|
79
80
|
| **Claude Provider Bridge** | ✅ | Connect Claude Code to OpenAI Chat Completions-compatible providers and Ollama via the built-in Claude-compatible proxy |
|
|
80
81
|
| **OpenCode Provider Store** | ✅ | Keep multiple OpenCode providers in `~/.codexmate` while projecting only the selected provider to native OpenCode config |
|
|
82
|
+
| **KiloCode Config Bridge** | ✅ | Configure KiloCode through `codexmate kilo config` or the Web UI, writing provider URL/model/API key data to `~/.config/kilo/kilo.jsonc` |
|
|
81
83
|
| **Provider Health Check** | ✅ | Probe local Codex/Claude provider routes, highlight failed configs, and bulk-remove selected broken providers safely |
|
|
82
84
|
| **Prompt Templates** | ✅ | Reusable prompt plugins with variables |
|
|
83
85
|
| **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. |
|
|
@@ -128,6 +130,7 @@ curl -fsSL https://raw.githubusercontent.com/SakuraByteCore/codexmate/main/scrip
|
|
|
128
130
|
- **Claude Code**: `npm install -g @anthropic-ai/claude-code`
|
|
129
131
|
- **Gemini CLI**: `npm install -g @google/gemini-cli`
|
|
130
132
|
- **CodeBuddy**: `npm install -g @tencent-ai/codebuddy-code`
|
|
133
|
+
- **KiloCode**: `npm install -g @kilocode/cli` (`kilo` / `kilocode`)
|
|
131
134
|
- **OpenCode**: install from the [official OpenCode docs](https://opencode.ai/)
|
|
132
135
|
|
|
133
136
|
---
|
|
@@ -1,14 +1,20 @@
|
|
|
1
|
-
const DEFAULT_BRIDGE_MAX_RETRIES =
|
|
2
|
-
const MIN_BRIDGE_MAX_RETRIES = 2;
|
|
3
|
-
const MAX_BRIDGE_MAX_RETRIES = 10;
|
|
1
|
+
const DEFAULT_BRIDGE_MAX_RETRIES = Infinity;
|
|
4
2
|
const BASE_TRANSIENT_RETRY_DELAY_MS = 200;
|
|
5
3
|
const MAX_TRANSIENT_RETRY_DELAY_MS = 5000;
|
|
6
4
|
|
|
7
5
|
function normalizeBridgeMaxRetries(value, fallback = DEFAULT_BRIDGE_MAX_RETRIES) {
|
|
8
6
|
const raw = Number(value);
|
|
9
7
|
const fallbackRaw = Number(fallback);
|
|
10
|
-
|
|
11
|
-
|
|
8
|
+
if (Number.isFinite(raw) && raw >= 0) return Math.floor(raw);
|
|
9
|
+
if (Number.isFinite(fallbackRaw) && fallbackRaw >= 0) return Math.floor(fallbackRaw);
|
|
10
|
+
return DEFAULT_BRIDGE_MAX_RETRIES;
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
|
|
14
|
+
function isTransientHttpStatus(status) {
|
|
15
|
+
const code = Number(status);
|
|
16
|
+
if (code === 408 || code === 409 || code === 425 || code === 429) return true;
|
|
17
|
+
return code >= 500 && code <= 599;
|
|
12
18
|
}
|
|
13
19
|
|
|
14
20
|
function isTransientNetworkError(error) {
|
|
@@ -30,7 +36,7 @@ function getTransientRetryDelayMs(attempt) {
|
|
|
30
36
|
async function retryTransientRequest(executor, options = {}) {
|
|
31
37
|
const maxRetries = normalizeBridgeMaxRetries(options && options.maxRetries);
|
|
32
38
|
let lastResult = null;
|
|
33
|
-
for (let attempt = 0; attempt <= maxRetries; attempt += 1) {
|
|
39
|
+
for (let attempt = 0; attempt === 0 || attempt <= maxRetries; attempt += 1) {
|
|
34
40
|
if (attempt > 0) {
|
|
35
41
|
const delay = getTransientRetryDelayMs(attempt);
|
|
36
42
|
// eslint-disable-next-line no-await-in-loop
|
|
@@ -43,9 +49,12 @@ async function retryTransientRequest(executor, options = {}) {
|
|
|
43
49
|
const result = await executor(attempt);
|
|
44
50
|
lastResult = result;
|
|
45
51
|
if (!result) return result;
|
|
52
|
+
if (result.status && result.status > 0) {
|
|
53
|
+
if (!isTransientHttpStatus(result.status)) return result;
|
|
54
|
+
continue;
|
|
55
|
+
}
|
|
46
56
|
if (result.ok) return result;
|
|
47
57
|
if (result.retry) return result;
|
|
48
|
-
if (result.status && result.status > 0) return result;
|
|
49
58
|
if (!isTransientNetworkError(result.error)) return result;
|
|
50
59
|
}
|
|
51
60
|
return lastResult;
|
|
@@ -53,10 +62,9 @@ async function retryTransientRequest(executor, options = {}) {
|
|
|
53
62
|
|
|
54
63
|
module.exports = {
|
|
55
64
|
DEFAULT_BRIDGE_MAX_RETRIES,
|
|
56
|
-
MIN_BRIDGE_MAX_RETRIES,
|
|
57
|
-
MAX_BRIDGE_MAX_RETRIES,
|
|
58
65
|
normalizeBridgeMaxRetries,
|
|
59
66
|
isTransientNetworkError,
|
|
67
|
+
isTransientHttpStatus,
|
|
60
68
|
getTransientRetryDelayMs,
|
|
61
69
|
retryTransientRequest
|
|
62
70
|
};
|
|
@@ -1472,7 +1472,20 @@ function streamChatCompletionsAsResponsesSse(targetUrl, options = {}) {
|
|
|
1472
1472
|
response: {
|
|
1473
1473
|
id: state.responseId,
|
|
1474
1474
|
model: state.model,
|
|
1475
|
-
created_at: state.createdAt
|
|
1475
|
+
created_at: state.createdAt,
|
|
1476
|
+
status: 'in_progress',
|
|
1477
|
+
output: []
|
|
1478
|
+
}
|
|
1479
|
+
});
|
|
1480
|
+
writeSse(res, 'response.in_progress', {
|
|
1481
|
+
type: 'response.in_progress',
|
|
1482
|
+
sequence_number: state.nextSeq(),
|
|
1483
|
+
response: {
|
|
1484
|
+
id: state.responseId,
|
|
1485
|
+
model: state.model,
|
|
1486
|
+
created_at: state.createdAt,
|
|
1487
|
+
status: 'in_progress',
|
|
1488
|
+
output: []
|
|
1476
1489
|
}
|
|
1477
1490
|
});
|
|
1478
1491
|
|
package/cli/openai-bridge.js
CHANGED
|
@@ -87,7 +87,6 @@ function normalizeOpenaiUpstreamBaseUrl(rawValue) {
|
|
|
87
87
|
}
|
|
88
88
|
|
|
89
89
|
const {
|
|
90
|
-
normalizeBridgeMaxRetries,
|
|
91
90
|
parseJsonOrError,
|
|
92
91
|
extractChatCompletionResult,
|
|
93
92
|
convertResponsesRequestToChatCompletions,
|
|
@@ -114,11 +113,10 @@ function normalizeUpstreamEntry(entry) {
|
|
|
114
113
|
const apiKey = normalizeText(entry.apiKey || entry.api_key || entry.key || '');
|
|
115
114
|
const headersRaw = entry.headers || entry.extraHeaders || entry.extra_headers || null;
|
|
116
115
|
const headers = normalizeHeadersMap(headersRaw);
|
|
117
|
-
const maxRetries = normalizeBridgeMaxRetries(entry.maxRetries ?? entry.max_retries);
|
|
118
116
|
if (!baseUrl || !isValidHttpUrl(baseUrl)) {
|
|
119
117
|
return null;
|
|
120
118
|
}
|
|
121
|
-
return { baseUrl, apiKey, headers
|
|
119
|
+
return { baseUrl, apiKey, headers };
|
|
122
120
|
}
|
|
123
121
|
|
|
124
122
|
function normalizeHeadersMap(value) {
|
|
@@ -169,7 +167,6 @@ function upsertOpenaiBridgeProvider(filePath, providerName, upstreamBaseUrl, api
|
|
|
169
167
|
const baseUrl = normalizeOpenaiUpstreamBaseUrl(upstreamBaseUrl);
|
|
170
168
|
const key = normalizeText(apiKey);
|
|
171
169
|
const nextHeaders = normalizeHeadersMap(headers);
|
|
172
|
-
const maxRetries = normalizeBridgeMaxRetries(options && options.maxRetries);
|
|
173
170
|
|
|
174
171
|
if (!name) {
|
|
175
172
|
return { error: 'Provider name is required' };
|
|
@@ -191,7 +188,6 @@ function upsertOpenaiBridgeProvider(filePath, providerName, upstreamBaseUrl, api
|
|
|
191
188
|
baseUrl,
|
|
192
189
|
apiKey: key,
|
|
193
190
|
headers: Object.keys(nextHeaders).length ? nextHeaders : existingHeaders,
|
|
194
|
-
maxRetries
|
|
195
191
|
}
|
|
196
192
|
}
|
|
197
193
|
};
|
|
@@ -384,7 +380,7 @@ function createOpenaiBridgeHttpHandler(options = {}) {
|
|
|
384
380
|
maxBytes: maxUpstreamBytes,
|
|
385
381
|
httpAgent,
|
|
386
382
|
httpsAgent
|
|
387
|
-
})
|
|
383
|
+
}));
|
|
388
384
|
if (!result.ok) {
|
|
389
385
|
res.writeHead(502, { 'Content-Type': 'application/json; charset=utf-8' });
|
|
390
386
|
res.end(JSON.stringify({ error: `Upstream request failed: ${result.error}` }));
|
|
@@ -446,7 +442,7 @@ function createOpenaiBridgeHttpHandler(options = {}) {
|
|
|
446
442
|
res,
|
|
447
443
|
model: typeof chatBody.model === 'string' ? chatBody.model : '',
|
|
448
444
|
toolTypesByName: converted.toolTypesByName || {}
|
|
449
|
-
})
|
|
445
|
+
}));
|
|
450
446
|
if (!streamed.ok) {
|
|
451
447
|
if (res.writableEnded || res.destroyed) {
|
|
452
448
|
return;
|
|
@@ -471,7 +467,7 @@ function createOpenaiBridgeHttpHandler(options = {}) {
|
|
|
471
467
|
maxBytes: maxUpstreamBytes,
|
|
472
468
|
httpAgent,
|
|
473
469
|
httpsAgent
|
|
474
|
-
})
|
|
470
|
+
}));
|
|
475
471
|
if (!upstreamResult.ok) {
|
|
476
472
|
res.writeHead(502, { 'Content-Type': 'application/json; charset=utf-8' });
|
|
477
473
|
res.end(JSON.stringify({ error: `Upstream request failed: ${upstreamResult.error}` }));
|
|
@@ -514,8 +510,23 @@ function createOpenaiBridgeHttpHandler(options = {}) {
|
|
|
514
510
|
res.writeHead(200, { 'Content-Type': 'application/json; charset=utf-8' });
|
|
515
511
|
res.end(JSON.stringify(ensureResponseMetadata(responsesPayload)));
|
|
516
512
|
} catch (e) {
|
|
513
|
+
if (res.writableEnded || res.destroyed) {
|
|
514
|
+
return;
|
|
515
|
+
}
|
|
516
|
+
const message = e && e.message ? e.message : 'Internal Error';
|
|
517
|
+
if (res.headersSent) {
|
|
518
|
+
try {
|
|
519
|
+
res.end();
|
|
520
|
+
} catch (_) {
|
|
521
|
+
// Headers are already committed. Close the socket instead of leaving the client waiting forever.
|
|
522
|
+
if (!res.destroyed && typeof res.destroy === 'function') {
|
|
523
|
+
res.destroy(e);
|
|
524
|
+
}
|
|
525
|
+
}
|
|
526
|
+
return;
|
|
527
|
+
}
|
|
517
528
|
res.writeHead(500, { 'Content-Type': 'application/json; charset=utf-8' });
|
|
518
|
-
res.end(JSON.stringify({ error:
|
|
529
|
+
res.end(JSON.stringify({ error: message }));
|
|
519
530
|
}
|
|
520
531
|
})();
|
|
521
532
|
|
|
@@ -543,7 +554,6 @@ module.exports = {
|
|
|
543
554
|
extractChatCompletionResult,
|
|
544
555
|
buildResponsesPayloadFromChatResult,
|
|
545
556
|
retryTransientRequest,
|
|
546
|
-
normalizeBridgeMaxRetries,
|
|
547
557
|
normalizeOpenaiUpstreamBaseUrl,
|
|
548
558
|
extractResponsesOutputText,
|
|
549
559
|
shouldFallbackFromUpstreamResponses,
|