codexmate 0.0.24 → 0.0.25

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 (35) hide show
  1. package/README.md +28 -6
  2. package/README.zh.md +29 -7
  3. package/cli/builtin-proxy.js +35 -0
  4. package/cli/claude-proxy.js +24 -0
  5. package/cli/import-skills-url.js +23 -1
  6. package/cli/openai-bridge.js +51 -4
  7. package/cli/session-usage.js +8 -2
  8. package/cli.js +1543 -117
  9. package/lib/automation.js +404 -0
  10. package/lib/cli-path-utils.js +21 -5
  11. package/lib/cli-sessions.js +32 -1
  12. package/lib/download-artifacts.js +17 -2
  13. package/lib/mcp-stdio.js +13 -0
  14. package/package.json +2 -5
  15. package/web-ui/app.js +6 -3
  16. package/web-ui/logic.sessions.mjs +2 -2
  17. package/web-ui/modules/app.computed.dashboard.mjs +2 -0
  18. package/web-ui/modules/app.computed.session.mjs +17 -0
  19. package/web-ui/modules/app.methods.install.mjs +28 -0
  20. package/web-ui/modules/app.methods.session-actions.mjs +8 -1
  21. package/web-ui/modules/app.methods.session-browser.mjs +28 -4
  22. package/web-ui/modules/app.methods.session-trash.mjs +4 -2
  23. package/web-ui/modules/i18n.dict.mjs +24 -8
  24. package/web-ui/partials/index/layout-header.html +12 -2
  25. package/web-ui/partials/index/panel-sessions.html +4 -2
  26. package/web-ui/partials/index/panel-usage.html +7 -0
  27. package/web-ui/styles/controls-forms.css +49 -2
  28. package/web-ui/styles/layout-shell.css +1 -0
  29. package/web-ui/styles/responsive.css +0 -2
  30. package/web-ui/styles/sessions-list.css +2 -4
  31. package/web-ui/styles/sessions-toolbar-trash.css +4 -4
  32. package/web-ui/styles/sessions-usage.css +24 -0
  33. /package/{res → web-ui/res}/json5.min.js +0 -0
  34. /package/{res → web-ui/res}/logo-pack.webp +0 -0
  35. /package/{res → web-ui/res}/vue.global.prod.js +0 -0
package/README.md CHANGED
@@ -1,6 +1,6 @@
1
1
  <div align="center">
2
2
 
3
- <img src="res/logo.png" alt="Codex Mate logo" width="180" />
3
+ <img src="site/.vitepress/public/images/logo.png" alt="Codex Mate logo" width="180" />
4
4
 
5
5
  # Codex Mate
6
6
 
@@ -29,7 +29,7 @@ Codex Mate is a local-first CLI + Web UI for unified management of:
29
29
  - Claude Code `CLAUDE.md` editing (writes to `~/.claude/CLAUDE.md`)
30
30
  - OpenClaw JSON5 profiles and workspace `AGENTS.md`
31
31
  - Local skills market for Codex / Claude Code (target switching, local skills management, cross-app import, ZIP distribution)
32
- - Local Codex/Claude sessions (list/filter/export/delete) with Usage analytics overview
32
+ - Local Codex/Claude/Gemini CLI/CodeBuddy Code sessions (list/filter/export/delete) with Usage analytics overview
33
33
  - Plugins (Prompt templates): reusable templates with variables and one-click copy
34
34
  - Task orchestration: plan/queue/run/review local tasks
35
35
 
@@ -57,12 +57,18 @@ It works on local files directly and does not require cloud hosting. The skills
57
57
  - OpenClaw JSON5 profile management
58
58
 
59
59
  **Session Management**
60
- - Unified Codex + Claude session list
60
+ - Unified Codex + Claude + Gemini CLI + CodeBuddy Code session list
61
+ - Session locations (local-first, configurable):
62
+ - Codex: `~/.codex/sessions/*.jsonl` (or `$CODEX_HOME/sessions`, `$XDG_CONFIG_HOME/codex/sessions`)
63
+ - Claude: `~/.claude/projects/**/**/*.jsonl` (or `$CLAUDE_HOME/projects`, `$XDG_CONFIG_HOME/claude/projects`)
64
+ - Gemini: `~/.gemini/tmp/*/chats/*.json` (or `$GEMINI_HOME/tmp`, `$XDG_CONFIG_HOME/gemini/tmp`)
65
+ - CodeBuddy: `~/.codebuddy/projects/**/**/*.jsonl` (or `$CODEBUDDY_CODE_HOME_DIR/projects`)
61
66
  - Local session pinning with persistent pinned state and pinned-first ordering
62
- - Keyword/source/cwd filters
67
+ - Keyword/source/cwd/role/time filters, plus shareable filter links
68
+ - Copy resume command (Codex/Gemini/CodeBuddy): `codex resume <sessionId>` / `gemini -r <sessionId>` / `codebuddy -r <sessionId>`
63
69
  - Fast search UX: short-lived query result caching to avoid rescanning on each keystroke
64
70
  - Usage subview with 7d / 30d session trends, message trends, source share, and top paths
65
- - Markdown export
71
+ - Markdown export (Web UI + `codexmate export-session`, supports `--session-id` or `--file`)
66
72
  - Session-level and message-level delete (supports batch), with a local recycle bin for restore/purge
67
73
  - Large-session preview optimization (fast tail preview path)
68
74
 
@@ -78,10 +84,20 @@ It works on local files directly and does not require cloud hosting. The skills
78
84
 
79
85
  **Engineering Utilities**
80
86
  - MCP stdio domains (`tools`, `resources`, `prompts`)
87
+ - Automation hooks (`/hooks/*`) + outbound webhook notifiers
81
88
  - Built-in proxy controls (`proxy`)
82
89
  - Auth profile management (`auth`)
83
90
  - Zip/unzip utilities
84
91
 
92
+ ## Automation (signal → action)
93
+
94
+ When running `codexmate run`, you can accept external webhooks and convert them into queued tasks:
95
+
96
+ - Webhook entry: `POST /hooks/<source>` (currently `github`, `gitlab`)
97
+ - Rule config: `~/.codex/codexmate-automation.json`
98
+ - Supported action: `task.queue.add` (optionally `startQueue: true`)
99
+ - Notifications: `notifiers[]` supports `type: "webhook"` for Slack/Feishu-style incoming webhooks
100
+
85
101
  ## Architecture
86
102
 
87
103
  ### At a glance (what it does → what you get)
@@ -166,6 +182,12 @@ npm install -g @mmmbuto/codex-cli-termux@latest
166
182
 
167
183
  # Claude Code
168
184
  npm install -g @anthropic-ai/claude-code
185
+
186
+ # Gemini CLI
187
+ npm install -g @google/gemini-cli
188
+
189
+ # CodeBuddy Code
190
+ npm install -g @tencent-ai/codebuddy-code
169
191
  ```
170
192
 
171
193
  ### Run from source
@@ -213,7 +235,7 @@ npm run reset 79
213
235
  | `codexmate qwen [args...]` | Qwen CLI passthrough entrypoint |
214
236
  | `codexmate run [--host <HOST>] [--no-browser]` | Start Web UI |
215
237
  | `codexmate mcp serve [--read-only\|--allow-write]` | Start MCP stdio server |
216
- | `codexmate export-session --source <codex\|claude> ...` | Export session to Markdown |
238
+ | `codexmate export-session --source <codex\|claude\|gemini\|codebuddy> ...` | Export session to Markdown |
217
239
  | `codexmate zip <path> [--max:0-9]` / `codexmate unzip <zip> [out]` | Zip / unzip |
218
240
  | `codexmate unzip-ext <zip-dir> [out] [--ext:suffix[,suffix...]] [--no-recursive]` | Extract files with target suffixes from ZIP files in a directory (default `.json`, recursive by default) |
219
241
 
package/README.zh.md CHANGED
@@ -1,6 +1,6 @@
1
1
  <div align="center">
2
2
 
3
- <img src="res/logo.png" alt="Codex Mate logo" width="180" />
3
+ <img src="site/.vitepress/public/images/logo.png" alt="Codex Mate logo" width="180" />
4
4
 
5
5
  # Codex Mate
6
6
 
@@ -29,7 +29,7 @@ Codex Mate 提供一套本地优先的 CLI + Web UI,用于统一管理:
29
29
  - Claude Code `CLAUDE.md` 编辑(写入 `~/.claude/CLAUDE.md`)
30
30
  - OpenClaw JSON5 配置与 Workspace `AGENTS.md`
31
31
  - Codex / Claude Code Skills 市场(安装目标切换、本地 skills 管理、跨应用导入、ZIP 分发)
32
- - Codex / Claude 本地会话浏览、筛选、导出、删除与 Usage 统计概览
32
+ - Codex / Claude / Gemini CLI / CodeBuddy Code 本地会话浏览、筛选、导出、删除与 Usage 统计概览
33
33
  - 插件(提示词模板):模板复用、变量填写、一键复制
34
34
  - 任务编排:规划 / 排队 / 执行 / 回看
35
35
 
@@ -58,12 +58,18 @@ Codex Mate 提供一套本地优先的 CLI + Web UI,用于统一管理:
58
58
  - OpenClaw JSON5 配置方案管理
59
59
 
60
60
  **会话管理**
61
- - 同页查看 Codex 与 Claude 会话
61
+ - 同页查看 Codex、Claude、Gemini CLI CodeBuddy Code 会话
62
+ - 会话来源与默认路径(本地优先,可通过环境变量覆盖):
63
+ - Codex:`~/.codex/sessions/*.jsonl`(或 `$CODEX_HOME/sessions`、`$XDG_CONFIG_HOME/codex/sessions`)
64
+ - Claude:`~/.claude/projects/**/**/*.jsonl`(或 `$CLAUDE_HOME/projects`、`$XDG_CONFIG_HOME/claude/projects`)
65
+ - Gemini:`~/.gemini/tmp/*/chats/*.json`(或 `$GEMINI_HOME/tmp`、`$XDG_CONFIG_HOME/gemini/tmp`)
66
+ - CodeBuddy:`~/.codebuddy/projects/**/**/*.jsonl`(或 `$CODEBUDDY_CODE_HOME_DIR/projects`)
62
67
  - 支持本地会话置顶,置顶状态持久化保存并优先排序显示
63
- - 关键词搜索、来源筛选、cwd 路径筛选
68
+ - 关键词搜索、来源筛选、cwd/角色/时间筛选,并支持复制筛选链接
69
+ - 复制恢复命令(Codex/Gemini/CodeBuddy):`codex resume <sessionId>` / `gemini -r <sessionId>` / `codebuddy -r <sessionId>`
64
70
  - 搜索体验优化:短周期结果缓存,避免输入时重复扫描
65
71
  - Usage 子页:近 7 天 / 近 30 天会话趋势、消息趋势、来源占比、高频路径
66
- - 会话导出 Markdown
72
+ - 会话导出 Markdown(Web UI + `codexmate export-session`,支持 `--session-id` 或 `--file`)
67
73
  - 会话与消息级删除(支持批量),并提供本地回收站用于恢复/彻底删除
68
74
  - 大会话预览优化(快速 tail 预览路径)
69
75
 
@@ -83,8 +89,18 @@ Codex Mate 提供一套本地优先的 CLI + Web UI,用于统一管理:
83
89
 
84
90
  **工程能力**
85
91
  - MCP stdio 能力(tools/resources/prompts)
92
+ - 自动化钩子(`/hooks/*`)+ 外发 webhook 通知
86
93
  - Zip 压缩/解压(优先系统工具,失败回退 JS 库)
87
94
 
95
+ ## 自动化(信号 → 行动)
96
+
97
+ 运行 `codexmate run` 后,可接收外部 webhook 并转为任务队列:
98
+
99
+ - 入口:`POST /hooks/<source>`(当前支持 `github`、`gitlab`)
100
+ - 规则:`~/.codex/codexmate-automation.json`
101
+ - 动作:`task.queue.add`(可选 `startQueue: true`)
102
+ - 通知:`notifiers[]` 支持 `type: "webhook"`(适配 Slack/飞书等入站 webhook)
103
+
88
104
  ## 架构总览
89
105
 
90
106
  ### 一图看懂(从“做什么”到“产生什么效果”)
@@ -156,7 +172,7 @@ codexmate run
156
172
 
157
173
  > 安全提示:默认监听会在当前局域网暴露未鉴权的管理界面。若包含 API Key、provider 配置或 skills 管理,请仅在可信网络中使用;如需仅本机访问,可设置 `CODEXMATE_HOST=127.0.0.1` 或启动时传入 `--host 127.0.0.1`。
158
174
 
159
- ### 安装 Codex CLI / Claude Code(可选)
175
+ ### 安装 Codex CLI / Claude Code / Gemini CLI / CodeBuddy Code(可选)
160
176
 
161
177
  Codex Mate 支持透传调用官方 CLI(例如 `codexmate codex ...`),建议先安装:
162
178
 
@@ -169,6 +185,12 @@ npm install -g @mmmbuto/codex-cli-termux@latest
169
185
 
170
186
  # Claude Code
171
187
  npm install -g @anthropic-ai/claude-code
188
+
189
+ # Gemini CLI
190
+ npm install -g @google/gemini-cli
191
+
192
+ # CodeBuddy Code
193
+ npm install -g @tencent-ai/codebuddy-code
172
194
  ```
173
195
 
174
196
  ### 从源码运行
@@ -215,7 +237,7 @@ npm run reset 79
215
237
  | `codexmate qwen [args...]` | Qwen CLI 透传入口 |
216
238
  | `codexmate run [--host <HOST>] [--no-browser]` | 启动 Web UI |
217
239
  | `codexmate mcp serve [--read-only\|--allow-write]` | 启动 MCP stdio 服务 |
218
- | `codexmate export-session --source <codex\|claude> ...` | 导出会话为 Markdown |
240
+ | `codexmate export-session --source <codex\|claude\|gemini\|codebuddy> ...` | 导出会话为 Markdown |
219
241
  | `codexmate zip <path> [--max:0-9]` / `codexmate unzip <zip> [out]` | 压缩 / 解压 |
220
242
  | `codexmate unzip-ext <zip-dir> [out] [--ext:suffix[,suffix...]] [--no-recursive]` | 批量提取目录下 ZIP 内指定后缀文件(默认 `.json`,默认递归) |
221
243
 
@@ -674,6 +674,41 @@ function createBuiltinProxyRuntimeController(deps = {}) {
674
674
  return;
675
675
  }
676
676
 
677
+ const remoteAddr = req && req.socket ? req.socket.remoteAddress : '';
678
+ const isLoopback = !remoteAddr
679
+ || remoteAddr === '127.0.0.1'
680
+ || remoteAddr === '::1'
681
+ || remoteAddr === '::ffff:127.0.0.1';
682
+ if (!isLoopback) {
683
+ const expected = typeof process.env.CODEXMATE_HTTP_TOKEN === 'string'
684
+ ? process.env.CODEXMATE_HTTP_TOKEN.trim()
685
+ : '';
686
+ if (!expected) {
687
+ const body = JSON.stringify({ error: 'Remote access is disabled (set CODEXMATE_HTTP_TOKEN)' });
688
+ res.writeHead(403, {
689
+ 'Content-Type': 'application/json; charset=utf-8',
690
+ 'Content-Length': Buffer.byteLength(body, 'utf-8')
691
+ });
692
+ res.end(body, 'utf-8');
693
+ return;
694
+ }
695
+ const headers = req && req.headers && typeof req.headers === 'object' ? req.headers : {};
696
+ const rawAuth = typeof headers.authorization === 'string' ? headers.authorization.trim() : '';
697
+ const match = rawAuth ? rawAuth.match(/^bearer\s+(.+)$/i) : null;
698
+ const actual = match && match[1]
699
+ ? match[1].trim()
700
+ : (rawAuth ? rawAuth : (typeof headers['x-codexmate-token'] === 'string' ? String(headers['x-codexmate-token']).trim() : ''));
701
+ if (!actual || actual !== expected) {
702
+ const body = JSON.stringify({ error: 'Unauthorized' });
703
+ res.writeHead(401, {
704
+ 'Content-Type': 'application/json; charset=utf-8',
705
+ 'Content-Length': Buffer.byteLength(body, 'utf-8')
706
+ });
707
+ res.end(body, 'utf-8');
708
+ return;
709
+ }
710
+ }
711
+
677
712
  const incomingPath = parsedIncoming.pathname || '/';
678
713
  if (incomingPath === '/health' || incomingPath === '/status') {
679
714
  const body = JSON.stringify({
@@ -864,6 +864,30 @@ function createBuiltinClaudeProxyRuntimeController(deps = {}) {
864
864
  function createBuiltinClaudeProxyServer(settings, upstream) {
865
865
  const connections = new Set();
866
866
  const server = http.createServer((req, res) => {
867
+ const remoteAddr = req && req.socket ? req.socket.remoteAddress : '';
868
+ const isLoopback = !remoteAddr
869
+ || remoteAddr === '127.0.0.1'
870
+ || remoteAddr === '::1'
871
+ || remoteAddr === '::ffff:127.0.0.1';
872
+ if (!isLoopback) {
873
+ const expected = typeof process.env.CODEXMATE_HTTP_TOKEN === 'string'
874
+ ? process.env.CODEXMATE_HTTP_TOKEN.trim()
875
+ : '';
876
+ if (!expected) {
877
+ writeAnthropicProxyError(res, 403, 'Remote access is disabled (set CODEXMATE_HTTP_TOKEN)', 'authentication_error');
878
+ return;
879
+ }
880
+ const headers = req && req.headers && typeof req.headers === 'object' ? req.headers : {};
881
+ const rawAuth = typeof headers.authorization === 'string' ? headers.authorization.trim() : '';
882
+ const match = rawAuth ? rawAuth.match(/^bearer\s+(.+)$/i) : null;
883
+ const actual = match && match[1]
884
+ ? match[1].trim()
885
+ : (rawAuth ? rawAuth : (typeof headers['x-codexmate-token'] === 'string' ? String(headers['x-codexmate-token']).trim() : ''));
886
+ if (!actual || actual !== expected) {
887
+ writeAnthropicProxyError(res, 401, 'Unauthorized', 'authentication_error');
888
+ return;
889
+ }
890
+ }
867
891
  handleBuiltinClaudeProxyRequest(req, res, settings, upstream).catch((err) => {
868
892
  if (res.headersSent) {
869
893
  try { res.destroy(err); } catch (_) {}
@@ -97,6 +97,17 @@ function extractHttpStatusFromError(err) {
97
97
  return Number.isFinite(value) ? value : 0;
98
98
  }
99
99
 
100
+ function isAllowedSkillsRedirectHost(originHost, nextHost) {
101
+ const origin = typeof originHost === 'string' ? originHost.trim().toLowerCase() : '';
102
+ const next = typeof nextHost === 'string' ? nextHost.trim().toLowerCase() : '';
103
+ if (!origin || !next) return false;
104
+ if (origin === next) return true;
105
+ if (process.env.CODEXMATE_ALLOW_SKILLS_REDIRECT === '1') return true;
106
+ if (origin === 'github.com' && next === 'codeload.github.com') return true;
107
+ if (origin === 'github.com' && next.endsWith('.githubusercontent.com')) return true;
108
+ return false;
109
+ }
110
+
100
111
  function downloadUrlToFile(targetUrl, filePath, options = {}) {
101
112
  const maxBytes = Number.isFinite(options.maxBytes) && options.maxBytes > 0
102
113
  ? Math.floor(options.maxBytes)
@@ -141,8 +152,19 @@ function downloadUrlToFile(targetUrl, filePath, options = {}) {
141
152
  const nextUrl = redirectLocation.startsWith('http')
142
153
  ? redirectLocation
143
154
  : `${parsed.origin}${redirectLocation}`;
155
+ let originHost = typeof options.originHost === 'string' && options.originHost.trim()
156
+ ? options.originHost.trim()
157
+ : parsed.host;
158
+ try {
159
+ const nextParsed = new URL(nextUrl);
160
+ if (!isAllowedSkillsRedirectHost(originHost, nextParsed.host)) {
161
+ res.resume();
162
+ reject(new Error('Cross-origin redirect is not allowed'));
163
+ return;
164
+ }
165
+ } catch (_) {}
144
166
  res.resume();
145
- downloadUrlToFile(nextUrl, filePath, { maxBytes, timeoutMs, maxRedirects: maxRedirects - 1 })
167
+ downloadUrlToFile(nextUrl, filePath, { maxBytes, timeoutMs, maxRedirects: maxRedirects - 1, originHost })
146
168
  .then(resolve)
147
169
  .catch(reject);
148
170
  return;
@@ -238,6 +238,10 @@ function normalizeResponsesInputToChatMessages(input) {
238
238
  out.push({ type: 'text', text: block.text });
239
239
  continue;
240
240
  }
241
+ if ((type === 'reasoning' || type === 'reasoning_text' || type === 'reasoning_content') && typeof block.text === 'string') {
242
+ out.push({ type: 'text', text: block.text });
243
+ continue;
244
+ }
241
245
  if (type === 'input_image') {
242
246
  const raw = block.image_url != null ? block.image_url : block.imageUrl;
243
247
  const url = typeof raw === 'string'
@@ -255,7 +259,21 @@ function normalizeResponsesInputToChatMessages(input) {
255
259
  }
256
260
  if (type === 'image_url' && block.image_url) {
257
261
  out.push({ type: 'image_url', image_url: block.image_url });
262
+ continue;
263
+ }
264
+ const text = typeof block.text === 'string'
265
+ ? block.text
266
+ : (typeof block.content === 'string' ? block.content : '');
267
+ if (text) {
268
+ out.push({ type: 'text', text });
269
+ continue;
258
270
  }
271
+ try {
272
+ const raw = JSON.stringify(block);
273
+ if (raw) {
274
+ out.push({ type: 'text', text: raw.slice(0, 4000) });
275
+ }
276
+ } catch (_) {}
259
277
  }
260
278
  if (out.length === 0) return '';
261
279
  return out;
@@ -635,6 +653,9 @@ async function proxyRequestJson(targetUrl, options = {}) {
635
653
  const parsed = new URL(targetUrl);
636
654
  const transport = parsed.protocol === 'https:' ? https : http;
637
655
  const bodyText = options.body ? JSON.stringify(options.body) : '';
656
+ const maxBytes = Number.isFinite(options.maxBytes) && options.maxBytes > 0
657
+ ? Math.floor(options.maxBytes)
658
+ : 0;
638
659
  const headers = {
639
660
  'Accept': 'application/json',
640
661
  ...(options.body ? { 'Content-Type': 'application/json' } : {}),
@@ -664,7 +685,21 @@ async function proxyRequestJson(targetUrl, options = {}) {
664
685
  agent: parsed.protocol === 'https:' ? options.httpsAgent : options.httpAgent
665
686
  }, (upstreamRes) => {
666
687
  const chunks = [];
667
- upstreamRes.on('data', (chunk) => chunk && chunks.push(chunk));
688
+ let size = 0;
689
+ upstreamRes.on('data', (chunk) => {
690
+ if (!chunk) return;
691
+ if (maxBytes > 0) {
692
+ size += chunk.length;
693
+ if (size > maxBytes) {
694
+ chunks.length = 0;
695
+ try { upstreamRes.destroy(new Error('response too large')); } catch (_) {}
696
+ try { req.destroy(new Error('response too large')); } catch (_) {}
697
+ finish({ ok: false, error: 'response too large' });
698
+ return;
699
+ }
700
+ }
701
+ chunks.push(chunk);
702
+ });
668
703
  upstreamRes.on('end', () => {
669
704
  const text = chunks.length ? Buffer.concat(chunks).toString('utf-8') : '';
670
705
  finish({
@@ -689,12 +724,16 @@ async function proxyRequestJson(targetUrl, options = {}) {
689
724
 
690
725
  function createOpenaiBridgeHttpHandler(options = {}) {
691
726
  const settingsFile = options.settingsFile;
692
- const expectedToken = typeof options.expectedToken === 'string' && options.expectedToken.trim()
693
- ? options.expectedToken.trim()
694
- : DEFAULT_BRIDGE_TOKEN;
727
+ const expectedTokenRaw = typeof options.expectedToken === 'string' ? options.expectedToken.trim() : '';
728
+ const expectedToken = Object.prototype.hasOwnProperty.call(options, 'expectedToken')
729
+ ? expectedTokenRaw
730
+ : (expectedTokenRaw || DEFAULT_BRIDGE_TOKEN);
695
731
  const maxBodySize = Number.isFinite(options.maxBodySize) ? options.maxBodySize : 0;
696
732
  const httpAgent = options.httpAgent;
697
733
  const httpsAgent = options.httpsAgent;
734
+ const maxUpstreamBytes = Number.isFinite(options.maxUpstreamBytes) && options.maxUpstreamBytes > 0
735
+ ? Math.floor(options.maxUpstreamBytes)
736
+ : Math.max(16 * 1024 * 1024, maxBodySize > 0 ? maxBodySize * 4 : 0);
698
737
 
699
738
  if (!settingsFile) {
700
739
  throw new Error('createOpenaiBridgeHttpHandler 缺少 settingsFile');
@@ -730,6 +769,11 @@ function createOpenaiBridgeHttpHandler(options = {}) {
730
769
  // 为避免在 LAN 暴露无鉴权的代理,这里仅允许 loopback 连接缺省 token。
731
770
  const remoteAddr = req && req.socket ? req.socket.remoteAddress : '';
732
771
  const isLoopback = isLoopbackAddress(remoteAddr);
772
+ if (!isLoopback && !expectedToken) {
773
+ res.writeHead(403, { 'Content-Type': 'application/json; charset=utf-8' });
774
+ res.end(JSON.stringify({ error: 'Remote access is disabled (set CODEXMATE_HTTP_TOKEN)' }));
775
+ return;
776
+ }
733
777
  if (!token && !isLoopback) {
734
778
  res.writeHead(401, { 'Content-Type': 'application/json; charset=utf-8' });
735
779
  res.end(JSON.stringify({ error: 'Unauthorized' }));
@@ -774,6 +818,7 @@ function createOpenaiBridgeHttpHandler(options = {}) {
774
818
  ...(authHeader ? { Authorization: authHeader } : {}),
775
819
  ...upstreamHeaders
776
820
  },
821
+ maxBytes: maxUpstreamBytes,
777
822
  httpAgent,
778
823
  httpsAgent
779
824
  });
@@ -827,6 +872,7 @@ function createOpenaiBridgeHttpHandler(options = {}) {
827
872
  ...(authHeader ? { Authorization: authHeader } : {}),
828
873
  ...upstreamHeaders
829
874
  },
875
+ maxBytes: maxUpstreamBytes,
830
876
  httpAgent,
831
877
  httpsAgent
832
878
  });
@@ -887,6 +933,7 @@ function createOpenaiBridgeHttpHandler(options = {}) {
887
933
  ...(authHeader ? { Authorization: authHeader } : {}),
888
934
  ...upstreamHeaders
889
935
  },
936
+ maxBytes: maxUpstreamBytes,
890
937
  httpAgent,
891
938
  httpsAgent
892
939
  });
@@ -7,11 +7,13 @@ async function listSessionUsageCore(params = {}, deps = {}) {
7
7
  listSessionBrowse,
8
8
  parseCodexSessionSummary,
9
9
  parseClaudeSessionSummary,
10
+ parseCodeBuddySessionSummary,
11
+ parseGeminiSessionSummary,
10
12
  MAX_SESSION_USAGE_LIST_SIZE,
11
13
  SESSION_BROWSE_SUMMARY_READ_BYTES
12
14
  } = deps;
13
15
 
14
- const source = params.source === 'codex' || params.source === 'claude'
16
+ const source = params.source === 'codex' || params.source === 'claude' || params.source === 'gemini' || params.source === 'codebuddy'
15
17
  ? params.source
16
18
  : 'all';
17
19
  const rawLimit = Number(params.limit);
@@ -81,7 +83,11 @@ async function listSessionUsageCore(params = {}, deps = {}) {
81
83
  try {
82
84
  summary = normalized.source === 'claude'
83
85
  ? parseClaudeSessionSummary(filePath, summaryOptions)
84
- : parseCodexSessionSummary(filePath, summaryOptions);
86
+ : (normalized.source === 'gemini'
87
+ ? parseGeminiSessionSummary(filePath, summaryOptions)
88
+ : (normalized.source === 'codebuddy'
89
+ ? parseCodeBuddySessionSummary(filePath, summaryOptions)
90
+ : parseCodexSessionSummary(filePath, summaryOptions)));
85
91
  } catch (_) {
86
92
  summary = null;
87
93
  }