✨ feat(vscode): sessionBundle 会话文件查找/安装/去重原语
Claude-Session: https://claude.ai/code/session_011cEyL6k351U2BzX1Qmygph
This commit is contained in:
1 parent
ad82808dd0
commit
569f24c11a
2 files changed
+247
No files matched your search
@@ -0,0 +1,122 @@
|
||||
/**
|
||||
* claude / codex 会话文件的搬运原语。
|
||||
*
|
||||
* claude 会话 = `<projectsDir>/<sid>.jsonl` + 可选 `<projectsDir>/<sid>/`
|
||||
* (subagents、tool-results),两者必须一起搬。新版 claude 在任意目录
|
||||
* `--resume` 时会搜全部 project 目录,同一 sid 出现两份就拒绝 resume——
|
||||
* 所以「安装」总是先拷再删本机其它副本。
|
||||
*/
|
||||
|
||||
import { promises as fsp } from 'node:fs'
|
||||
import * as os from 'node:os'
|
||||
import * as path from 'node:path'
|
||||
import { ROLLOUT_UUID_REGEX } from './codexSessionWatcher'
|
||||
|
||||
export interface ClaudeSessionFiles {
|
||||
projectsDir: string
|
||||
jsonl: string
|
||||
dir?: string
|
||||
}
|
||||
|
||||
export function claudeProjectsRoot(): string {
|
||||
return path.join(os.homedir(), '.claude', 'projects')
|
||||
}
|
||||
|
||||
async function exists(p: string): Promise<boolean> {
|
||||
try {
|
||||
await fsp.stat(p)
|
||||
return true
|
||||
}
|
||||
catch {
|
||||
return false
|
||||
}
|
||||
}
|
||||
|
||||
async function sessionFilesIn(projectsDir: string, sid: string): Promise<ClaudeSessionFiles | undefined> {
|
||||
const jsonl = path.join(projectsDir, `${sid}.jsonl`)
|
||||
if (!(await exists(jsonl)))
|
||||
return undefined
|
||||
const dir = path.join(projectsDir, sid)
|
||||
return (await exists(dir)) ? { projectsDir, jsonl, dir } : { projectsDir, jsonl }
|
||||
}
|
||||
|
||||
export async function listClaudeSessionCopies(sid: string, projectsRoot: string): Promise<ClaudeSessionFiles[]> {
|
||||
let entries: string[]
|
||||
try {
|
||||
entries = await fsp.readdir(projectsRoot)
|
||||
}
|
||||
catch {
|
||||
return []
|
||||
}
|
||||
const out: ClaudeSessionFiles[] = []
|
||||
for (const name of entries) {
|
||||
const found = await sessionFilesIn(path.join(projectsRoot, name), sid)
|
||||
if (found)
|
||||
out.push(found)
|
||||
}
|
||||
return out
|
||||
}
|
||||
|
||||
export async function findClaudeSessionFiles(sid: string, projectsRoot: string): Promise<ClaudeSessionFiles | undefined> {
|
||||
const copies = await listClaudeSessionCopies(sid, projectsRoot)
|
||||
if (copies.length === 0)
|
||||
return undefined
|
||||
const stamped = await Promise.all(copies.map(async c => ({ c, mtime: (await fsp.stat(c.jsonl)).mtimeMs })))
|
||||
stamped.sort((a, b) => b.mtime - a.mtime)
|
||||
return stamped[0].c
|
||||
}
|
||||
|
||||
export async function copyClaudeSessionFiles(files: ClaudeSessionFiles, dstDir: string): Promise<void> {
|
||||
await fsp.mkdir(dstDir, { recursive: true })
|
||||
await fsp.copyFile(files.jsonl, path.join(dstDir, path.basename(files.jsonl)))
|
||||
if (files.dir)
|
||||
await fsp.cp(files.dir, path.join(dstDir, path.basename(files.dir)), { recursive: true, force: true })
|
||||
}
|
||||
|
||||
async function removeClaudeSessionFiles(files: ClaudeSessionFiles): Promise<void> {
|
||||
await fsp.rm(files.jsonl, { force: true })
|
||||
if (files.dir)
|
||||
await fsp.rm(files.dir, { recursive: true, force: true })
|
||||
}
|
||||
|
||||
export async function installClaudeSession(opts: {
|
||||
sid: string
|
||||
srcDir: string
|
||||
dstProjectsDir: string
|
||||
projectsRoot: string
|
||||
}): Promise<void> {
|
||||
const src = await sessionFilesIn(path.resolve(opts.srcDir), opts.sid)
|
||||
if (!src)
|
||||
throw new Error(`源目录缺少会话文件 ${opts.sid}.jsonl:${opts.srcDir}`)
|
||||
const dst = path.resolve(opts.dstProjectsDir)
|
||||
if (src.projectsDir !== dst)
|
||||
await copyClaudeSessionFiles(src, dst)
|
||||
for (const copy of await listClaudeSessionCopies(opts.sid, opts.projectsRoot)) {
|
||||
if (path.resolve(copy.projectsDir) !== dst)
|
||||
await removeClaudeSessionFiles(copy)
|
||||
}
|
||||
}
|
||||
|
||||
/** codex rollout 全局存放,按文件名尾部 uuid 定位;返回相对 codexRoot 的路径。 */
|
||||
export async function codexRolloutRelPathFor(sid: string, codexRoot: string): Promise<string | undefined> {
|
||||
const want = sid.toLowerCase()
|
||||
let entries: string[]
|
||||
try {
|
||||
entries = await fsp.readdir(codexRoot, { recursive: true })
|
||||
}
|
||||
catch {
|
||||
return undefined
|
||||
}
|
||||
for (const entry of entries) {
|
||||
const m = path.basename(entry).match(ROLLOUT_UUID_REGEX)
|
||||
if (m && m[1].toLowerCase() === want)
|
||||
return entry
|
||||
}
|
||||
return undefined
|
||||
}
|
||||
|
||||
export async function installCodexSession(relPath: string, srcFile: string, codexRoot: string): Promise<void> {
|
||||
const dst = path.join(codexRoot, relPath)
|
||||
await fsp.mkdir(path.dirname(dst), { recursive: true })
|
||||
await fsp.copyFile(srcFile, dst)
|
||||
}
|
||||
Reference in new issue
Block a user