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.
Files changed (38) hide show
  1. package/README.md +6 -3
  2. package/cli/openai-bridge-retry.js +17 -9
  3. package/cli/openai-bridge-runtime.js +14 -1
  4. package/cli/openai-bridge.js +20 -10
  5. package/cli.js +375 -4
  6. package/package.json +1 -1
  7. package/web-ui/app.js +18 -4
  8. package/web-ui/index.html +1 -0
  9. package/web-ui/modules/app.computed.dashboard.mjs +14 -0
  10. package/web-ui/modules/app.methods.agents.mjs +2 -2
  11. package/web-ui/modules/app.methods.index.mjs +2 -0
  12. package/web-ui/modules/app.methods.install.mjs +14 -0
  13. package/web-ui/modules/app.methods.kilocode-config.mjs +164 -0
  14. package/web-ui/modules/app.methods.navigation.mjs +12 -0
  15. package/web-ui/modules/app.methods.openclaw-core.mjs +87 -0
  16. package/web-ui/modules/app.methods.providers.mjs +5 -32
  17. package/web-ui/modules/app.methods.startup-claude.mjs +2 -1
  18. package/web-ui/modules/app.methods.tool-config-permissions.mjs +3 -2
  19. package/web-ui/modules/app.methods.web-ui-preferences.mjs +2 -1
  20. package/web-ui/modules/config-mode.computed.mjs +11 -1
  21. package/web-ui/modules/i18n/locales/en.mjs +31 -4
  22. package/web-ui/modules/i18n/locales/ja.mjs +34 -8
  23. package/web-ui/modules/i18n/locales/vi.mjs +35 -9
  24. package/web-ui/modules/i18n/locales/zh-tw.mjs +36 -9
  25. package/web-ui/modules/i18n/locales/zh.mjs +40 -13
  26. package/web-ui/partials/index/layout-header.html +32 -21
  27. package/web-ui/partials/index/modal-config-template-agents.html +8 -4
  28. package/web-ui/partials/index/modals-basic.html +0 -26
  29. package/web-ui/partials/index/panel-config-kilocode.html +101 -0
  30. package/web-ui/partials/index/panel-config-openclaw.html +42 -14
  31. package/web-ui/partials/index/panel-config-opencode.html +1 -0
  32. package/web-ui/partials/index/panel-docs.html +17 -1
  33. package/web-ui/res/web-ui-render.precompiled.js +400 -123
  34. package/web-ui/styles/controls-forms.css +10 -0
  35. package/web-ui/styles/docs-panel.css +51 -0
  36. package/web-ui/styles/layout-shell.css +12 -0
  37. package/web-ui/styles/modals-core.css +79 -0
  38. 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;
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
- const base = Number.isFinite(raw) ? raw : (Number.isFinite(fallbackRaw) ? fallbackRaw : DEFAULT_BRIDGE_MAX_RETRIES);
11
- return Math.min(MAX_BRIDGE_MAX_RETRIES, Math.max(MIN_BRIDGE_MAX_RETRIES, Math.floor(base)));
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
 
@@ -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, maxRetries };
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
- }), { maxRetries: upstream.maxRetries });
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
- }), { maxRetries: upstream.maxRetries });
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
- }), { maxRetries: upstream.maxRetries });
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: e && e.message ? e.message : 'Internal 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,