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
74 changes: 72 additions & 2 deletions CLAUDE.md
Original file line number Diff line number Diff line change
Expand Up @@ -10,14 +10,84 @@ Make "talk to the WaveKat platform from Rust" a solved problem so each consumer

- `Client` — reqwest-backed HTTP client with bearer auth attached.
- Loopback OAuth handshake (`platform.wavekat.com/cli-login` → loopback `127.0.0.1:<ephemeral>/callback`).
- Typed wrappers for stable platform endpoints used by multiple consumers (`/api/me`, token revoke, artifact upload).
- Typed wrappers for stable platform endpoints (`/api/me`, token revoke, artifact upload).
- **Platform sync endpoints** that upload/read user-owned resources. See "Platform sync endpoints belong here" below.
- Error types covering network, deserialization, auth-state mismatches.

## Platform sync endpoints belong here

Any endpoint pair under `/api/voice/{resource}/{sync,list}` — or any
future equivalent that uploads a user-owned resource from a client to
the platform and reads it back — is implemented in this crate as a
`SyncEndpoint` marker, **even if only one consumer uses it today**.

Reason: every WaveKat client (desktop daemon, CLI, future agents)
ships against the same platform. Making each consumer reinvent the
upload pipeline (batching, retries, cursor pagination, error mapping)
guarantees drift — subtly different batch sizes, different envelope
shapes, different idempotency keys. The bridge crate exists to stop
that.

Concretely: when you add a new sync-able resource (calls today;
recordings, transcripts, summaries later), you land:

- A `SyncEndpoint` impl on a zero-sized marker type (`VoiceCalls`,
`VoiceRecordings`, …).
- The wire-shape `Record` + `Query` types alongside it.
- The platform-side migration + routes in `wavekat-platform`.

Consumers then call `client.sync::<VoiceCalls>(items)` /
`client.list::<VoiceCalls>(query)` — they don't write a new HTTP
pipeline.

### Every record carries a `SyncEnvelope`

Each `SyncEndpoint::Record` type embeds `SyncEnvelope` via
`#[serde(flatten)]` and implements `HasSyncEnvelope`:

```rust
#[derive(Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct MyResourceRecord {
/* resource-specific fields */

#[serde(flatten, default)]
pub envelope: SyncEnvelope,
}

impl HasSyncEnvelope for MyResourceRecord {
fn envelope_mut(&mut self) -> &mut SyncEnvelope { &mut self.envelope }
}
```

The envelope contributes two fields to the wire:

- `schemaVersion` — auto-stamped by `Client::sync` from
`SyncEndpoint::CURRENT_SCHEMA_VERSION` when the consumer leaves
it `None`. Lets the platform branch on which client wrote the
row when an additive field change isn't enough.
- `extras` — free-form JSON for fields a newer client knows about
but this platform version doesn't yet have a typed column for.
The platform persists `extras` verbatim so a future deploy can
promote a field out of it into a typed column without data loss.

### Wire schemas are additive-only

When you add a field to a `Record`, make it optional with a serde
default (`#[serde(default, skip_serializing_if = "Option::is_none")]`
on `Option<…>`). When you remove one, deprecate first, ignore for a
release, then drop. When the *meaning* of a field needs to change,
introduce a new name and bump `CURRENT_SCHEMA_VERSION`. Hard schema
breaks ship as a new endpoint pair under a new `RESOURCE` segment.

Design rationale and the calls-first slice that established the
pattern: see [`wavekat-voice/docs/21-platform-call-history-sync.md`](https://github.com/wavekat/wavekat-voice/blob/main/docs/21-platform-call-history-sync.md).

## What does NOT belong here

- **Credential storage policy.** Consumers pick: `wavekat-cli` writes a JSON file at `~/.config/wavekat/auth.json`; `wavekat-voice` uses the OS keychain via the `keyring` crate. The crate's surface takes a `token: String` and returns one — it never reads or writes disk.
- **CLI-shaped concerns.** Argument parsing, terminal rendering, progress bars, anything `clap`/`unicode-width` shaped. Those stay in `wavekat-cli`.
- **Consumer-specific endpoints.** If only one product calls it, it stays in that product. Promote to this crate when a second consumer needs it.
- **Truly one-off endpoints.** Things one client genuinely needs and no other client ever will — a CLI-only `wk doctor` debug dump, the desktop-only loopback OAuth callback handler, etc. Sync endpoints are *not* in this bucket: default to landing them here unless you can articulate why no other client will ever want them.
- **Async runtime.** Use `reqwest` async; let consumers bring tokio.

## Design principles
Expand Down
7 changes: 7 additions & 0 deletions crates/wavekat-platform-client/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -40,10 +40,17 @@ mod client;
mod error;
mod me;
mod oauth;
mod sync;
mod token;
mod voice;

pub use client::Client;
pub use error::{Error, Result};
pub use me::Me;
pub use oauth::{loopback_handshake, HandshakeOptions, HandshakeOutcome, PendingHandshake};
pub use sync::{HasSyncEnvelope, Page, SyncEndpoint, SyncEnvelope, SyncRequest, SyncResponse};
pub use token::Token;
pub use voice::{
VoiceCallDirection, VoiceCallDisposition, VoiceCallEndReason, VoiceCallRecord, VoiceCalls,
VoiceCallsQuery,
};
Loading