Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 4 additions & 1 deletion Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -100,13 +100,16 @@ required-features = ["search"]
[features]
# Explicit cloud Cowork reads reuse the Claude Desktop transport.
cowork_remote = ["claude_chat"]
default = ["opencode", "hermes", "claude_chat", "cowork_remote", "chatgpt", "search"]
default = ["opencode", "hermes", "uji", "claude_chat", "cowork_remote", "chatgpt", "search"]
# The OpenCode harness store reads OpenCode's SQLite database. The OpenCode
# codec itself is always available; only the on-disk store needs rusqlite.
opencode = ["dep:rusqlite"]
# Hermes Agent's canonical session store is SQLite. The JSON export codec is
# always available; only local discovery/loading needs rusqlite.
hermes = ["dep:rusqlite"]
# uji keeps its sessions in SQLite. The JSON text codec is always available;
# only the database store needs rusqlite.
uji = ["dep:rusqlite"]
# Claude Chat is a live, read-only remote store. Its codec remains available
# featureless; the feature adds browser-compatible HTTPS access and macOS
# Claude Desktop cookie decryption.
Expand Down
1 change: 1 addition & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -119,6 +119,7 @@ Each name links to its format documentation. Use the ID with `--from` and `--wit
| [Grok Bot](docs/formats/grok-bot.md) | `grok_bot` | Yes | Via local gateway |
| [fx](docs/formats/fx.md) | `fx` | Yes | Yes |
| [Antigravity](docs/formats/antigravity.md) | `antigravity` | Yes | Yes |
| [uji](docs/formats/uji.md) | `uji` | Yes | Yes |
| [Hermes Agent](docs/formats/hermes.md) | `hermes` | Yes | No |
| [Amp](docs/formats/amp.md) | `amp` | Yes | No |
| [Cloud Cowork](docs/formats/cowork-remote.md) | `cowork_remote` | Live account | No |
Expand Down
9 changes: 5 additions & 4 deletions cli/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -69,7 +69,7 @@ mod pager;
mod view;

pub const HARNESSES: &str = "harnesses: claude_code, claude_chat, chatgpt, codex, opencode, pi, campfire, cursor, cursor_desktop, grok, fx, hermes, \
amp, antigravity, simple, cowork, cowork_remote";
amp, antigravity, simple, cowork, cowork_remote, uji";

/// The `txcript` binary's command line.
#[derive(Parser)]
Expand Down Expand Up @@ -1324,6 +1324,7 @@ mod style {
HarnessId::Antigravity => "\x1b[94m", // bright blue
HarnessId::Simple => "\x1b[92m", // bright green
HarnessId::Cowork => "\x1b[38;5;208m", // orange
HarnessId::Uji => "\x1b[38;5;180m", // tan
}
}
}
Expand Down Expand Up @@ -1943,12 +1944,12 @@ fn fresh_identity(
if out.is_some() {
return;
}
// Codex stamps its rollouts with v7 UUIDs; matching the shape keeps the
// copy out of any version-aware code path. v4 everywhere else. Harnesses
// Codex and uji stamp their sessions with v7 UUIDs; matching the shape
// keeps the copy out of any version-aware code path. v4 everywhere else. Harnesses
// that need a different spelling (opencode's `ses_` prefix) re-shape this
// themselves in `from_common`.
common.meta.id = match target {
HarnessId::Codex => uuid::Uuid::now_v7().to_string(),
HarnessId::Codex | HarnessId::Uji => uuid::Uuid::now_v7().to_string(),
_ => uuid::Uuid::new_v4().to_string(),
};
common.meta.timestamp = chrono::Utc::now();
Expand Down
1 change: 1 addition & 0 deletions docs/formats/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -57,3 +57,4 @@ than none.
| [pi.md](pi.md) | pi | `src/harness/pi.rs` |
| [campfire.md](campfire.md) | Campfire (embeds pi) | `src/harness/campfire.rs` |
| [simple.md](simple.md) | Simple (txcript's own interchange format) | `src/harness/simple.rs` |
| [uji.md](uji.md) | uji | `src/harness/uji.rs` |
95 changes: 95 additions & 0 deletions docs/formats/uji.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,95 @@
# uji

uji keeps every session in one `SQLite` database, `uji.db`. Each session is
a row in `sessions`, and each entry in its transcript is a row in
`messages`, whose `data` column holds the entry as JSON. txcript's portable
text form for uji is the session row with its message rows nested in a
`messages` array, each row's `data` decoded. Provenance is **open source**:
the format is read from uji's storage and agent code, pinned below.

```
~/.local/share/uji/uji.db one database for every session
├─ sessions one row per session
│ id, title, directory, metadata; parent is set for
│ time_created, time_updated, parent sessions another session started
├─ messages ordered rows per session
│ id, session_id, seq, type, data is the entry as JSON
│ time_created, data
└─ settings uji's own settings, untouched
```

## On disk

The database resolves the way uji resolves it: `$UJI_DB`, else `uji.db` in
`$UJI_DATA_DIR`, else in `$XDG_DATA_HOME/uji` when that path is absolute,
else in `~/.local/share/uji`. Discovery lists every `sessions` row, including
sessions another session started, which carry their `parent` as
`Relation::Spawn` lineage. Loading reads the session's `messages` rows in
`seq` order. Times are milliseconds since the Unix epoch.

Saving writes a new session and its rows in one transaction, replacing any
session with the same id. `uji resume --id` accepts only UUID-shaped ids, so
a transcript whose id is not a UUID is saved under a fresh one, and the
returned id is the one to resume. Only uji creates and migrates the
database, so saving into one that does not exist yet fails and asks you to
start uji once. Delete removes the session and its rows.

## Dissection of a transcript

| Their name | What it is | Maps to |
|---|---|---|
| `sessions` row | `id`, `title` (`untitled` until uji names it), `directory`, `time_created`, `parent` | `Meta`; `untitled` becomes no title |
| `user`, `context` | `text`, and `images` with `media_type` and base64 `data` | `Role::User` with `Block::Text` and `Block::Image` |
| `assistant` | `text`, `reasoning`, `tool_calls` with `id`, `name` and JSON-string `arguments`, and `replay` | `Role::Assistant` with `Block::Thinking`, `Block::Text` and `Block::ToolUse` |
| `tool` | `tool_call_id`, `name`, `content`, `images` | `Role::User` with `Block::ToolResult`, followed by any images |
| `compaction` | `summary` of the messages before it | `Role::User` with the summary as `Block::Text` |
| `shell`, `error`, `system` | `!` commands, turn errors, and notes | kept in the native body, no conversational turn |

Tool names map to the Claude convention and back: `read_file` ⇄ `Read`,
`write_file` ⇄ `Write` and `edit_file` ⇄ `Edit`, with `path` ⇄ `file_path`,
and `run_command` ⇄ `Bash`, with `timeout` in seconds ⇄ `timeout_ms`. Other
names, such as plugin and MCP tools, pass through as `Tool::Raw`. A tool
result is an error when its content starts with `error:` or `denied:`, which
is how uji words failed and refused calls.

`replay` holds what a provider needs back on a later turn, tagged with the
API and the model it came from:

| `replay.api` | Contents | Maps to |
|---|---|---|
| `anthropic` | the reply's content blocks, in order: `thinking` with its `signature`, `redacted_thinking` with its `data`, `text`, and `tool_use` | one `Block::Thinking` per thinking block, with `signature`, or with `encrypted` for redacted thinking |
| `responses` | the reply's output items: `reasoning` items with their encrypted content, and `message` and `function_call` items | one `Block::Thinking` per reasoning item, with the item as JSON in `encrypted` |

Writing rebuilds `replay` from those blocks, using the message's model, so
uji sends signed thinking and encrypted reasoning back when it continues with
the same model. Without them, the thinking text is written as `reasoning`.

## Caveats

- An assistant row has one `text`, so several text blocks in one message
join with blank lines, and text written after a tool call moves before it.
- uji stores no stop reason or per-message token usage, so none survive a
hop through uji.
- A user-side slash command (`Tool::Command`) and its output have no slot in
uji and are left out.
- Gemini's per-call thought signatures have no slot in `Common`, so they do
not survive a hop out of uji.
- Images keep their type and data. uji's own `width`, `height` and `name`
are not carried into `Common`.

## References

- Schema and migrations:
[`lua/uji/core/store/init.lua`](https://github.com/uji-labs/uji/blob/3e8bcc1bad3c852e5409ed3246cda2c17b9083e3/lua/uji/core/store/init.lua)
- Message rows and their decoding:
[`lua/uji/core/store/session.lua`](https://github.com/uji-labs/uji/blob/3e8bcc1bad3c852e5409ed3246cda2c17b9083e3/lua/uji/core/store/session.lua)
- Assistant entries and `replay`:
[`lua/uji/core/loop.lua`](https://github.com/uji-labs/uji/blob/3e8bcc1bad3c852e5409ed3246cda2c17b9083e3/lua/uji/core/loop.lua)
- Database location:
[`lua/uji/core/paths.lua`](https://github.com/uji-labs/uji/blob/3e8bcc1bad3c852e5409ed3246cda2c17b9083e3/lua/uji/core/paths.lua)

The parser (`src/harness/uji.rs`) and the integration tests
(`tests/integration/uji.rs`, including a real temp `SQLite` database with
uji's schema) are the normative mapping.

Last verified: 2026-10-02.
1 change: 1 addition & 0 deletions docs/translations/README.de.md
Original file line number Diff line number Diff line change
Expand Up @@ -125,6 +125,7 @@ Jeder Name führt zur Dokumentation des jeweiligen Formats. Verwende die ID mit
| [Grok Bot](../formats/grok-bot.md) | `grok_bot` | Ja | Über lokales Gateway |
| [fx](../formats/fx.md) | `fx` | Ja | Ja |
| [Antigravity](../formats/antigravity.md) | `antigravity` | Ja | Ja |
| [uji](../formats/uji.md) | `uji` | Ja | Ja |
| [Hermes Agent](../formats/hermes.md) | `hermes` | Ja | Nein |
| [Amp](../formats/amp.md) | `amp` | Ja | Nein |
| [Claude Chat](../formats/claude-chat.md) | `claude_chat` | Online-Konto | Nein |
Expand Down
1 change: 1 addition & 0 deletions docs/translations/README.es.md
Original file line number Diff line number Diff line change
Expand Up @@ -125,6 +125,7 @@ Cada nombre enlaza a la documentación de su formato. Usa el ID con `--from` y `
| [Grok Bot](../formats/grok-bot.md) | `grok_bot` | Sí | Mediante la pasarela local |
| [fx](../formats/fx.md) | `fx` | Sí | Sí |
| [Antigravity](../formats/antigravity.md) | `antigravity` | Sí | Sí |
| [uji](../formats/uji.md) | `uji` | Sí | Sí |
| [Hermes Agent](../formats/hermes.md) | `hermes` | Sí | No |
| [Amp](../formats/amp.md) | `amp` | Sí | No |
| [Claude Chat](../formats/claude-chat.md) | `claude_chat` | Cuenta en línea | No |
Expand Down
1 change: 1 addition & 0 deletions docs/translations/README.fr.md
Original file line number Diff line number Diff line change
Expand Up @@ -125,6 +125,7 @@ Chaque nom renvoie à la documentation du format correspondant. Utilisez l'ID av
| [Grok Bot](../formats/grok-bot.md) | `grok_bot` | Oui | Via la passerelle locale |
| [fx](../formats/fx.md) | `fx` | Oui | Oui |
| [Antigravity](../formats/antigravity.md) | `antigravity` | Oui | Oui |
| [uji](../formats/uji.md) | `uji` | Oui | Oui |
| [Hermes Agent](../formats/hermes.md) | `hermes` | Oui | Non |
| [Amp](../formats/amp.md) | `amp` | Oui | Non |
| [Claude Chat](../formats/claude-chat.md) | `claude_chat` | Compte en ligne | Non |
Expand Down
1 change: 1 addition & 0 deletions docs/translations/README.it.md
Original file line number Diff line number Diff line change
Expand Up @@ -125,6 +125,7 @@ Ogni nome rimanda alla documentazione del relativo formato. Usa l'ID con `--from
| [Grok Bot](../formats/grok-bot.md) | `grok_bot` | Sì | Tramite gateway locale |
| [fx](../formats/fx.md) | `fx` | Sì | Sì |
| [Antigravity](../formats/antigravity.md) | `antigravity` | Sì | Sì |
| [uji](../formats/uji.md) | `uji` | Sì | Sì |
| [Hermes Agent](../formats/hermes.md) | `hermes` | Sì | No |
| [Amp](../formats/amp.md) | `amp` | Sì | No |
| [Claude Chat](../formats/claude-chat.md) | `claude_chat` | Account online | No |
Expand Down
1 change: 1 addition & 0 deletions docs/translations/README.ja.md
Original file line number Diff line number Diff line change
Expand Up @@ -125,6 +125,7 @@ writeFileSync("rollout.jsonl", output);
| [Grok Bot](../formats/grok-bot.md) | `grok_bot` | 対応 | ローカルゲートウェイ経由 |
| [fx](../formats/fx.md) | `fx` | 対応 | 対応 |
| [Antigravity](../formats/antigravity.md) | `antigravity` | 対応 | 対応 |
| [uji](../formats/uji.md) | `uji` | 対応 | 対応 |
| [Hermes Agent](../formats/hermes.md) | `hermes` | 対応 | 非対応 |
| [Amp](../formats/amp.md) | `amp` | 対応 | 非対応 |
| [Claude Chat](../formats/claude-chat.md) | `claude_chat` | オンラインアカウント | 非対応 |
Expand Down
1 change: 1 addition & 0 deletions docs/translations/README.ko.md
Original file line number Diff line number Diff line change
Expand Up @@ -125,6 +125,7 @@ writeFileSync("rollout.jsonl", output);
| [Grok Bot](../formats/grok-bot.md) | `grok_bot` | 지원 | 로컬 게이트웨이 사용 |
| [fx](../formats/fx.md) | `fx` | 지원 | 지원 |
| [Antigravity](../formats/antigravity.md) | `antigravity` | 지원 | 지원 |
| [uji](../formats/uji.md) | `uji` | 지원 | 지원 |
| [Hermes Agent](../formats/hermes.md) | `hermes` | 지원 | 미지원 |
| [Amp](../formats/amp.md) | `amp` | 지원 | 미지원 |
| [Claude Chat](../formats/claude-chat.md) | `claude_chat` | 온라인 계정 | 미지원 |
Expand Down
1 change: 1 addition & 0 deletions docs/translations/README.mr.md
Original file line number Diff line number Diff line change
Expand Up @@ -125,6 +125,7 @@ writeFileSync("rollout.jsonl", output);
| [Grok Bot](../formats/grok-bot.md) | `grok_bot` | होय | स्थानिक गेटवेद्वारे |
| [fx](../formats/fx.md) | `fx` | होय | होय |
| [Antigravity](../formats/antigravity.md) | `antigravity` | होय | होय |
| [uji](../formats/uji.md) | `uji` | होय | होय |
| [Hermes Agent](../formats/hermes.md) | `hermes` | होय | नाही |
| [Amp](../formats/amp.md) | `amp` | होय | नाही |
| [Claude Chat](../formats/claude-chat.md) | `claude_chat` | ऑनलाइन खाते | नाही |
Expand Down
1 change: 1 addition & 0 deletions docs/translations/README.pt-BR.md
Original file line number Diff line number Diff line change
Expand Up @@ -125,6 +125,7 @@ Cada nome leva à documentação do respectivo formato. Use o ID com `--from` e
| [Grok Bot](../formats/grok-bot.md) | `grok_bot` | Sim | Via gateway local |
| [fx](../formats/fx.md) | `fx` | Sim | Sim |
| [Antigravity](../formats/antigravity.md) | `antigravity` | Sim | Sim |
| [uji](../formats/uji.md) | `uji` | Sim | Sim |
| [Hermes Agent](../formats/hermes.md) | `hermes` | Sim | Não |
| [Amp](../formats/amp.md) | `amp` | Sim | Não |
| [Claude Chat](../formats/claude-chat.md) | `claude_chat` | Conta online | Não |
Expand Down
1 change: 1 addition & 0 deletions docs/translations/README.ru.md
Original file line number Diff line number Diff line change
Expand Up @@ -125,6 +125,7 @@ writeFileSync("rollout.jsonl", output);
| [Grok Bot](../formats/grok-bot.md) | `grok_bot` | Да | Через локальный шлюз |
| [fx](../formats/fx.md) | `fx` | Да | Да |
| [Antigravity](../formats/antigravity.md) | `antigravity` | Да | Да |
| [uji](../formats/uji.md) | `uji` | Да | Да |
| [Hermes Agent](../formats/hermes.md) | `hermes` | Да | Нет |
| [Amp](../formats/amp.md) | `amp` | Да | Нет |
| [Claude Chat](../formats/claude-chat.md) | `claude_chat` | Онлайн-аккаунт | Нет |
Expand Down
1 change: 1 addition & 0 deletions docs/translations/README.ta.md
Original file line number Diff line number Diff line change
Expand Up @@ -125,6 +125,7 @@ writeFileSync("rollout.jsonl", output);
| [Grok Bot](../formats/grok-bot.md) | `grok_bot` | ஆம் | உள்ளூர் நுழைவாயில் வழியாக |
| [fx](../formats/fx.md) | `fx` | ஆம் | ஆம் |
| [Antigravity](../formats/antigravity.md) | `antigravity` | ஆம் | ஆம் |
| [uji](../formats/uji.md) | `uji` | ஆம் | ஆம் |
| [Hermes Agent](../formats/hermes.md) | `hermes` | ஆம் | இல்லை |
| [Amp](../formats/amp.md) | `amp` | ஆம் | இல்லை |
| [Claude Chat](../formats/claude-chat.md) | `claude_chat` | இணையக் கணக்கு | இல்லை |
Expand Down
1 change: 1 addition & 0 deletions docs/translations/README.zh-CN.md
Original file line number Diff line number Diff line change
Expand Up @@ -125,6 +125,7 @@ writeFileSync("rollout.jsonl", output);
| [Grok Bot](../formats/grok-bot.md) | `grok_bot` | 支持 | 通过本地网关 |
| [fx](../formats/fx.md) | `fx` | 支持 | 支持 |
| [Antigravity](../formats/antigravity.md) | `antigravity` | 支持 | 支持 |
| [uji](../formats/uji.md) | `uji` | 支持 | 支持 |
| [Hermes Agent](../formats/hermes.md) | `hermes` | 支持 | 不支持 |
| [Amp](../formats/amp.md) | `amp` | 支持 | 不支持 |
| [Claude Chat](../formats/claude-chat.md) | `claude_chat` | 在线账户 | 不支持 |
Expand Down
1 change: 1 addition & 0 deletions docs/translations/README.zh-TW.md
Original file line number Diff line number Diff line change
Expand Up @@ -125,6 +125,7 @@ writeFileSync("rollout.jsonl", output);
| [Grok Bot](../formats/grok-bot.md) | `grok_bot` | 支援 | 透過本機閘道 |
| [fx](../formats/fx.md) | `fx` | 支援 | 支援 |
| [Antigravity](../formats/antigravity.md) | `antigravity` | 支援 | 支援 |
| [uji](../formats/uji.md) | `uji` | 支援 | 支援 |
| [Hermes Agent](../formats/hermes.md) | `hermes` | 支援 | 不支援 |
| [Amp](../formats/amp.md) | `amp` | 支援 | 不支援 |
| [Claude Chat](../formats/claude-chat.md) | `claude_chat` | 線上帳號 | 不支援 |
Expand Down
7 changes: 4 additions & 3 deletions docs/usage.md
Original file line number Diff line number Diff line change
Expand Up @@ -120,13 +120,13 @@ txcript = "0.14"
# txcript = { version = "0.14", default-features = false }
```

Default features: `opencode` (the SQLite stores: OpenCode, both Cursors, Antigravity), `hermes`, `claude_chat`, `cowork_remote`, `chatgpt`, and `search`.
Default features: `opencode` (the SQLite stores: OpenCode, both Cursors, Antigravity), `hermes`, `uji`, `claude_chat`, `cowork_remote`, `chatgpt`, and `search`.

Three layers, smallest to largest:

- `Codec`: `to_common` / `from_common` per harness; `convert::<A, B>` chains them through the canonical model.
- `TextCodec`: `from_text` / `to_text` to parse and render a harness's native session text, no I/O.
- `Store`: discover/load/save against a real backend (session directories, or SQLite DBs for OpenCode, Hermes, both Cursors, and Antigravity).
- `Store`: discover/load/save against a real backend (session directories, or SQLite DBs for OpenCode, Hermes, uji, both Cursors, and Antigravity).

Convert in memory (no filesystem):

Expand Down Expand Up @@ -222,7 +222,7 @@ writeFileSync("session.jsonl", convert(input, "codex", "claude_code"));
const common = JSON.parse(toCommon(input, "codex")); // { meta, messages }
const pi = fromCommon(JSON.stringify(common), "pi");

harnesses(); // ["claude_code","claude_chat","cowork_remote","chatgpt","codex","opencode","pi","campfire","cursor","cursor_desktop","grok","grok_bot","fx","hermes","amp","antigravity","simple","cowork"]
harnesses(); // ["claude_code","claude_chat","cowork_remote","chatgpt","codex","opencode","pi","campfire","cursor","cursor_desktop","grok","grok_bot","fx","hermes","amp","antigravity","simple","cowork","uji"]
```

Text-in / text-out: `input` is the source harness's native session text and the result is the target's. Invalid harness names or unparseable input throw a JS `Error`.
Expand Down Expand Up @@ -258,6 +258,7 @@ const matches = JSON.parse(index.query(JSON.stringify({ pattern: "relay bug" }))
| `antigravity` | JSON dump of the conversation database, protobuf blobs hex-encoded |
| `simple` | the [Simple](formats/simple.md) interchange JSON document |
| `cowork` | JSON bundle of the session record, Claude Code transcript, and audit log |
| `uji` | the session row with its message rows, each row's `data` decoded |

To build the wasm from source instead:

Expand Down
1 change: 1 addition & 0 deletions src/harness/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,7 @@ pub mod hermes;
pub mod opencode;
pub mod pi;
pub mod simple;
pub mod uji;

pub(crate) mod jsonl;

Expand Down
Loading