codexmate 0.0.30 → 0.0.31
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 +18 -76
- package/README.zh.md +18 -1
- package/cli/session-convert-args.js +5 -1
- package/cli/session-convert.js +111 -4
- package/cli.js +329 -32
- package/package.json +1 -1
- package/web-ui/app.js +1 -0
- package/web-ui/modules/app.methods.codex-config.mjs +0 -76
- package/web-ui/modules/app.methods.session-actions.mjs +16 -18
- package/web-ui/modules/i18n.dict.mjs +30 -2
- package/web-ui/partials/index/panel-config-codex.html +31 -11
- package/web-ui/partials/index/panel-config-codex.html.bak +337 -0
- package/web-ui/partials/index/panel-sessions.html +4 -10
- package/web-ui/session-helpers.mjs +0 -3
- package/web-ui/styles/bridge-pool.css +197 -0
- package/web-ui/styles/responsive.css +3 -3
- package/web-ui/styles/sessions-list.css +2 -2
- package/web-ui/styles/sessions-toolbar-trash.css +14 -0
- package/web-ui/styles.css +1 -0
package/README.md
CHANGED
|
@@ -6,9 +6,11 @@
|
|
|
6
6
|
|
|
7
7
|
**One dashboard for all your local AI coding tools — switch providers, manage sessions, edit configs, and orchestrate tasks across Codex, Claude Code, and OpenClaw. Built-in OpenAI-compatible bridge, usage analytics, and prompt templates. Zero cloud, zero setup.**
|
|
8
8
|
|
|
9
|
-
[](https://github.com/SakuraByteCore/codexmate/actions/workflows/release.yml)
|
|
10
9
|
[](https://www.npmjs.com/package/codexmate)
|
|
10
|
+
[](https://github.com/SakuraByteCore/codexmate/actions/workflows/release.yml)
|
|
11
11
|
[](https://www.npmjs.com/package/codexmate)
|
|
12
|
+
[](#install-via-curl-standalone)
|
|
13
|
+
[](#quick-start)
|
|
12
14
|
[](https://nodejs.org/)
|
|
13
15
|
[](LICENSE)
|
|
14
16
|
[](https://github.com/SakuraByteCore/codexmate/stargazers)
|
|
@@ -175,6 +177,21 @@ Default listen address is `0.0.0.0:3737` for LAN access, and browser auto-open i
|
|
|
175
177
|
|
|
176
178
|
> Safety note: the unauthenticated management UI is exposed to your current LAN by default. Use trusted networks only; for local-only access, set `CODEXMATE_HOST=127.0.0.1` or pass `--host 127.0.0.1`.
|
|
177
179
|
|
|
180
|
+
### Install via curl (standalone)
|
|
181
|
+
|
|
182
|
+
No npm required. Downloads a self-contained tarball with `node_modules` bundled:
|
|
183
|
+
|
|
184
|
+
```bash
|
|
185
|
+
curl -fsSL https://raw.githubusercontent.com/SakuraByteCore/codexmate/main/scripts/install.sh | bash
|
|
186
|
+
```
|
|
187
|
+
|
|
188
|
+
Installs to `~/.codexmate`, symlinks to `~/.local/bin/codexmate`, and auto-adds PATH.
|
|
189
|
+
|
|
190
|
+
| Variable | Default | Description |
|
|
191
|
+
| --- | --- | --- |
|
|
192
|
+
| `CODEXMATE_INSTALL_DIR` | `~/.codexmate` | Installation directory |
|
|
193
|
+
| `CODEXMATE_BIN_DIR` | `~/.local/bin` | Symlink directory |
|
|
194
|
+
|
|
178
195
|
### Install Codex CLI / Claude Code CLI (optional)
|
|
179
196
|
|
|
180
197
|
Codex Mate can pass through to the official CLIs (e.g. `codexmate codex ...`). Install them first:
|
|
@@ -344,78 +361,3 @@ Issues and pull requests are accepted.
|
|
|
344
361
|
## License
|
|
345
362
|
|
|
346
363
|
Apache-2.0
|
|
347
|
-
|
|
348
|
-
### Claude Code Mode
|
|
349
|
-
- Multi-profile management
|
|
350
|
-
- Default write to `~/.claude/settings.json`
|
|
351
|
-
- `~/.claude/CLAUDE.md` editing
|
|
352
|
-
- Shareable import command copy
|
|
353
|
-
|
|
354
|
-
### OpenClaw Mode
|
|
355
|
-
- JSON5 multi-profile management
|
|
356
|
-
- Apply to `~/.openclaw/openclaw.json`
|
|
357
|
-
- Manage `~/.openclaw/workspace/AGENTS.md`
|
|
358
|
-
|
|
359
|
-
### Sessions Mode
|
|
360
|
-
- Unified Codex + Claude sessions
|
|
361
|
-
- Browser / Usage subview switching
|
|
362
|
-
- Local pin/unpin with persistent storage and pinned-first ordering
|
|
363
|
-
- Search, filter, export, delete, batch cleanup
|
|
364
|
-
- Usage view includes 7d / 30d session trends, message trends, source share, and top paths
|
|
365
|
-
|
|
366
|
-
### Skills Market Tab
|
|
367
|
-
- Switch the skills install target between `Codex` and `Claude Code`
|
|
368
|
-
- Show the current local skills root, installed items, and importable items
|
|
369
|
-
- Scan importable sources under `Codex` / `Claude Code` / `Agents`
|
|
370
|
-
- Support cross-app import, ZIP import/export, and batch delete
|
|
371
|
-
|
|
372
|
-
## MCP
|
|
373
|
-
|
|
374
|
-
> Transport: `stdio`
|
|
375
|
-
|
|
376
|
-
- Default: read-only tools
|
|
377
|
-
- Enable writes: `--allow-write` or `CODEXMATE_MCP_ALLOW_WRITE=1`
|
|
378
|
-
- Domains: `tools`, `resources`, `prompts`
|
|
379
|
-
|
|
380
|
-
Examples:
|
|
381
|
-
|
|
382
|
-
```bash
|
|
383
|
-
codexmate mcp serve --read-only
|
|
384
|
-
codexmate mcp serve --allow-write
|
|
385
|
-
```
|
|
386
|
-
|
|
387
|
-
## Config Files
|
|
388
|
-
|
|
389
|
-
- `~/.codex/config.toml`
|
|
390
|
-
- `~/.codex/auth.json`
|
|
391
|
-
- `~/.codex/models.json`
|
|
392
|
-
- `~/.codex/provider-current-models.json`
|
|
393
|
-
- `~/.claude/settings.json`
|
|
394
|
-
- `~/.claude/CLAUDE.md`
|
|
395
|
-
- `~/.openclaw/openclaw.json`
|
|
396
|
-
- `~/.openclaw/workspace/AGENTS.md`
|
|
397
|
-
|
|
398
|
-
## Environment Variables
|
|
399
|
-
|
|
400
|
-
| Variable | Default | Description |
|
|
401
|
-
| --- | --- | --- |
|
|
402
|
-
| `CODEXMATE_PORT` | `3737` | Web server port |
|
|
403
|
-
| `CODEXMATE_HOST` | `0.0.0.0` | Web listen host (set `127.0.0.1` for local-only access) |
|
|
404
|
-
| `CODEXMATE_NO_BROWSER` | unset | Set `1` to disable browser auto-open |
|
|
405
|
-
| `CODEXMATE_MCP_ALLOW_WRITE` | unset | Set `1` to allow MCP write tools by default |
|
|
406
|
-
| `CODEXMATE_FORCE_RESET_EXISTING_CONFIG` | `0` | Set `1` to force bootstrap reset of existing config |
|
|
407
|
-
|
|
408
|
-
## Tech Stack
|
|
409
|
-
|
|
410
|
-
- Node.js
|
|
411
|
-
- Vue.js 3 (Web UI)
|
|
412
|
-
- Native HTTP server
|
|
413
|
-
- `@iarna/toml`, `json5`
|
|
414
|
-
|
|
415
|
-
## Contributing
|
|
416
|
-
|
|
417
|
-
Issues and pull requests are accepted.
|
|
418
|
-
|
|
419
|
-
## License
|
|
420
|
-
|
|
421
|
-
Apache-2.0
|
package/README.zh.md
CHANGED
|
@@ -6,9 +6,11 @@
|
|
|
6
6
|
|
|
7
7
|
**一个面板管好所有本地 AI 编码工具 — 跨 Codex / Claude Code / OpenClaw 切 provider、管会话、改配置、编排任务。内置 OpenAI 兼容桥接、Usage 统计与提示词模板。纯本地,零上云。**
|
|
8
8
|
|
|
9
|
-
[](https://github.com/SakuraByteCore/codexmate/actions/workflows/release.yml)
|
|
10
9
|
[](https://www.npmjs.com/package/codexmate)
|
|
10
|
+
[](https://github.com/SakuraByteCore/codexmate/actions/workflows/release.yml)
|
|
11
11
|
[](https://www.npmjs.com/package/codexmate)
|
|
12
|
+
[](#curl-一键安装独立包无需-npm)
|
|
13
|
+
[](#快速开始)
|
|
12
14
|
[](https://nodejs.org/)
|
|
13
15
|
[](LICENSE)
|
|
14
16
|
[](https://github.com/SakuraByteCore/codexmate/stargazers)
|
|
@@ -178,6 +180,21 @@ codexmate run
|
|
|
178
180
|
|
|
179
181
|
> 安全提示:默认监听会在当前局域网暴露未鉴权的管理界面。若包含 API Key、provider 配置或 skills 管理,请仅在可信网络中使用;如需仅本机访问,可设置 `CODEXMATE_HOST=127.0.0.1` 或启动时传入 `--host 127.0.0.1`。
|
|
180
182
|
|
|
183
|
+
### curl 一键安装(独立包,无需 npm)
|
|
184
|
+
|
|
185
|
+
下载包含 `node_modules` 的自包含安装包,不依赖 npm:
|
|
186
|
+
|
|
187
|
+
```bash
|
|
188
|
+
curl -fsSL https://raw.githubusercontent.com/SakuraByteCore/codexmate/main/scripts/install.sh | bash
|
|
189
|
+
```
|
|
190
|
+
|
|
191
|
+
安装到 `~/.codexmate`,自动软链接到 `~/.local/bin/codexmate`,并添加 PATH。
|
|
192
|
+
|
|
193
|
+
| 变量 | 默认值 | 说明 |
|
|
194
|
+
| --- | --- | --- |
|
|
195
|
+
| `CODEXMATE_INSTALL_DIR` | `~/.codexmate` | 安装目录 |
|
|
196
|
+
| `CODEXMATE_BIN_DIR` | `~/.local/bin` | 软链接目录 |
|
|
197
|
+
|
|
181
198
|
### 安装 Codex CLI / Claude Code / Gemini CLI / CodeBuddy Code(可选)
|
|
182
199
|
|
|
183
200
|
Codex Mate 支持透传调用官方 CLI(例如 `codexmate codex ...`),建议先安装:
|
|
@@ -25,7 +25,7 @@ function resolveOutputPath(outputPath, defaultFileName) {
|
|
|
25
25
|
}
|
|
26
26
|
|
|
27
27
|
function parseArgs(args = []) {
|
|
28
|
-
const options = { from: '', to: '', sessionId: '', filePath: '', output: '', maxMessages: undefined };
|
|
28
|
+
const options = { from: '', to: '', sessionId: '', filePath: '', output: '', outputDir: 'native', maxMessages: undefined };
|
|
29
29
|
const errors = [];
|
|
30
30
|
for (let i = 0; i < args.length; i += 1) {
|
|
31
31
|
const arg = String(args[i] || '');
|
|
@@ -41,6 +41,8 @@ function parseArgs(args = []) {
|
|
|
41
41
|
if (arg.startsWith('--file=')) { options.filePath = arg.slice(7); continue; }
|
|
42
42
|
if (arg === '--output') { options.output = next; i += 1; continue; }
|
|
43
43
|
if (arg.startsWith('--output=')) { options.output = arg.slice(9); continue; }
|
|
44
|
+
if (arg === '--output-dir') { options.outputDir = next; i += 1; continue; }
|
|
45
|
+
if (arg.startsWith('--output-dir=')) { options.outputDir = arg.slice(13); continue; }
|
|
44
46
|
if (arg === '--max-messages') { options.maxMessages = next; i += 1; continue; }
|
|
45
47
|
if (arg.startsWith('--max-messages=')) { options.maxMessages = arg.slice(15); continue; }
|
|
46
48
|
errors.push(`未知参数: ${arg}`);
|
|
@@ -50,6 +52,8 @@ function parseArgs(args = []) {
|
|
|
50
52
|
if (options.from !== 'codex' && options.from !== 'claude') errors.push('参数 --from 仅支持 codex 或 claude');
|
|
51
53
|
if (options.to !== 'codex' && options.to !== 'claude') errors.push('参数 --to 仅支持 codex 或 claude');
|
|
52
54
|
if (options.from && options.to && options.from === options.to) errors.push('--from 与 --to 不能相同');
|
|
55
|
+
options.outputDir = String(options.outputDir || 'native').trim().toLowerCase();
|
|
56
|
+
if (options.outputDir !== 'native' && options.outputDir !== 'derived') errors.push('参数 --output-dir 仅支持 native 或 derived');
|
|
53
57
|
if (!options.from) errors.push('缺少 --from');
|
|
54
58
|
if (!options.to) errors.push('缺少 --to');
|
|
55
59
|
if (!options.sessionId && !options.filePath) errors.push('必须指定 --session-id 或 --file');
|
package/cli/session-convert.js
CHANGED
|
@@ -6,7 +6,80 @@ const { readSessionMessages, buildTargetRecords } = require('./session-convert-i
|
|
|
6
6
|
|
|
7
7
|
function printUsage() {
|
|
8
8
|
console.log('\n用法:');
|
|
9
|
-
console.log(' codexmate convert-session --from <codex|claude> --to <codex|claude> (--session-id <ID>|--file <PATH>) [--output <PATH>] [--max-messages <N|all|Infinity>]');
|
|
9
|
+
console.log(' codexmate convert-session --from <codex|claude> --to <codex|claude> (--session-id <ID>|--file <PATH>) [--output <PATH>] [--output-dir <native|derived>] [--max-messages <N|all|Infinity>]');
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
function resolveExistingDir(candidates, fallback) {
|
|
14
|
+
for (const candidate of candidates) {
|
|
15
|
+
if (!candidate) continue;
|
|
16
|
+
try {
|
|
17
|
+
if (fs.existsSync(candidate) && fs.statSync(candidate).isDirectory()) return candidate;
|
|
18
|
+
} catch (_) {}
|
|
19
|
+
}
|
|
20
|
+
return fallback;
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
function resolveCodexSessionsDir() {
|
|
24
|
+
const home = process.env.HOME || process.env.USERPROFILE || '';
|
|
25
|
+
const candidates = [];
|
|
26
|
+
if (process.env.CODEX_HOME) candidates.push(path.join(process.env.CODEX_HOME, 'sessions'));
|
|
27
|
+
if (process.env.XDG_CONFIG_HOME) candidates.push(path.join(process.env.XDG_CONFIG_HOME, 'codex', 'sessions'));
|
|
28
|
+
if (home) {
|
|
29
|
+
candidates.push(path.join(home, '.config', 'codex', 'sessions'));
|
|
30
|
+
candidates.push(path.join(home, '.codex', 'sessions'));
|
|
31
|
+
}
|
|
32
|
+
return resolveExistingDir(candidates, candidates[candidates.length - 1] || path.resolve('.codex/sessions'));
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
function resolveClaudeProjectsDir() {
|
|
36
|
+
const home = process.env.HOME || process.env.USERPROFILE || '';
|
|
37
|
+
const candidates = [];
|
|
38
|
+
const claudeHome = process.env.CLAUDE_HOME || process.env.CLAUDE_CONFIG_DIR || '';
|
|
39
|
+
if (claudeHome) candidates.push(path.join(claudeHome, 'projects'));
|
|
40
|
+
if (process.env.XDG_CONFIG_HOME) candidates.push(path.join(process.env.XDG_CONFIG_HOME, 'claude', 'projects'));
|
|
41
|
+
if (home) {
|
|
42
|
+
candidates.push(path.join(home, '.config', 'claude', 'projects'));
|
|
43
|
+
candidates.push(path.join(home, '.claude', 'projects'));
|
|
44
|
+
}
|
|
45
|
+
return resolveExistingDir(candidates, candidates[candidates.length - 1] || path.resolve('.claude/projects'));
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
function sanitizeClaudeProjectName(cwd) {
|
|
49
|
+
const value = typeof cwd === 'string' && cwd.trim() ? path.resolve(cwd.trim()) : 'codexmate-derived';
|
|
50
|
+
return value.replace(/[^a-zA-Z0-9._-]/g, '-').replace(/-+/g, '-').replace(/^-|-$/g, '') || 'codexmate-derived';
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
function buildSourceKey(from, sessionId, filePath) {
|
|
54
|
+
const seed = `${from}|${sessionId || ''}|${filePath || ''}`;
|
|
55
|
+
let hash = 0;
|
|
56
|
+
for (let i = 0; i < seed.length; i += 1) hash = ((hash << 5) - hash + seed.charCodeAt(i)) | 0;
|
|
57
|
+
return String(Math.abs(hash)).padStart(8, '0').slice(0, 8);
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
function resolveDefaultOutputPath(opt, sessionId, cwd) {
|
|
61
|
+
const safeSessionId = String(sessionId).replace(/[^a-zA-Z0-9_-]/g, '_') || 'session';
|
|
62
|
+
if (opt.output) return resolveOutputPath(opt.output, `${opt.to}-session-${safeSessionId}.jsonl`);
|
|
63
|
+
if (opt.outputDir === 'derived') {
|
|
64
|
+
const home = process.env.HOME || process.env.USERPROFILE || '';
|
|
65
|
+
const root = path.join(home || process.cwd(), '.codexmate', 'sessions', 'derived', opt.to, opt.from, buildSourceKey(opt.from, sessionId, opt.filePath));
|
|
66
|
+
return path.join(root, `${safeSessionId}.jsonl`);
|
|
67
|
+
}
|
|
68
|
+
if (opt.to === 'codex') return path.join(resolveCodexSessionsDir(), `${safeSessionId}.jsonl`);
|
|
69
|
+
return path.join(resolveClaudeProjectsDir(), sanitizeClaudeProjectName(cwd), `${safeSessionId}.jsonl`);
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
function getDerivedSessionMetaPath(filePath) {
|
|
73
|
+
if (!filePath) return '';
|
|
74
|
+
return filePath.toLowerCase().endsWith('.jsonl')
|
|
75
|
+
? filePath.slice(0, -6) + '.meta.json'
|
|
76
|
+
: `${filePath}.meta.json`;
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
function writeJsonAtomic(filePath, value) {
|
|
80
|
+
const tmpPath = `${filePath}.tmp-${process.pid}-${Date.now()}`;
|
|
81
|
+
fs.writeFileSync(tmpPath, `${JSON.stringify(value, null, 2)}\n`, { encoding: 'utf-8', flag: 'wx' });
|
|
82
|
+
fs.renameSync(tmpPath, filePath);
|
|
10
83
|
}
|
|
11
84
|
|
|
12
85
|
async function cmdConvertSession(args = [], deps = {}) {
|
|
@@ -28,12 +101,46 @@ async function cmdConvertSession(args = [], deps = {}) {
|
|
|
28
101
|
}
|
|
29
102
|
const extracted = await readSessionMessages(filePath, opt.from, opt.maxMessages);
|
|
30
103
|
const sessionId = extracted.sessionId || opt.sessionId || path.basename(filePath, '.jsonl');
|
|
31
|
-
const safeSessionId = String(sessionId).replace(/[^a-zA-Z0-9_-]/g, '_');
|
|
32
104
|
const records = buildTargetRecords(opt.to, { sessionId, cwd: extracted.cwd || '', messages: extracted.messages });
|
|
33
105
|
const jsonl = `${records.map(r => JSON.stringify(r)).join('\n')}\n`;
|
|
34
|
-
const outputPath =
|
|
106
|
+
const outputPath = resolveDefaultOutputPath(opt, sessionId, extracted.cwd || '');
|
|
107
|
+
const metaPath = getDerivedSessionMetaPath(outputPath);
|
|
35
108
|
ensureDir(path.dirname(outputPath));
|
|
36
|
-
|
|
109
|
+
try {
|
|
110
|
+
if (fs.existsSync(metaPath)) {
|
|
111
|
+
const error = new Error(`target session metadata already exists: ${metaPath}`);
|
|
112
|
+
error.code = 'EEXIST';
|
|
113
|
+
throw error;
|
|
114
|
+
}
|
|
115
|
+
fs.writeFileSync(outputPath, jsonl, { encoding: 'utf-8', flag: 'wx' });
|
|
116
|
+
writeJsonAtomic(metaPath, {
|
|
117
|
+
version: 1,
|
|
118
|
+
createdAt: new Date().toISOString(),
|
|
119
|
+
source: {
|
|
120
|
+
type: opt.from,
|
|
121
|
+
sessionId,
|
|
122
|
+
filePath
|
|
123
|
+
},
|
|
124
|
+
target: {
|
|
125
|
+
type: opt.to,
|
|
126
|
+
sessionId,
|
|
127
|
+
filePath: outputPath
|
|
128
|
+
},
|
|
129
|
+
options: {
|
|
130
|
+
maxMessages: opt.maxMessages,
|
|
131
|
+
outputDir: opt.outputDir
|
|
132
|
+
}
|
|
133
|
+
});
|
|
134
|
+
} catch (error) {
|
|
135
|
+
if (error && error.code === 'EEXIST') {
|
|
136
|
+
console.error('转换失败: target session already exists:', outputPath);
|
|
137
|
+
process.exit(1);
|
|
138
|
+
}
|
|
139
|
+
try {
|
|
140
|
+
if (fs.existsSync(outputPath) && !fs.existsSync(metaPath)) fs.unlinkSync(outputPath);
|
|
141
|
+
} catch (_) {}
|
|
142
|
+
throw error;
|
|
143
|
+
}
|
|
37
144
|
console.log('\n✓ 会话已转换:', outputPath);
|
|
38
145
|
if (extracted.truncated) console.log('! 已截断: 可使用 --max-messages=all');
|
|
39
146
|
console.log();
|