Skip to content
Merged
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
12 changes: 12 additions & 0 deletions .dockerignore
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
.git
.github
.scratch
logs
target
worktree

**/node_modules
**/.next
**/out

gateway/bin
1 change: 1 addition & 0 deletions Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -3,4 +3,5 @@ resolver = "2"
members = [
"server",
"clients/rust",
"cxtx",
]
8 changes: 6 additions & 2 deletions Dockerfile
Original file line number Diff line number Diff line change
Expand Up @@ -36,17 +36,21 @@ WORKDIR /app
COPY Cargo.toml Cargo.lock* ./
COPY server/Cargo.toml ./server/
COPY clients/rust/Cargo.toml ./clients/rust/
COPY cxtx/Cargo.toml ./cxtx/

# Create dummy sources to build dependencies
RUN mkdir -p server/src clients/rust/src && \
RUN mkdir -p server/src clients/rust/src cxtx/src && \
echo "fn main() {}" > server/src/main.rs && \
echo "pub fn dummy() {}" > clients/rust/src/lib.rs && \
echo "pub fn dummy() {}" > cxtx/src/lib.rs && \
echo "fn main() {}" > cxtx/src/main.rs && \
cargo build --release --manifest-path server/Cargo.toml && \
rm -rf server/src clients/rust/src
rm -rf server/src clients/rust/src cxtx/src

# Copy actual source and build
COPY server/ ./server/
COPY clients/ ./clients/
COPY cxtx/ ./cxtx/
RUN touch server/src/main.rs && \
cargo build --release --manifest-path server/Cargo.toml

Expand Down
11 changes: 10 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@ Built on a Turn DAG + Blob CAS architecture, CXDB gives you:
- **Content deduplication**: Identical payloads stored once via BLAKE3 hashing
- **Type-safe projections**: Msgpack storage with typed JSON views for UIs
- **Built-in UI**: React frontend with turn visualization and custom renderers
- **First-party CLI capture**: `cxtx` wraps `codex` and `claude` sessions and stores them as canonical conversation turns

## Quick Start

Expand Down Expand Up @@ -36,6 +37,13 @@ curl -X POST http://localhost:9010/v1/contexts/1/append \
open http://localhost:9010
```

Capture a local `codex` or `claude` session into CXDB with the first-party wrapper:

```bash
cargo run -p cxtx -- --help
cargo run -p cxtx -- codex -- --help
```

## Installation

### From Source
Expand All @@ -54,7 +62,7 @@ cd cxdb
cargo build --release

# Run the server
./target/release/ai-cxdb-store
./target/release/cxdb-server

# Build the gateway (optional - for OAuth and frontend serving)
cd gateway
Expand Down Expand Up @@ -201,6 +209,7 @@ CXDB is a three-tier system:
- **HTTP API**: [docs/http-api.md](docs/http-api.md)
- **Type Registry**: [docs/type-registry.md](docs/type-registry.md)
- **Renderers**: [docs/renderers.md](docs/renderers.md)
- **CLI Wrapper (`cxtx`)**: [cxtx/README.md](cxtx/README.md)
- **Deployment**: [docs/deployment.md](docs/deployment.md)
- **Troubleshooting**: [docs/troubleshooting.md](docs/troubleshooting.md)
- **Development**: [docs/development.md](docs/development.md)
Expand Down
31 changes: 31 additions & 0 deletions cxtx/Cargo.toml
Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@
[package]
name = "cxtx"
version = "0.1.0"
edition = "2021"
license = "Apache-2.0"
description = "CLI wrapper that captures claude/codex provider traffic and uploads canonical conversation context into CXDB"

[dependencies]
anyhow = "1.0"
async-stream = "0.3"
axum = { version = "0.7", features = ["macros"] }
base64 = "0.22"
bytes = "1.10"
chrono = { version = "0.4", features = ["clock", "serde"] }
clap = { version = "4.5", features = ["derive", "env"] }
cxdb = { path = "../clients/rust" }
futures-util = "0.3"
http = "1.3"
reqwest = { version = "0.12", default-features = false, features = ["json", "rustls-tls", "stream"] }
serde = { version = "1.0", features = ["derive"] }
serde_json = "1.0"
tokio = { version = "1", features = ["fs", "io-util", "macros", "net", "process", "rt-multi-thread", "signal", "sync", "time"] }
url = "2.5"
uuid = { version = "1.18", features = ["v4"] }

[dev-dependencies]
assert_cmd = "2.0"
cxdb-server = { path = "../server" }
predicates = "3.1"
rmpv = "1.3"
tempfile = "3.10"
55 changes: 55 additions & 0 deletions cxtx/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,55 @@
# `cxtx`

`cxtx` wraps `codex` or `claude`, routes provider traffic through a local reverse proxy, uploads canonical `cxdb.ConversationItem` turns into CXDB, and keeps raw provider evidence under `.scratch/cxtx/sessions/`.

## Prerequisites

- A running CXDB HTTP endpoint, typically `http://127.0.0.1:9010`
- Either the `codex` or `claude` CLI installed and discoverable on `PATH`
- Existing provider credentials for the child CLI in your environment

## Build

```bash
cargo build --release -p cxtx
```

## Usage

```bash
# Wrap Codex and send captured turns to the local CXDB HTTP endpoint
./target/release/cxtx codex -- --model gpt-5

# Wrap Claude against a specific CXDB server
./target/release/cxtx --url http://127.0.0.1:9010 claude -- --print stream
```

`cxtx` preserves child stdin, stdout, stderr, and exit status. On successful execution it does not write wrapper-authored stdout. If CXDB is unavailable, it still launches the child, enters queued-delivery mode, and records delivery state in the local ledger until delivery recovers or shutdown drain completes.

## Resulting Artifacts

- CXDB receives canonical `system`, `user_input`, `assistant_turn`, and tool-related items for the wrapped session.
- The first uploaded turn carries `ContextMetadata` and `Provenance`, so the context is queryable in CXDB listings.
- `cxtx` publishes the bundled canonical `cxdb.ConversationItem` registry descriptor automatically before the first append when the server does not already have it.
- Local evidence is written under `.scratch/cxtx/sessions/<stable-session-id>/`:
- `ledger.json`
- `exchanges/<exchange-id>/request.json`
- `exchanges/<exchange-id>/response.json`
- `exchanges/<exchange-id>/stream.ndjson`

## Troubleshooting

- `failed to launch codex` or `failed to launch claude`:
- The child binary is missing from `PATH` or is not executable.
- `cxtx: CXDB ingest unavailable, entering queued-delivery mode`:
- The wrapper could not reach the configured `--url`. Check the CXDB server, but the child session is still running and the ledger will show queue state.
- No captured turns appear in CXDB:
- Confirm the child CLI honors the injected provider base URL variables. `cxtx` depends on those environment overrides for transparent capture.

## Verification

```bash
cargo run -p cxtx -- --help
cargo test -p cxtx
cargo test -p cxtx --test integration
```
63 changes: 63 additions & 0 deletions cxtx/src/cli.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,63 @@
use clap::{Parser, Subcommand};

use crate::provider::ProviderKind;

#[derive(Debug, Clone, Parser)]
#[command(
name = "cxtx",
about = "Wrap claude or codex, capture provider traffic, and upload canonical conversation context to CXDB",
after_help = "Examples:\n cxtx codex -- --model gpt-5\n cxtx --url http://127.0.0.1:9010 claude -- --print stream"
)]
pub struct Cli {
#[arg(
long,
default_value = "http://127.0.0.1:9010",
help = "CXDB HTTP base URL used for registry publication, context creation, and turn append"
)]
pub url: String,

#[command(subcommand)]
pub command: Command,
}

#[derive(Debug, Clone, Subcommand)]
pub enum Command {
/// Launch the `claude` CLI through a local Anthropic-aware capture proxy.
Claude {
#[arg(trailing_var_arg = true, allow_hyphen_values = true)]
args: Vec<String>,
},
/// Launch the `codex` CLI through a local OpenAI-aware capture proxy.
Codex {
#[arg(trailing_var_arg = true, allow_hyphen_values = true)]
args: Vec<String>,
},
}

impl Command {
pub fn provider(&self) -> ProviderKind {
match self {
Self::Claude { .. } => ProviderKind::Claude,
Self::Codex { .. } => ProviderKind::Codex,
}
}

pub fn args(&self) -> &[String] {
match self {
Self::Claude { args } | Self::Codex { args } => args,
}
}
}

impl Cli {
pub fn for_tests(provider: ProviderKind, args: Vec<String>, url: &str) -> Self {
let command = match provider {
ProviderKind::Claude => Command::Claude { args },
ProviderKind::Codex => Command::Codex { args },
};
Self {
url: url.to_string(),
command,
}
}
}
Loading
Loading