Documentation
¶
Overview ¶
Package bundle creates and (later) reads .codexbundle archives: a ZIP containing a manifest, a checksum map, and the copied Codex rollout files.
A .codexbundle is a portable, hosting-free way to move local Codex sessions between devices. It never includes or modifies Codex's SQLite state DB.
Index ¶
- Constants
- func DirExists(p string) bool
- func ParseSince(s string) (time.Time, error)
- type Action
- type CWDInfo
- type CWDMapping
- type CWDSummary
- type Checksums
- type ExportOptions
- type ExportResult
- type ImportItem
- type ImportOptions
- type ImportResult
- type InspectResult
- type Manifest
- type ManifestSession
- type TranslateItem
- type TranslateOptions
- type TranslateResult
Constants ¶
const ( ManifestName = "manifest.json" ChecksumsName = "checksums.json" )
Bundle file names.
const FormatVersion = "codex-sync-bundle-v1"
FormatVersion identifies the bundle layout. Bump this on breaking changes.
Variables ¶
This section is empty.
Functions ¶
func DirExists ¶
DirExists reports whether p exists on the local machine and is a directory. It only reads (os.Stat); it never creates anything.
func ParseSince ¶ added in v0.4.0
ParseSince accepts either an absolute date (YYYY-MM-DD, interpreted as UTC midnight) or a relative duration ending in d/h/m (e.g. 7d, 48h, 90m) measured back from now. It returns the cutoff instant; sessions updated at or after it pass the filter. It is shared by the CLI (`export --since`) and the desktop UI.
Types ¶
type Action ¶
type Action string
Action describes what import decided to do with one bundle entry.
const ( ActionImport Action = "import" // new file; copied (unless dry-run) ActionSkipIdentical Action = "skip-identical" // target exists with same checksum ActionConflict Action = "conflict" // target exists with different checksum; not overwritten ActionReplace Action = "replace" // conflict, overwritten after backing up the local file ActionImportCopy Action = "import-copy" // conflict, imported as a brand-new session (fresh id + filename) ActionUpdate Action = "update" // --merge: local is a prefix of the bundle; extended in place (append-only, lossless) ActionSkipAhead Action = "skip-ahead" // --merge: bundle is a prefix of the local file; local already current ActionSkipArchived Action = "skip-archived" // archived_sessions entry; not imported in v0.1 ActionSkipNonSession Action = "skip-non-session" // unexpected non-session file ActionSkipDeselected Action = "skip-deselected" // not among the --session ids requested for import )
type CWDInfo ¶
type CWDInfo struct {
// Path is the first-seen original spelling of the cwd (for display).
Path string
// Count is how many sessions recorded this cwd.
Count int
// ExistsLocal reports whether the directory exists on this machine. A
// session whose cwd does not exist locally is hidden in Codex's project
// sidebar until the folder is created or the cwd is remapped on import.
ExistsLocal bool
}
CWDInfo describes one distinct recorded working directory across a bundle's sessions and whether that directory exists on the local machine.
type CWDMapping ¶
CWDMapping rewrites a session's recorded working directory from Old to New during import. It is the only feature that mutates session content, and it touches nothing except the canonical "cwd" field of the session_meta line.
func ParseCWDMappings ¶
func ParseCWDMappings(specs []string) ([]CWDMapping, error)
ParseCWDMappings parses and validates a list of "OLD=NEW" mapping specs.
Rules:
- syntax must be OLD=NEW (split on the first '='),
- OLD must not be empty,
- NEW must not be empty and must be absolute/plausible for the target OS,
- OLD and NEW must differ after normalization,
- duplicate OLD paths (case-insensitive on Windows) are rejected.
type CWDSummary ¶
type CWDSummary struct {
// Dirs are the distinct non-empty recorded cwds, sorted by path.
Dirs []CWDInfo
// UnknownCWD counts sessions with no recorded cwd (e.g. compressed
// sessions, whose cwd is unknown in v0.1).
UnknownCWD int
// MissingCount is how many entries in Dirs do not exist locally.
MissingCount int
}
CWDSummary groups a bundle's sessions by their recorded working directory and reports which directories are missing on the local machine. It is the data behind the "project folders" view that helps users find the #1 portability gotcha (imported sessions hidden because their cwd does not exist here).
func SummarizeCWDs ¶
func SummarizeCWDs(sessions []ManifestSession, exists func(string) bool) CWDSummary
SummarizeCWDs groups sessions by recorded cwd and, for each distinct directory, reports the session count and whether it exists locally. Grouping is case-insensitive on Windows (via normalizeCWD), matching how cwd is compared elsewhere; the first-seen spelling is preserved for display. The exists function is injected so the logic is testable without touching the filesystem; production callers pass DirExists.
type Checksums ¶
Checksums maps each bundle-relative file path (forward slashes) to its SHA-256 hex digest. It is written as checksums.json at the root of the ZIP and covers every file in the bundle except checksums.json itself.
type ExportOptions ¶
type ExportOptions struct {
// Tool selects which agent's sessions to export ("" / agent.Codex scans the
// Codex home; agent.Claude scans ClaudeHome). The chosen tool is recorded in
// the manifest so import knows how to place the files back.
Tool agent.Kind
// ClaudeHome is the resolved Claude Code home, used only when Tool is
// agent.Claude. (The Codex home is passed to Export directly.)
ClaudeHome claudehome.Home
// ProjectPath, when non-empty, restricts the export to sessions whose
// SessionMeta cwd matches this (already absolute) path. Leave empty to
// export every session regardless of cwd (the `--all` behavior).
ProjectPath string
// OutputPath is the .codexbundle file to write.
OutputPath string
// IncludeArchived also considers archived sessions.
IncludeArchived bool
// Since, when non-zero, restricts the export to sessions whose file
// modification time is at or after this instant.
Since time.Time
// SessionID, when non-empty, exports exactly the single session whose
// thread id equals (or uniquely begins with) this value, regardless of
// cwd. It is mutually exclusive with project/all filtering.
SessionID string
// WithGit forces capture of the project's git metadata (remote, branch,
// commit, dirty/unpushed) into the manifest even when ProjectPath is empty
// (e.g. with --all or --session). When ProjectPath is set, git metadata is
// always captured regardless of this flag.
WithGit bool
}
ExportOptions configures an export.
type ExportResult ¶
type ExportResult struct {
BundlePath string
Manifest Manifest
IncludedCount int
TotalScanned int
CompressedSkipped int // compressed sessions skipped by cwd filter (cwd unknown)
Warnings []string
}
ExportResult summarizes what was exported.
func Export ¶
func Export(home codexhome.Home, opts ExportOptions) (ExportResult, error)
Export scans the Codex home, selects sessions (optionally filtered to a project's cwd), and writes a .codexbundle ZIP containing the rollout files, a manifest, and a checksum map. It never reads or writes Codex's SQLite.
type ImportItem ¶
type ImportItem struct {
BundlePath string
DestPath string
Action Action
OriginalCWD string
CWDMismatch bool
// Mapped reports whether --map-cwd rewrote this session's cwd.
Mapped bool
// Copied reports whether this entry was imported as a brand-new session
// (ActionImportCopy, --import-as-copy) with NewThreadID as its fresh id.
Copied bool
// NewThreadID is the freshly assigned session id when Copied is true.
NewThreadID string
// BackupPath, when non-empty, is where the pre-existing local file was
// backed up before being replaced (ActionReplace, --replace-with-backup).
BackupPath string
// LinesAdded is the number of new lines a --merge update appended on top of
// the existing local file (ActionUpdate). It is an approximate, line-based
// count for reporting only.
LinesAdded int
// contains filtered or unexported fields
}
ImportItem is the plan/outcome for a single bundle entry.
type ImportOptions ¶
type ImportOptions struct {
BundlePath string
DryRun bool
// ProjectPath, when non-empty, enables per-session cwd-mismatch checks
// against this (already absolute) path.
ProjectPath string
// MapCWD, when set, rewrites matching sessions' recorded cwd on import.
// Only plain .jsonl files are rewritten; .jsonl.zst files that match a
// mapping are copied byte-for-byte and reported as unmappable.
MapCWD []CWDMapping
// ReplaceWithBackup turns conflicts (a local file with different content
// for the same session) into a replace: the local file is backed up next
// to itself and then overwritten with the bundle's version. Without it,
// conflicts are reported and skipped (the default, never-overwrite behavior).
ReplaceWithBackup bool
// SessionIDs, when non-empty, restricts the import to the bundle sessions
// whose thread id equals (or uniquely begins with) one of these values; every
// other session in the bundle is skipped (ActionSkipDeselected). An id that
// matches no session in the bundle is an error (nothing is written).
SessionIDs []string
// ImportAsCopy turns conflicts into a brand-new session: the bundle's
// version is assigned a fresh session id and written under a new rollout
// filename, so it coexists with the diverged local session rather than being
// skipped or replacing it. Only plain .jsonl files can be copied; a
// compressed (.jsonl.zst) conflict, or one without a session_meta id, stays a
// skipped conflict. Mutually exclusive with ReplaceWithBackup at the CLI.
ImportAsCopy bool
// Merge enables append-only incremental sync. Session files are append-only
// logs, so when a session that already exists locally grew on the other
// device, the bundle's copy is a byte-prefix superset of the local file.
// With Merge set, such a "grown" session is updated in place (ActionUpdate):
// the longer bundle version is written, which appends the new lines without
// losing anything. When the local file is already a superset of the bundle's
// (the local copy is ahead), the session is left untouched (ActionSkipAhead).
// Sessions that genuinely diverged (neither side is a prefix of the other)
// remain conflicts, handled by ReplaceWithBackup/ImportAsCopy or skipped.
// Merge composes with those two flags: it resolves clean growth first and
// hands true divergence to them.
Merge bool
}
ImportOptions configures an import.
type ImportResult ¶
type ImportResult struct {
Manifest Manifest
Items []ImportItem
Imported int
SkippedIdentical int
Conflicts int
SkippedOther int
// SkippedDeselected counts bundle sessions excluded by a --session filter.
SkippedDeselected int
CWDMismatchCount int
// Replaced counts conflicting sessions that were overwritten after the local
// file was backed up (--replace-with-backup).
Replaced int
// Updated counts sessions that grew on the other device and were extended in
// place by --merge (ActionUpdate). LinesAdded is the total number of lines
// those updates appended.
Updated int
LinesAdded int
// AlreadyAhead counts sessions left untouched by --merge because the local
// copy already contained everything in the bundle (and possibly more).
AlreadyAhead int
// ImportedCopies counts conflicting sessions imported as brand-new sessions
// (--import-as-copy).
ImportedCopies int
// Mapped counts imported sessions whose cwd was rewritten by --map-cwd.
Mapped int
// MappedCompressedSkipped counts compressed sessions that matched a
// mapping but could not be rewritten in v0.1 (copied byte-for-byte).
MappedCompressedSkipped int
ProjectProvided bool
DryRun bool
Warnings []string
}
ImportResult summarizes an import.
func Import ¶
func Import(home codexhome.Home, opts ImportOptions) (ImportResult, error)
Import validates a bundle end-to-end and, unless DryRun is set, copies its session files into the agent home (home.Root), preserving each agent's layout: Codex rollouts under sessions/YYYY/MM/DD/ and Claude Code transcripts under projects/<encoded-cwd>/. The bundle's recorded tool selects which.
Safety guarantees:
- The whole bundle's checksums are verified BEFORE anything is written.
- Unsafe entry paths (absolute, drive-letter, zip-slip) abort the import.
- Only the agent's recognized session files are imported (anything else is skipped and reported).
- Existing files are never overwritten: identical files are skipped, differing files are reported as conflicts and skipped.
- Writes are atomic (temp file + rename). The agent's index (Codex SQLite, Claude ~/.claude.json) is never touched.
- .jsonl.zst files are copied byte-for-byte; never parsed or decompressed.
type InspectResult ¶
InspectResult is the read-only view of a bundle: its manifest, its checksum map, and the names of the files it contains. Inspecting never extracts or writes anything.
func Inspect ¶
func Inspect(bundlePath string) (InspectResult, error)
Inspect opens a .codexbundle and reads manifest.json and checksums.json without extracting any session files. It validates that the bundle has the expected format version.
type Manifest ¶
type Manifest struct {
FormatVersion string `json:"format_version"`
// Tool records which coding agent the sessions came from ("codex" or
// "claude"). It is omitted for legacy Codex bundles, which are treated as
// "codex" on read, so older bundles remain importable unchanged.
Tool string `json:"tool,omitempty"`
CreatedAt string `json:"created_at"`
CreatedByDevice string `json:"created_by_device,omitempty"`
SourceOS string `json:"source_os"`
SourceCodexHome string `json:"source_codex_home"`
SourceProjectPath string `json:"source_project_path,omitempty"`
Git *git.Info `json:"git,omitempty"`
CodexVersion string `json:"codex_version,omitempty"`
Sessions []ManifestSession `json:"sessions"`
}
Manifest describes a bundle and everything inside it. It is written as manifest.json at the root of the ZIP.
type ManifestSession ¶
type ManifestSession struct {
ThreadID string `json:"thread_id,omitempty"`
OriginalPath string `json:"original_path"`
BundlePath string `json:"bundle_path"`
OriginalCWD string `json:"original_cwd,omitempty"`
Preview string `json:"preview,omitempty"`
FirstUserMessage string `json:"first_user_message,omitempty"`
CreatedAt string `json:"created_at,omitempty"`
UpdatedAt string `json:"updated_at,omitempty"`
Source string `json:"source,omitempty"`
ModelProvider string `json:"model_provider,omitempty"`
GitBranch string `json:"git_branch,omitempty"`
GitSHA string `json:"git_sha,omitempty"`
Archived bool `json:"archived"`
Compressed bool `json:"compressed"`
SizeBytes int64 `json:"size_bytes"`
SHA256 string `json:"sha256"`
}
ManifestSession is one rollout file recorded in the manifest.
type TranslateItem ¶ added in v0.3.0
type TranslateItem struct {
BundlePath string
DestPath string
SessionID string // id assigned in the target agent
Turns int
Action string // "translate" | "skip-existing" | "skip"
Reason string // why it was skipped, when Action == "skip"
}
TranslateItem is the plan/outcome for one translated session.
type TranslateOptions ¶ added in v0.3.0
TranslateOptions configures a cross-agent handoff import: reading a bundle of one agent's sessions and writing translated, continue-from-here sessions into a different agent's home.
type TranslateResult ¶ added in v0.3.0
type TranslateResult struct {
SourceTool agent.Kind
TargetTool agent.Kind
Translated int
SkippedExisting int
Skipped int
Items []TranslateItem
Warnings []string
DryRun bool
}
TranslateResult summarizes a cross-agent handoff import.
func TranslateImport ¶ added in v0.3.0
func TranslateImport(targetHome codexhome.Home, opts TranslateOptions) (TranslateResult, error)
TranslateImport reads the bundle (whatever agent it came from), converts each session into TargetTool's format via the handoff package, and writes the results into targetHome (whose Root is the target agent's home). It never overwrites: a session that already exists is skipped (translation is deterministic, so a re-run is an idempotent skip). Checksums of the source bundle are verified before anything is read, exactly like a native import.