Uh oh!
There was an error while loading. Please reload this page.
- Notifications
You must be signed in to change notification settings - Fork 35
feat(session): native derived sessions + claude proxy#136
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Uh oh!
There was an error while loading. Please reload this page.
Changes from all commits
59574e9092a8ca4088cbc717dae77c0fac1eff7140e1556da34878351e73f9aaa4148769d9f5278e7f31File filter
Filter by extension
Conversations
Uh oh!
There was an error while loading. Please reload this page.
Jump to
Uh oh!
There was an error while loading. Please reload this page.
Diff view
Diff view
There are no files selected for viewing
Large diffs are not rendered by default.
Uh oh!
There was an error while loading. Please reload this page.
| Original file line number | Diff line number | Diff line change | ||||||||||||||||||||||||||||||||||
|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
| @@ -6,7 +6,80 @@ const { readSessionMessages, buildTargetRecords } = require('./session-convert-i | ||||||||||||||||||||||||||||||||||||
| function printUsage() { | ||||||||||||||||||||||||||||||||||||
| console.log('\n用法:'); | ||||||||||||||||||||||||||||||||||||
| console.log(' codexmate convert-session --from <codex|claude> --to <codex|claude> (--session-id <ID>|--file <PATH>) [--output <PATH>] [--max-messages <N|all|Infinity>]'); | ||||||||||||||||||||||||||||||||||||
| 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>]'); | ||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||
| function resolveExistingDir(candidates, fallback) { | ||||||||||||||||||||||||||||||||||||
| for (const candidate of candidates) { | ||||||||||||||||||||||||||||||||||||
| if (!candidate) continue; | ||||||||||||||||||||||||||||||||||||
| try { | ||||||||||||||||||||||||||||||||||||
| if (fs.existsSync(candidate) && fs.statSync(candidate).isDirectory()) return candidate; | ||||||||||||||||||||||||||||||||||||
| } catch (_) {} | ||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||
| return fallback; | ||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||
| function resolveCodexSessionsDir() { | ||||||||||||||||||||||||||||||||||||
| const home = process.env.HOME || process.env.USERPROFILE || ''; | ||||||||||||||||||||||||||||||||||||
| const candidates = []; | ||||||||||||||||||||||||||||||||||||
| if (process.env.CODEX_HOME) candidates.push(path.join(process.env.CODEX_HOME, 'sessions')); | ||||||||||||||||||||||||||||||||||||
| if (process.env.XDG_CONFIG_HOME) candidates.push(path.join(process.env.XDG_CONFIG_HOME, 'codex', 'sessions')); | ||||||||||||||||||||||||||||||||||||
| if (home) { | ||||||||||||||||||||||||||||||||||||
| candidates.push(path.join(home, '.config', 'codex', 'sessions')); | ||||||||||||||||||||||||||||||||||||
| candidates.push(path.join(home, '.codex', 'sessions')); | ||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||
| return resolveExistingDir(candidates, candidates[candidates.length - 1] || path.resolve('.codex/sessions')); | ||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||
| function resolveClaudeProjectsDir() { | ||||||||||||||||||||||||||||||||||||
| const home = process.env.HOME || process.env.USERPROFILE || ''; | ||||||||||||||||||||||||||||||||||||
| const candidates = []; | ||||||||||||||||||||||||||||||||||||
| const claudeHome = process.env.CLAUDE_HOME || process.env.CLAUDE_CONFIG_DIR || ''; | ||||||||||||||||||||||||||||||||||||
| if (claudeHome) candidates.push(path.join(claudeHome, 'projects')); | ||||||||||||||||||||||||||||||||||||
| if (process.env.XDG_CONFIG_HOME) candidates.push(path.join(process.env.XDG_CONFIG_HOME, 'claude', 'projects')); | ||||||||||||||||||||||||||||||||||||
| if (home) { | ||||||||||||||||||||||||||||||||||||
| candidates.push(path.join(home, '.config', 'claude', 'projects')); | ||||||||||||||||||||||||||||||||||||
| candidates.push(path.join(home, '.claude', 'projects')); | ||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||
| return resolveExistingDir(candidates, candidates[candidates.length - 1] || path.resolve('.claude/projects')); | ||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||
Comment on lines
+35
to
+46
Contributor There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Guard against empty candidates array. Same edge case as in 🛡️ Suggested guard for robustness- return resolveExistingDir(candidates, candidates[candidates.length - 1] || path.resolve('.claude/projects'));+ const fallback = candidates.length > 0 ? candidates[candidates.length - 1] : null;+ return resolveExistingDir(candidates, fallback || path.resolve('.claude/projects'));🤖 Prompt for AI Agents | ||||||||||||||||||||||||||||||||||||
| function sanitizeClaudeProjectName(cwd) { | ||||||||||||||||||||||||||||||||||||
| const value = typeof cwd === 'string' && cwd.trim() ? path.resolve(cwd.trim()) : 'codexmate-derived'; | ||||||||||||||||||||||||||||||||||||
| return value.replace(/[^a-zA-Z0-9._-]/g, '-').replace(/-+/g, '-').replace(/^-|-$/g, '') || 'codexmate-derived'; | ||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||
| function buildSourceKey(from, sessionId, filePath) { | ||||||||||||||||||||||||||||||||||||
| const seed = `${from}|${sessionId || ''}|${filePath || ''}`; | ||||||||||||||||||||||||||||||||||||
| let hash = 0; | ||||||||||||||||||||||||||||||||||||
| for (let i = 0; i < seed.length; i += 1) hash = ((hash << 5) - hash + seed.charCodeAt(i)) | 0; | ||||||||||||||||||||||||||||||||||||
| return String(Math.abs(hash)).padStart(8, '0').slice(0, 8); | ||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||
| function resolveDefaultOutputPath(opt, sessionId, cwd) { | ||||||||||||||||||||||||||||||||||||
| const safeSessionId = String(sessionId).replace(/[^a-zA-Z0-9_-]/g, '_') || 'session'; | ||||||||||||||||||||||||||||||||||||
| if (opt.output) return resolveOutputPath(opt.output, `${opt.to}-session-${safeSessionId}.jsonl`); | ||||||||||||||||||||||||||||||||||||
| if (opt.outputDir === 'derived') { | ||||||||||||||||||||||||||||||||||||
| const home = process.env.HOME || process.env.USERPROFILE || ''; | ||||||||||||||||||||||||||||||||||||
| const root = path.join(home || process.cwd(), '.codexmate', 'sessions', 'derived', opt.to, opt.from, buildSourceKey(opt.from, sessionId, opt.filePath)); | ||||||||||||||||||||||||||||||||||||
| return path.join(root, `${safeSessionId}.jsonl`); | ||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||
| if (opt.to === 'codex') return path.join(resolveCodexSessionsDir(), `${safeSessionId}.jsonl`); | ||||||||||||||||||||||||||||||||||||
| return path.join(resolveClaudeProjectsDir(), sanitizeClaudeProjectName(cwd), `${safeSessionId}.jsonl`); | ||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||
| function getDerivedSessionMetaPath(filePath) { | ||||||||||||||||||||||||||||||||||||
| if (!filePath) return ''; | ||||||||||||||||||||||||||||||||||||
| return filePath.toLowerCase().endsWith('.jsonl') | ||||||||||||||||||||||||||||||||||||
| ? filePath.slice(0, -6) + '.meta.json' | ||||||||||||||||||||||||||||||||||||
| : `${filePath}.meta.json`; | ||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||
| function writeJsonAtomic(filePath, value) { | ||||||||||||||||||||||||||||||||||||
| const tmpPath = `${filePath}.tmp-${process.pid}-${Date.now()}`; | ||||||||||||||||||||||||||||||||||||
| fs.writeFileSync(tmpPath, `${JSON.stringify(value, null, 2)}\n`, { encoding: 'utf-8', flag: 'wx' }); | ||||||||||||||||||||||||||||||||||||
| fs.renameSync(tmpPath, filePath); | ||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||
Comment on lines
+79
to
83
Contributor There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Clean up temp file if rename fails. If 🧹 Proposed cleanup on error function writeJsonAtomic(filePath, value) {
const tmpPath = `${filePath}.tmp-${process.pid}-${Date.now()}`;
- fs.writeFileSync(tmpPath, `${JSON.stringify(value, null, 2)}\n`, { encoding: 'utf-8', flag: 'wx' });- fs.renameSync(tmpPath, filePath);+ try {+ fs.writeFileSync(tmpPath, `${JSON.stringify(value, null, 2)}\n`, { encoding: 'utf-8', flag: 'wx' });+ fs.renameSync(tmpPath, filePath);+ } catch (error) {+ try {+ if (fs.existsSync(tmpPath)) fs.unlinkSync(tmpPath);+ } catch (_) {}+ throw error;+ }
}📝 Committable suggestion
Suggested change
🤖 Prompt for AI Agents | ||||||||||||||||||||||||||||||||||||
| async function cmdConvertSession(args = [], deps = {}) { | ||||||||||||||||||||||||||||||||||||
| @@ -28,12 +101,46 @@ async function cmdConvertSession(args = [], deps = {}) { | ||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||
| const extracted = await readSessionMessages(filePath, opt.from, opt.maxMessages); | ||||||||||||||||||||||||||||||||||||
| const sessionId = extracted.sessionId || opt.sessionId || path.basename(filePath, '.jsonl'); | ||||||||||||||||||||||||||||||||||||
| const safeSessionId = String(sessionId).replace(/[^a-zA-Z0-9_-]/g, '_'); | ||||||||||||||||||||||||||||||||||||
| const records = buildTargetRecords(opt.to, { sessionId, cwd: extracted.cwd || '', messages: extracted.messages }); | ||||||||||||||||||||||||||||||||||||
| const jsonl = `${records.map(r => JSON.stringify(r)).join('\n')}\n`; | ||||||||||||||||||||||||||||||||||||
| const outputPath = resolveOutputPath(opt.output, `${opt.to}-session-${safeSessionId}.jsonl`); | ||||||||||||||||||||||||||||||||||||
| const outputPath = resolveDefaultOutputPath(opt, sessionId, extracted.cwd || ''); | ||||||||||||||||||||||||||||||||||||
| const metaPath = getDerivedSessionMetaPath(outputPath); | ||||||||||||||||||||||||||||||||||||
| ensureDir(path.dirname(outputPath)); | ||||||||||||||||||||||||||||||||||||
| fs.writeFileSync(outputPath, jsonl, 'utf-8'); | ||||||||||||||||||||||||||||||||||||
| try { | ||||||||||||||||||||||||||||||||||||
| if (fs.existsSync(metaPath)) { | ||||||||||||||||||||||||||||||||||||
| const error = new Error(`target session metadata already exists: ${metaPath}`); | ||||||||||||||||||||||||||||||||||||
| error.code = 'EEXIST'; | ||||||||||||||||||||||||||||||||||||
| throw error; | ||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||
| fs.writeFileSync(outputPath, jsonl, { encoding: 'utf-8', flag: 'wx' }); | ||||||||||||||||||||||||||||||||||||
| writeJsonAtomic(metaPath, { | ||||||||||||||||||||||||||||||||||||
| version: 1, | ||||||||||||||||||||||||||||||||||||
| createdAt: new Date().toISOString(), | ||||||||||||||||||||||||||||||||||||
| source: { | ||||||||||||||||||||||||||||||||||||
| type: opt.from, | ||||||||||||||||||||||||||||||||||||
| sessionId, | ||||||||||||||||||||||||||||||||||||
| filePath | ||||||||||||||||||||||||||||||||||||
| }, | ||||||||||||||||||||||||||||||||||||
| target: { | ||||||||||||||||||||||||||||||||||||
| type: opt.to, | ||||||||||||||||||||||||||||||||||||
| sessionId, | ||||||||||||||||||||||||||||||||||||
| filePath: outputPath | ||||||||||||||||||||||||||||||||||||
| }, | ||||||||||||||||||||||||||||||||||||
| options: { | ||||||||||||||||||||||||||||||||||||
| maxMessages: opt.maxMessages, | ||||||||||||||||||||||||||||||||||||
| outputDir: opt.outputDir | ||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||
| }); | ||||||||||||||||||||||||||||||||||||
| } catch (error) { | ||||||||||||||||||||||||||||||||||||
| if (error && error.code === 'EEXIST') { | ||||||||||||||||||||||||||||||||||||
| console.error('转换失败: target session already exists:', outputPath); | ||||||||||||||||||||||||||||||||||||
| process.exit(1); | ||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||
| try { | ||||||||||||||||||||||||||||||||||||
| if (fs.existsSync(outputPath) && !fs.existsSync(metaPath)) fs.unlinkSync(outputPath); | ||||||||||||||||||||||||||||||||||||
| } catch (_) {} | ||||||||||||||||||||||||||||||||||||
| throw error; | ||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||
| console.log('\n✓ 会话已转换:', outputPath); | ||||||||||||||||||||||||||||||||||||
coderabbitai[bot] marked this conversation as resolved.
Uh oh!There was an error while loading. Please reload this page. | ||||||||||||||||||||||||||||||||||||
| if (extracted.truncated) console.log('! 已截断: 可使用 --max-messages=all'); | ||||||||||||||||||||||||||||||||||||
| console.log(); | ||||||||||||||||||||||||||||||||||||
Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.
Uh oh!
There was an error while loading. Please reload this page.
Uh oh!
There was an error while loading. Please reload this page.
Uh oh!
There was an error while loading. Please reload this page.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Guard against empty candidates array.
If all environment variables are unset and
HOME/USERPROFILEis empty (rare but possible in containerized/test environments), thecandidatesarray could be empty or contain only falsy values. Line 32'scandidates[candidates.length - 1]would then beundefined, relying solely on the||fallback.While the fallback handles this, consider an explicit guard for clarity.
🛡️ Suggested guard for robustness
📝 Committable suggestion
🤖 Prompt for AI Agents