diff --git a/AGENTS.md b/AGENTS.md
index 1e37766..e36cec6 100644
--- a/AGENTS.md
+++ b/AGENTS.md
@@ -2,7 +2,7 @@
### Issue tracker
-Issues live in GitHub Issues for `devlooped/whatsbox`. See `docs/agents/issue-tracker.md`.
+Issues live in GitHub Issues for `devlooped/Inbox`. See `docs/agents/issue-tracker.md`.
### Triage labels
diff --git a/changelog.md b/changelog.md
index cb4b23d..ab49f16 100644
--- a/changelog.md
+++ b/changelog.md
@@ -1,48 +1,48 @@
# Changelog
-## [v0.1.5](https://github.com/devlooped/whatsbox/tree/v0.1.5) (2026-08-20)
+## [v0.1.5](https://github.com/devlooped/Inbox/tree/v0.1.5) (2026-08-20)
-[Full Changelog](https://github.com/devlooped/whatsbox/compare/v0.1.4...v0.1.5)
+[Full Changelog](https://github.com/devlooped/Inbox/compare/v0.1.4...v0.1.5)
:sparkles: Implemented enhancements:
-- Subscribe is JID-only; fix directory identity and reply quotes [\#10](https://github.com/devlooped/whatsbox/pull/10) (@kzu)
+- Subscribe is JID-only; fix directory identity and reply quotes [\#10](https://github.com/devlooped/Inbox/pull/10) (@kzu)
:twisted_rightwards_arrows: Merged:
-- Pack WhatsDemo as the wd tool with pointer and RID packages [\#12](https://github.com/devlooped/whatsbox/pull/12) (@kzu)
+- Pack WhatsDemo as the wd tool with pointer and RID packages [\#12](https://github.com/devlooped/Inbox/pull/12) (@kzu)
-## [v0.1.4](https://github.com/devlooped/whatsbox/tree/v0.1.4) (2026-08-19)
+## [v0.1.4](https://github.com/devlooped/Inbox/tree/v0.1.4) (2026-08-19)
-[Full Changelog](https://github.com/devlooped/whatsbox/compare/v0.1.3...v0.1.4)
+[Full Changelog](https://github.com/devlooped/Inbox/compare/v0.1.3...v0.1.4)
-## [v0.1.3](https://github.com/devlooped/whatsbox/tree/v0.1.3) (2026-08-19)
+## [v0.1.3](https://github.com/devlooped/Inbox/tree/v0.1.3) (2026-08-19)
-[Full Changelog](https://github.com/devlooped/whatsbox/compare/v0.1.2...v0.1.3)
+[Full Changelog](https://github.com/devlooped/Inbox/compare/v0.1.2...v0.1.3)
:bug: Fixed bugs:
-- Stop double-emitting the first pairing QR [\#8](https://github.com/devlooped/whatsbox/pull/8) (@kzu)
+- Stop double-emitting the first pairing QR [\#8](https://github.com/devlooped/Inbox/pull/8) (@kzu)
-## [v0.1.2](https://github.com/devlooped/whatsbox/tree/v0.1.2) (2026-08-19)
+## [v0.1.2](https://github.com/devlooped/Inbox/tree/v0.1.2) (2026-08-19)
-[Full Changelog](https://github.com/devlooped/whatsbox/compare/v0.1.1...v0.1.2)
+[Full Changelog](https://github.com/devlooped/Inbox/compare/v0.1.1...v0.1.2)
:sparkles: Implemented enhancements:
-- Allow setting the WhatsApp linked-device name on initialize [\#5](https://github.com/devlooped/whatsbox/pull/5) (@kzu)
+- Allow setting the WhatsApp linked-device name on initialize [\#5](https://github.com/devlooped/Inbox/pull/5) (@kzu)
:twisted_rightwards_arrows: Merged:
-- Expand markdown includes in package readme [\#7](https://github.com/devlooped/whatsbox/pull/7) (@kzu)
+- Expand markdown includes in package readme [\#7](https://github.com/devlooped/Inbox/pull/7) (@kzu)
-## [v0.1.1](https://github.com/devlooped/whatsbox/tree/v0.1.1) (2026-08-19)
+## [v0.1.1](https://github.com/devlooped/Inbox/tree/v0.1.1) (2026-08-19)
-[Full Changelog](https://github.com/devlooped/whatsbox/compare/7f2763626f62021d3a2070b22dde229fd8522363...v0.1.1)
+[Full Changelog](https://github.com/devlooped/Inbox/compare/7f2763626f62021d3a2070b22dde229fd8522363...v0.1.1)
:sparkles: Implemented enhancements:
-- Create C\# client around the native whatsbox CLI [\#4](https://github.com/devlooped/whatsbox/pull/4) (@kzu)
+- Create C\# client around the native whatsbox CLI [\#4](https://github.com/devlooped/Inbox/pull/4) (@kzu)
diff --git a/docs/INBOX.md b/docs/INBOX.md
index d38c247..6831380 100644
--- a/docs/INBOX.md
+++ b/docs/INBOX.md
@@ -1,11 +1,11 @@
-# Inbox Protocol
+# Inbox Client Protocol (ICP)
**Status:** v0.1 (draft, session-locked with WhatsBox)
**Wire version:** `"0.1"`
**Transport:** JSON-RPC 2.0, NDJSON over stdio
**Reference profile:** WhatsApp / [`docs/WHATSBOX.md`](WHATSBOX.md)
-Inbox is a **single client-facing JSON-RPC 2.0 pub/sub bus** for local companion processes. One binary owns one messaging-product session and exposes it over stdin/stdout. Clients subscribe to chats and two system topics, send a small set of actions, and receive live events. It is **not** an archive, a search engine, or a product CLI.
+Inbox Client Protocol (**ICP**) is a **single client-facing JSON-RPC 2.0 pub/sub bus** for local companion processes. One binary owns one messaging-product session and exposes it over stdin/stdout. Clients subscribe to chats and two system topics, send a small set of actions, and receive live events. It is **not** an archive, a search engine, or a product CLI.
A client implements the methods, events, store/files rules, and error tokens in this document **once**. Product differences appear only as:
@@ -1425,9 +1425,9 @@ Recommended for new unifying clients. Gateways MUST still ignore unknown `contex
## 23. Relationship to WhatsBox
-This document is the **protocol**. [`docs/WHATSBOX.md`](WHATSBOX.md) is the **WhatsApp profile**: store layout, LID canonicalization, HistorySync headers, ContextInfo `remoteJid`, group `MarkRead` participant, and the shipped `whatsbox` binary. It does not restate the method table, event envelope, or error tokens.
+This document is **Inbox Client Protocol (ICP)** (also the managed `Inbox` client). [`docs/WHATSBOX.md`](WHATSBOX.md) is the **WhatsApp profile** for the native `whatsbox` adapter in this Inbox repository: store layout, LID canonicalization, HistorySync headers, ContextInfo `remoteJid`, group `MarkRead` participant, and the shipped `whatsbox` binary. The `WhatsBox` NuGet is the managed host on top of that adapter. This spec does not restate the method table, event envelope, or error tokens.
-A `whatsbox` binary is a conformant Inbox Protocol implementation when it speaks this envelope (including `product`, `identity`, and `capabilities` on `initialize` / `session.status`) and maps WhatsApp as in WHATSBOX.md + RFC-1.
+A `whatsbox` binary is a conformant ICP implementation when it speaks this envelope (including `product`, `identity`, and `capabilities` on `initialize` / `session.status`) and maps WhatsApp as in WHATSBOX.md + RFC-1.
Wire version stays **`"0.1"`** so one codec spans the family.
diff --git a/docs/WHATSBOX.md b/docs/WHATSBOX.md
index b85de76..e082d7a 100644
--- a/docs/WHATSBOX.md
+++ b/docs/WHATSBOX.md
@@ -1,17 +1,17 @@
# WhatsBox
-WhatsBox is **an Inbox Protocol implementation for WhatsApp**.
+WhatsBox is the **WhatsApp adapter** for Inbox Client Protocol (ICP): native binary `whatsbox`, managed NuGet `WhatsBox`.
**Status:** v0.1 (session-locked)
**License (product):** MIT
**Language:** Go
**Library:** [whatsmeow](https://github.com/tulir/whatsmeow)
-whatsbox is the **WhatsApp implementation** of [Inbox Protocol](INBOX.md): a local linked-device companion that owns one WhatsApp session and exposes it as a JSON-RPC 2.0 pub/sub bus over stdio.
+`whatsbox` is the native WhatsApp adapter in this Inbox repository: a local linked-device companion that owns one WhatsApp session and exposes it as a JSON-RPC 2.0 pub/sub bus over stdio. The `WhatsBox` NuGet is the managed host on top of that adapter.
Wire, methods, events, files, errors, client-once rules, and capabilities are specified in **INBOX.md**. This document is the WhatsApp profile: how that envelope maps onto WhatsApp Web (LIDs, QR pairing, ContextInfo, HistorySync headers, store layout). It does **not** restate the method table.
-`external/whatsmeow` and `external/wacli` in this workspace are **reference only**; whatsbox is a greenfield repo and does not share databases, packages, or command surface with wacli.
+`external/whatsmeow` and `external/wacli` in this workspace are **reference only**; the adapter does not share databases, packages, or command surface with wacli.
---
diff --git a/osmfeula.txt b/osmfeula.txt
index 414beb9..6444457 100644
--- a/osmfeula.txt
+++ b/osmfeula.txt
@@ -2,7 +2,7 @@ End User License Agreement
This Open Source Maintenance Fee Agreement ("Agreement") is a legal agreement
between you ("User") and Devlooped ("Project") for the use of
-whatsbox ("Software"), an open source software project licensed under
+Inbox ("Software"), an open source software project licensed under
the MIT License ("OSI License"), an OSI-approved open source license.
Project offers a Binary Release of the Software to Users in exchange for a
maintenance fee ("Fee"). "Binary Release" refers to pre-compiled executable
diff --git a/readme.md b/readme.md
index ee487c6..b2f68c8 100644
--- a/readme.md
+++ b/readme.md
@@ -1,4 +1,4 @@
- whatsbox
+ Inbox
============
[](https://www.nuget.org/packages/WhatsBox)
@@ -19,89 +19,49 @@ OSMF tier. A single fee covers all of [Devlooped packages](https://www.nuget.org
---
-
-whatsbox is **an Inbox Protocol implementation for WhatsApp**: one process
-owns a linked-device session and exposes it as a **JSON-RPC 2.0 pub/sub bus
-over stdio**. Clients subscribe to chats (and two system topics), send a
-small set of actions, and receive live events. It is not an archive, a
-search engine, or a WhatsApp CLI.
-
-The WhatsApp connection is powered by [whatsmeow](https://github.com/tulir/whatsmeow).
-
-Use the **managed client** from .NET, or speak the **native protocol** from any
-language that can spawn a process and exchange newline-delimited JSON.
-
-## Managed Client
-
-The [`WhatsBox`](https://www.nuget.org/packages/WhatsBox) package is a typed
-.NET host for the native `whatsbox` sidecar plus the managed **Inbox Protocol**
-client (`InboxClient`). `PackageReference` it, then publish for your RID —
-the matching native binary is restored and copied next to the app.
+# Inbox Client Protocol
+
+**Inbox Client Protocol (ICP)** is a **JSON-RPC 2.0 pub/sub bus over stdio** for local
+companion processes. One binary owns one messaging-product session. Clients
+subscribe to chats (and two system topics), send a small set of actions, and
+receive live events. It is not an archive, a search engine, or a product CLI.
+
+The [`Inbox`](https://www.nuget.org/packages/Inbox) package is the managed
+client (`InboxClient`) for **any** implementation of that protocol. Spawn a
+box, exchange newline-delimited JSON, and keep the process alive for the life
+of the session. Product differences are opaque topic strings, `$session` auth
+payloads, and `product` / `identity` / `capabilities` on `initialize`. There
+are no per-product method names.
+
+Wire version `"0.1"`: methods, events (`contents[]` on chat topics), files,
+errors, and capabilities — [`docs/INBOX.md`](docs/INBOX.md).
```xml
-
-```
-
-Target framework: `net10.0`. The managed surface is AOT-compatible (source-generated JSON).
-
-### Packaging and publish
-
-`WhatsBox` is a **pointer package**: it ships `WhatsBox.dll` plus a
-`runtime.json` that maps each runtime identifier to a RID-only package.
-
-| Package | Contents |
-|---|---|
-| `WhatsBox` | Managed API (`WhatsBox.dll`) and `runtime.json` |
-| `WhatsBox.win-x64` / `.win-arm64` / `.linux-x64` / `.linux-arm64` / `.osx-x64` / `.osx-arm64` | Native `whatsbox` / `whatsbox.exe` under `runtimes/{rid}/native/` |
-
-You only reference `WhatsBox`. Restore and `dotnet publish -r ` pull the
-matching `WhatsBox.{rid}` package automatically. The sidecar lands next to the
-app (`AppContext.BaseDirectory`); `WhatsBoxClient` starts it from there — never
-from the current working directory. The pointer package depends on [`Inbox`](https://www.nuget.org/packages/Inbox)
-(`InboxClient`); Inbox's RID packing targets are **not** transitive.
-
-Adapters for other products PackageReference `Inbox` directly (or ProjectReference
-it and import `Inbox.targets`) so pointer + `dotnet pack -r` packaging is shared.
-
-```bash
-dotnet add package WhatsBox
-dotnet publish -c Release -r win-x64
-```
-
-Do not add `WhatsBox.win-x64` (or any other RID package) by hand. Do not treat
-this as a .NET tool (`PackAsTool`); it is a `PackageReference` library plus a
-native asset.
-
-The companion REPL is a separate tool package (`wd`) with the
-same pointer + RID split:
-
-```bash
-dotnet tool install -g wd
-wd
+
```
-Supported RIDs: `win-x64`, `win-arm64`, `linux-x64`, `linux-arm64`, `osx-x64`,
-`osx-arm64`.
+Target framework: `net10.0`. The managed surface is AOT-compatible
+(source-generated JSON). Adapters that ship a native sidecar PackageReference
+`Inbox` so pointer +
+`dotnet pack -r` packaging is shared.
-### API
+## InboxClient
-`WhatsBoxClient` owns the child process, turns unary JSON-RPC methods into
-`Task`, and exposes a single-consumer pull stream of typed events.
-Disposing the client stops the sidecar.
+`InboxClient` turns unary JSON-RPC methods into `Task` and exposes a
+single-consumer pull stream of typed `InboxEvent`s. Construct it over an
+already-started NDJSON `TextReader`+`TextWriter` pair (the box’s stdout and
+stdin). Disposing the client completes `Events`.
Start consuming `Events` **before** (or concurrently with) a connecting
-`InitializeAsync`. Pairing QR codes arrive as events while that call is still
-waiting for a scan.
+`InitializeAsync`. Auth events (QR, OAuth, device-code, …) arrive on
+`$session` while that call is still waiting.
-```csharp
-using WhatsBox;
+WhatsBox-shaped illustration — the same types work for any box:
-var store = Path.GetFullPath("whatsbox-store");
-var files = Path.GetFullPath("whatsbox-files");
-Directory.CreateDirectory(store);
-Directory.CreateDirectory(files);
+```csharp
+using Inbox;
-await using var box = new WhatsBoxClient();
+await using var box = new InboxClient(stdout, stdin);
var pump = Task.Run(async () =>
{
@@ -110,7 +70,7 @@ var pump = Task.Run(async () =>
switch (ev)
{
case SessionQr qr:
- // Render qr.Code as a QR image and scan it in WhatsApp → Linked devices.
+ // Product auth: WhatsBox emits qr; other boxes may emit oauth, …
Console.WriteLine(qr.Code);
break;
case SessionOnline online:
@@ -122,7 +82,7 @@ var pump = Task.Run(async () =>
case DirectoryReady:
var page = await box.ListDirectoryAsync(new DirectoryListOptions { Kind = "user" });
foreach (var row in page.Items)
- Console.WriteLine($"{row.Name ?? row.Topic} {row.Pn}");
+ Console.WriteLine($"{row.Name ?? row.Topic}");
break;
case DirectoryUpsert upsert:
Console.WriteLine($"directory: {upsert.Name ?? upsert.Jid}");
@@ -146,20 +106,18 @@ var session = await box.InitializeAsync(new InitializeOptions
if (session.Status == "online")
{
- var listed = await box.ListDirectoryAsync(new DirectoryListOptions { Query = "+15551234567" });
+ var listed = await box.ListDirectoryAsync(new DirectoryListOptions { Query = "alice" });
var chat = listed.Items[0].Topic;
await box.SubscribeAsync([chat]);
- await box.SendAsync(chat, text: "hello from whatsbox");
+ await box.SendAsync(chat, text: "hello from Inbox");
}
await pump;
```
`InitializeAsync(store)` is the short form: no files, no extra subscriptions,
-no connect. The linked-device name defaults to `whatsbox on {machine}`. Pass
-`InitializeOptions` when you want blobs, initial topics, a custom
-`DeviceName`, or `Connect = true` (implicit `session.connect`, and implicit
-QR pairing when the store is new).
+no connect. Pass `InitializeOptions` for blobs, initial topics, or
+`Connect = true` (implicit `session.connect`).
| Method | RPC | Result |
|---|---|---|
@@ -169,20 +127,19 @@ QR pairing when the store is new).
| `DisconnectAsync` | `session.disconnect` | `SessionSnapshot` |
| `LogoutAsync` | `session.logout` | `SessionSnapshot` (`new`) |
| `StatusAsync` | `session.status` | `SessionSnapshot` |
-| `SubscribeAsync` / `UnsubscribeAsync` | `subscribe` / `unsubscribe` | `TopicsResult` (canonical JIDs) |
+| `SubscribeAsync` / `UnsubscribeAsync` | `subscribe` / `unsubscribe` | `TopicsResult` (canonical topics) |
| `ListDirectoryAsync` | `directory.list` | `DirectoryListResult` |
| `GetDirectoryAsync` | `directory.get` | `DirectoryRow` |
| `SendAsync` / `ReactAsync` | `messages.send` | `SendResult` (`Id`, canonical `Topic`) |
| `ReadAsync` | `messages.read` | `ReadResult` |
-`SessionSnapshot.Status` is `new` (never paired), `offline` (keys on disk,
-socket down), or `online`. `Me` is the paired LID and is omitted when `new`.
+`SessionSnapshot.Status` is `new` (never authenticated), `offline` (keys on
+disk, socket down), or `online`. `Me` is the authenticated identity and is
+omitted when `new`.
-`SubscribeAsync` / `UnsubscribeAsync` take canonical JIDs (LID, group, or
-PN JID). Resolve names and phone numbers with `ListDirectoryAsync` first.
-`SendAsync`, `ReadAsync`, and `GetDirectoryAsync` still accept a LID, a
-phone-number JID, or a phone number (`+15551234567` or digits). Results and
-event topics are always **canonical** (LID or group JID) once a LID is known.
+`SubscribeAsync` / `UnsubscribeAsync` take **canonical topics** only. Resolve
+names with `ListDirectoryAsync` first. Results and event topics are always
+canonical once the box knows them.
Send text (sugar), a file under `files`, a reply, and/or a reaction:
@@ -198,75 +155,218 @@ await box.ReactAsync(chat, target: id, by, "👍");
```
`by` is required on reply, react, and `ReadAsync` — copy it from the inbound
-event. Use `"me"` when targeting your own message. In 1:1 the sidecar ignores
-`by`; in groups every id in that call must share that author. Mark-read is
-never automatic.
+event. Use `"me"` when targeting your own message. Mark-read is never
+automatic.
-RPC failures throw `WhatsRpcException` with the JSON-RPC `Code` and a stable
+RPC failures throw `InboxRpcException` with the JSON-RPC `Code` and a stable
`Token` (`not_initialized`, `files_required`, `not_found`, …).
-### Events
+stderr is logs only. It is never protocol.
+
+## Events and contents
-`Events` is a single-consumer `IAsyncEnumerable`. Enumerate it
+`Events` is a single-consumer `IAsyncEnumerable`. Enumerate it
once. It completes when the child stdout ends or the client is disposed.
| Type | Topic | Kind |
|---|---|---|
-| `SessionQr` | `$session` | `qr` — string to render as QR |
+| `SessionQr` | `$session` | `qr` — string to render (WhatsBox illustration) |
| `SessionPaired` | `$session` | `paired` |
| `SessionPairError` | `$session` | `pair_error` |
| `SessionOnline` / `SessionOffline` | `$session` | `online` / `offline` |
| `SessionLoggedOut` | `$session` | `logged_out` |
-| `SessionRemap` | `$session` | `remap` — subscription moved PN → LID |
+| `SessionRemap` | `$session` | `remap` — subscription moved to a new canonical topic |
| `SessionOverflow` | `$session` | `overflow` — per-topic queue dropped oldest |
| `DirectoryUpsert` / `DirectoryRemove` / `DirectoryReady` | `$directory` | catalog changes |
-| `ChatMessage` | chat JID | `message` — `Contents` (text, media, location, unknown); `Text` concatenates text parts |
-| `ChatReaction` | chat JID | `reaction` — one reaction part |
-| `ChatAck` | chat JID | `ack` — `delivered` / `read` / `played` |
-| `ChatMeta` | chat JID | `meta` — join/leave/rename/… |
-
-Chat events share `Id`, `By` (`"me"` or a LID), `Handle` (`@username` when
-known), `TopicName`, `ByName`, and `Contents`. There is no phone number on
-chat events — look up `By` (or a 1:1 `Topic`) with `GetDirectoryAsync` when
-you need `Pn`.
-
-### Host
-
-`new WhatsBoxClient()` starts `whatsbox` / `whatsbox.exe` from
-`AppContext.BaseDirectory`. Use `WhatsBoxClient.Start(baseDirectory)` to
-point at another folder, or construct from an already-started `WhatsBoxHost`
-/ a raw NDJSON `TextReader`+`TextWriter` pair if you spawn the process
+| `ChatMessage` | chat topic | `message` — `Contents` (text, media, location, unknown); `Text` concatenates text parts |
+| `ChatReaction` | chat topic | `reaction` — one reaction part |
+| `ChatAck` | chat topic | `ack` — `delivered` / `read` / `played` |
+| `ChatMeta` | chat topic | `meta` — join/leave/rename/… |
+
+Chat events share `Id`, `By` (`"me"` or an opaque user id), `Handle`
+(`@username` when known), `TopicName`, `ByName`, and `Contents`. Look up `By`
+(or a 1:1 `Topic`) with `GetDirectoryAsync` for extra directory fields.
+
+Content parts: `text`, `image`, `video`, `audio`, `document`, `sticker`,
+`location`, `unknown`, plus `reaction` / `ack` / `meta` on those kinds.
+Blob parts carry a relative `Path` under `initialize.files`.
+
+## Implementing a box
+
+An implementation speaks this envelope on stdio (JSON-RPC 2.0, one object per
+line, no batch arrays). Advertise `product`, `identity`, and `capabilities` on
+`initialize` / `session.status`. Consumers construct `InboxClient` over that
+process — they never type-parse topic / `by` strings.
+
+Suggested binaries (not normative): `whatsbox`, `discordbox`, `slackbox`,
+`teamsbox`, `telegrambox`, `matrixbox`. Full method table, event shapes, and
+error tokens: [`docs/INBOX.md`](docs/INBOX.md).
+
+# WhatsBox
+
+The native **`whatsbox` adapter** implements Inbox Client Protocol (ICP) for WhatsApp —
+one process owns a linked-device session and exposes it on the bus. The
+[`WhatsBox`](https://www.nuget.org/packages/WhatsBox) NuGet is the managed
+host on top of that native adapter (it is not the protocol itself). Protocol,
+events, and `InboxClient`: the [`Inbox`](https://www.nuget.org/packages/Inbox)
+package.
+
+The WhatsApp connection is powered by [whatsmeow](https://github.com/tulir/whatsmeow);
+clients never talk to whatsmeow directly. WhatsApp-specific mapping (LID
+topics, QR pairing, ContextInfo quotes, HistorySync headers,
+`attachments: "single"`) is [`docs/WHATSBOX.md`](docs/WHATSBOX.md).
+
+## Managed host
+
+`WhatsBoxClient` is an `InboxClient` that starts the native `whatsbox` /
+`whatsbox.exe` sidecar. `PackageReference` it, then publish for your RID —
+the matching native binary is restored and copied next to the app.
+
+```xml
+
+```
+
+`WhatsBox` is a **pointer package**: it ships `WhatsBox.dll` plus a
+`runtime.json` that maps each runtime identifier to a RID-only package.
+
+| Package | Contents |
+|---|---|
+| `WhatsBox` | Managed API (`WhatsBox.dll`) and `runtime.json` |
+| `WhatsBox.win-x64` / `.win-arm64` / `.linux-x64` / `.linux-arm64` / `.osx-x64` / `.osx-arm64` | Native `whatsbox` / `whatsbox.exe` under `runtimes/{rid}/native/` |
+
+You only reference `WhatsBox`. Restore and `dotnet publish -r ` pull the
+matching `WhatsBox.{rid}` package automatically. The sidecar lands next to the
+app (`AppContext.BaseDirectory`); `WhatsBoxClient` starts it from there — never
+from the current working directory. Inbox's RID packing targets are **not**
+transitive.
+
+```bash
+dotnet add package WhatsBox
+dotnet publish -c Release -r win-x64
+```
+
+Do not add `WhatsBox.win-x64` (or any other RID package) by hand. Do not treat
+this as a .NET tool (`PackAsTool`); it is a `PackageReference` library plus a
+native asset.
+
+The companion sample REPL is a separate tool package (`wd`, for WhatsBox Demo) with the
+same pointer + RID split:
+
+```bash
+ndnx wd
+```
+
+> We recommend using [`ndnx`](https://github.com/devlooped/ndnx)
+> for fastest native-only execution. It's like dnx but native, with
+> no .NET runtime/SDK dependency.
+
+Supported RIDs: `win-x64`, `win-arm64`, `linux-x64`, `linux-arm64`, `osx-x64`,
+`osx-arm64`.
+
+## QR pairing and store
+
+`new WhatsBoxClient()` starts the sidecar from `AppContext.BaseDirectory`.
+Use `WhatsBoxClient.Start(baseDirectory)` to point at another folder, or
+construct from an already-started `WhatsBoxHost` if you spawn the process
yourself.
-stderr is logs only (`Debug.WriteLine` when the client owns the host). It is
-never protocol.
+Start consuming `Events` **before** (or concurrently with) a connecting
+`InitializeAsync`. Pairing QR codes arrive as `SessionQr` while that call is
+still waiting for a scan.
-## Native Protocol
+```csharp
+using WhatsBox;
-The `whatsbox` binary is a local companion process. Spawn it, speak
-**JSON-RPC 2.0** (NDJSON) on stdio, and keep the process alive for the life
-of the session. The wire is **Inbox Protocol** [`docs/INBOX.md`](docs/INBOX.md)
-version `"0.1"`: methods, events (`contents[]` on chat topics), files,
-errors, and capabilities. WhatsApp-specific mapping (LID topics, QR pairing,
-ContextInfo quotes, HistorySync headers, `attachments: "single"`) is
-[`docs/WHATSBOX.md`](docs/WHATSBOX.md).
+var store = Path.GetFullPath("whatsbox-store");
+var files = Path.GetFullPath("whatsbox-files");
+Directory.CreateDirectory(store);
+Directory.CreateDirectory(files);
+
+await using var box = new WhatsBoxClient();
+
+var pump = Task.Run(async () =>
+{
+ await foreach (var ev in box.Events)
+ {
+ switch (ev)
+ {
+ case SessionQr qr:
+ // Render qr.Code as a QR image and scan it in WhatsApp → Linked devices.
+ Console.WriteLine(qr.Code);
+ break;
+ case SessionOnline online:
+ Console.WriteLine($"online as {online.Me}");
+ break;
+ case ChatMessage msg:
+ Console.WriteLine($"{msg.ByName ?? msg.By}: {msg.Text}");
+ if (msg.Id is not null)
+ await box.ReadAsync(msg);
+ break;
+ }
+ }
+});
+
+var session = await box.InitializeAsync(new InitializeOptions
+{
+ Store = store,
+ Files = files,
+ Subscribe = ["$directory"],
+ Connect = true,
+});
+
+if (session.Status == "online")
+{
+ var listed = await box.ListDirectoryAsync(new DirectoryListOptions { Query = "+15551234567" });
+ var chat = listed.Items[0].Topic;
+ await box.SubscribeAsync([chat]);
+ await box.SendAsync(chat, text: "hello from whatsbox");
+}
+
+await pump;
+```
+
+`InitializeAsync(store)` is the short form. The linked-device name defaults to
+`whatsbox on {machine}`. Pass `InitializeOptions` when you want blobs, initial
+topics, a custom `DeviceName`, or `Connect = true` (implicit `session.connect`,
+and implicit QR pairing when the store is new).
+
+There is **no default store**. One process, one store, one WhatsApp session.
+`SubscribeAsync` / `UnsubscribeAsync` take canonical JIDs (LID, group, or PN
+JID). Resolve names and phone numbers with `ListDirectoryAsync` first.
+`SendAsync`, `ReadAsync`, and `GetDirectoryAsync` still accept a LID, a
+phone-number JID, or a phone number (`+15551234567` or digits). Results and
+event topics are always **canonical** (LID or group JID) once a LID is known.
-The WhatsApp Web socket behind it is
-[whatsmeow](https://github.com/tulir/whatsmeow); clients never talk to
-whatsmeow directly.
+> [JID](https://wiki.xmpp.org/web/JID_and_Contacts) or Jabber ID
+> is the canonical identifier for a WhatsApp chat. LID is a
+> logical identifier (like a username) that is stable across devices.
-### Invocation
+Chat events have no phone number — look up `By` (or a 1:1 `Topic`) with
+`GetDirectoryAsync` when you need `Pn`. In 1:1 the sidecar ignores `by` on
+reply / react / read; in groups every id in that call must share that author.
+
+## Native sidecar
```text
whatsbox [--store ABSOLUTE_PATH] [--version] [--help]
```
-stdin / stdout is NDJSON JSON-RPC; stderr is logs. No default store. One
-process, one store, one WhatsApp session. Full method table, event shapes,
-and error tokens: [`docs/INBOX.md`](docs/INBOX.md). LID / QR / store layout:
+stdin / stdout is NDJSON JSON-RPC; stderr is logs. LID / QR / store layout:
[`docs/WHATSBOX.md`](docs/WHATSBOX.md).
-
+### v1 scope
+
+**Does:** pair via QR, connect / auto-reconnect / disconnect / logout,
+directory populate + list/get + live `$directory`, subscribe by JID
+(LID-first), live messages / receipts / in-chat `meta`, send `contents[]`,
+reply, react, explicit mark-read.
+
+**Does not:** message history, search, backfill, export; stored bodies or
+last-message previews; typing or “available” presence; edit or revoke;
+pair-code or passkey pairing; channels, status, calls, blocklist or group
+admin RPCs; MCP / sockets; multi-account in one process; topic wildcards; a
+default store path.
+
## Demo
@@ -285,7 +385,7 @@ ndnx wd
```
`dnx` always goes through the SDK. `ndnx` starts the cached AOT binary
-directly — no SDK after the first download. Pin a version (`wd@1.0.0`)
+directly — no SDK needed at all. Pin a version (`wd@1.0.0`)
to skip latest-version lookup.
To install a `wd` command on PATH instead:
@@ -304,19 +404,6 @@ reuse that store.
-### v1 scope
-
-**Does:** pair via QR, connect / auto-reconnect / disconnect / logout,
-directory populate + list/get + live `$directory`, subscribe by JID
-(LID-first), live messages / receipts / in-chat `meta`, send `contents[]`,
-reply, react, explicit mark-read.
-
-**Does not:** message history, search, backfill, export; stored bodies or
-last-message previews; typing or “available” presence; edit or revoke;
-pair-code or passkey pairing; channels, status, calls, blocklist or group
-admin RPCs; MCP / sockets; multi-account in one process; topic wildcards; a
-default store path.
-
---
# Sponsors
diff --git a/src/Directory.props b/src/Directory.props
index 8a24802..6f4e4be 100644
--- a/src/Directory.props
+++ b/src/Directory.props
@@ -1,6 +1,6 @@
- whatsbox
+ Inbox
true
\ No newline at end of file
diff --git a/src/Inbox/Inbox.csproj b/src/Inbox/Inbox.csproj
index 2f6cfaf..bd68bb9 100644
--- a/src/Inbox/Inbox.csproj
+++ b/src/Inbox/Inbox.csproj
@@ -4,7 +4,7 @@
Inbox
Inbox
Inbox
- Managed reference client for an Inbox Protocol-implementation CLI (JSON-RPC 2.0, NDJSON over stdio), plus RID packing targets for native adapters.
+ Managed reference client for an Inbox Client Protocol (ICP) implementation CLI (JSON-RPC 2.0, NDJSON over stdio), plus RID packing targets for native adapters.
true
@@ -13,6 +13,7 @@
+
diff --git a/src/Inbox/readme.md b/src/Inbox/readme.md
index 624e9a5..ff1adce 100644
--- a/src/Inbox/readme.md
+++ b/src/Inbox/readme.md
@@ -1,14 +1,12 @@
The [`Inbox`](https://www.nuget.org/packages/Inbox) package is the managed
-**Inbox Protocol** client (`InboxClient`) plus non-transitive MSBuild targets
+**Inbox Client Protocol (ICP)** client (`InboxClient`) plus non-transitive MSBuild targets
for adapters that ship a native sidecar.
-```xml
-
-```
-
Apps that only want WhatsApp should PackageReference `WhatsBox` instead — these
targets are **not** transitive.
+
+
## Adapter packing
`Inbox.targets` is imported only by a **direct** reference (nupkg `build/`, not
diff --git a/src/Tests/InboxProtocolSpecTests.cs b/src/Tests/InboxProtocolSpecTests.cs
index cb716ae..1c64e77 100644
--- a/src/Tests/InboxProtocolSpecTests.cs
+++ b/src/Tests/InboxProtocolSpecTests.cs
@@ -1,7 +1,7 @@
namespace Tests;
///
-/// Structural checks on the shipped Inbox Protocol spec (docs/INBOX.md).
+/// Structural checks on the shipped Inbox Client Protocol (ICP) spec (docs/INBOX.md).
/// The artifact is the spec; this test drives that file on disk, not a reimplementation.
///
public class InboxProtocolSpecTests
@@ -30,7 +30,7 @@ static string LoadSpec()
public void Spec_is_non_empty_prose()
{
Assert.True(spec.Length > 8_000, $"spec too short: {spec.Length}");
- Assert.Contains("# Inbox Protocol", spec, StringComparison.Ordinal);
+ Assert.Contains("# Inbox Client Protocol (ICP)", spec, StringComparison.Ordinal);
Assert.Contains("JSON-RPC 2.0", spec, StringComparison.Ordinal);
}
diff --git a/src/Tests/ProjectIdentityDocsTests.cs b/src/Tests/ProjectIdentityDocsTests.cs
new file mode 100644
index 0000000..e4f6121
--- /dev/null
+++ b/src/Tests/ProjectIdentityDocsTests.cs
@@ -0,0 +1,222 @@
+namespace Tests;
+
+///
+/// Structural checks on shipped current-facing docs after the project/repo rename to Inbox.
+/// The artifacts are the markdown files; this test drives those files on disk.
+///
+public class ProjectIdentityDocsTests
+{
+ static readonly string Repo = FindRepoRoot();
+
+ [Fact]
+ public void Root_readme_names_the_project_Inbox_and_states_the_adapter_split()
+ {
+ var text = File.ReadAllText(Path.Combine(Repo, "readme.md"));
+ var heading = Heading(text);
+ var inbox = Fragment(text, "inbox");
+ var whatsbox = Fragment(text, "whatsbox");
+
+ Assert.Contains("Inbox", heading, StringComparison.Ordinal);
+ Assert.DoesNotContain("whatsbox", heading, StringComparison.OrdinalIgnoreCase);
+
+ Assert.True(
+ text.IndexOf("", StringComparison.Ordinal) <
+ text.IndexOf("", StringComparison.Ordinal),
+ "root readme must present #inbox before #whatsbox");
+
+ Assert.Contains("Inbox Client Protocol (ICP)", inbox, StringComparison.Ordinal);
+ Assert.Contains("`InboxClient`", inbox, StringComparison.Ordinal);
+ Assert.DoesNotContain("whatsbox is **an Inbox Protocol", inbox, StringComparison.Ordinal);
+
+ Assert.Contains("`whatsbox` adapter", whatsbox, StringComparison.Ordinal);
+ Assert.Contains("`WhatsBox`", whatsbox, StringComparison.Ordinal);
+ Assert.Contains("NuGet", whatsbox, StringComparison.Ordinal);
+ Assert.Contains("managed host", whatsbox, StringComparison.OrdinalIgnoreCase);
+ Assert.Contains("it is not the protocol itself", whatsbox, StringComparison.OrdinalIgnoreCase);
+ }
+
+ [Fact]
+ public void Source_and_issue_links_are_devlooped_Inbox()
+ {
+ var agents = File.ReadAllText(Path.Combine(Repo, "AGENTS.md"));
+ Assert.Contains("`devlooped/Inbox`", agents, StringComparison.Ordinal);
+ Assert.DoesNotContain("devlooped/whatsbox", agents, StringComparison.Ordinal);
+
+ var whatsBoxReadme = File.ReadAllText(Path.Combine(Repo, "src", "WhatsBox", "readme.md"));
+ Assert.Contains("https://github.com/devlooped/Inbox", whatsBoxReadme, StringComparison.Ordinal);
+ Assert.DoesNotContain("devlooped/whatsbox", whatsBoxReadme, StringComparison.Ordinal);
+
+ var inboxReadme = File.ReadAllText(Path.Combine(Repo, "src", "Inbox", "readme.md"));
+ Assert.Contains("PackageReference `WhatsBox`", inboxReadme, StringComparison.Ordinal);
+ Assert.Contains("readme.md#inbox", inboxReadme, StringComparison.Ordinal);
+ Assert.DoesNotContain("readme.md#content", inboxReadme, StringComparison.Ordinal);
+ Assert.DoesNotContain("readme.md#whatsbox", inboxReadme, StringComparison.Ordinal);
+
+ Assert.Contains("readme.md#whatsbox", whatsBoxReadme, StringComparison.Ordinal);
+ Assert.DoesNotContain("readme.md#content", whatsBoxReadme, StringComparison.Ordinal);
+ Assert.DoesNotContain("readme.md#inbox", whatsBoxReadme, StringComparison.Ordinal);
+ }
+
+ [Fact]
+ public void Inbox_fragment_is_box_agnostic_protocol_and_client()
+ {
+ var inbox = Fragment(File.ReadAllText(Path.Combine(Repo, "readme.md")), "inbox");
+
+ Assert.Contains("Inbox Client Protocol (ICP)", inbox, StringComparison.Ordinal);
+ Assert.Contains("`InboxClient`", inbox, StringComparison.Ordinal);
+ Assert.Contains("`InboxEvent`", inbox, StringComparison.Ordinal);
+ Assert.Contains("ChatMessage", inbox, StringComparison.Ordinal);
+ Assert.Contains("contents", inbox, StringComparison.OrdinalIgnoreCase);
+ Assert.Contains("Implementing a box", inbox, StringComparison.Ordinal);
+ Assert.Contains("new InboxClient(stdout, stdin)", inbox, StringComparison.Ordinal);
+
+ Assert.DoesNotContain("QR pairing", inbox, StringComparison.OrdinalIgnoreCase);
+ Assert.DoesNotContain("whatsbox sidecar", inbox, StringComparison.OrdinalIgnoreCase);
+ Assert.DoesNotContain("whatsbox.exe", inbox, StringComparison.OrdinalIgnoreCase);
+ Assert.DoesNotContain("WhatsBox.win-x64", inbox, StringComparison.Ordinal);
+ Assert.DoesNotContain("runtime.json", inbox, StringComparison.Ordinal);
+ Assert.DoesNotContain("WhatsBoxClient", inbox, StringComparison.Ordinal);
+ Assert.DoesNotContain("whatsmeow", inbox, StringComparison.OrdinalIgnoreCase);
+ }
+
+ [Fact]
+ public void WhatsBox_fragment_is_the_WhatsApp_adapter()
+ {
+ var whatsbox = Fragment(File.ReadAllText(Path.Combine(Repo, "readme.md")), "whatsbox");
+
+ Assert.Contains("QR pairing", whatsbox, StringComparison.Ordinal);
+ Assert.Contains("store", whatsbox, StringComparison.OrdinalIgnoreCase);
+ Assert.Contains("sidecar", whatsbox, StringComparison.OrdinalIgnoreCase);
+ Assert.Contains("whatsbox [--store ABSOLUTE_PATH]", whatsbox, StringComparison.Ordinal);
+ Assert.Contains("WhatsBoxClient", whatsbox, StringComparison.Ordinal);
+ Assert.Contains("https://www.nuget.org/packages/Inbox", whatsbox, StringComparison.Ordinal);
+ Assert.Contains("whatsmeow", whatsbox, StringComparison.OrdinalIgnoreCase);
+ }
+
+ [Fact]
+ public void Remaining_content_markers_are_only_the_WhatsDemo_include()
+ {
+ var text = File.ReadAllText(Path.Combine(Repo, "readme.md"));
+ var whatsboxClose = text.LastIndexOf("", StringComparison.Ordinal);
+ Assert.True(whatsboxClose > 0);
+
+ var i = 0;
+ var hits = 0;
+ while (true)
+ {
+ var at = text.IndexOf("", i, StringComparison.Ordinal);
+ if (at < 0)
+ break;
+ hits++;
+ Assert.True(at > whatsboxClose, " before #whatsbox close at " + at);
+ i = at + 1;
+ }
+
+ Assert.Equal(2, hits);
+ Assert.Contains("src/WhatsDemo/readme.md#content", text, StringComparison.Ordinal);
+ }
+
+ [Fact]
+ public void WhatsBox_profile_is_the_adapter_not_this_repository()
+ {
+ var text = File.ReadAllText(Path.Combine(Repo, "docs", "WHATSBOX.md"));
+ var firstRule = text.IndexOf("---", StringComparison.Ordinal);
+ Assert.True(firstRule > 0, "WHATSBOX.md intro missing horizontal rule");
+ var intro = text[..firstRule];
+
+ Assert.Contains("WhatsApp adapter", intro, StringComparison.Ordinal);
+ Assert.Contains("Inbox Client Protocol (ICP)", intro, StringComparison.Ordinal);
+ Assert.Contains("`whatsbox`", intro, StringComparison.Ordinal);
+ Assert.Contains("`WhatsBox` NuGet", intro, StringComparison.Ordinal);
+ Assert.DoesNotContain("greenfield repo", intro, StringComparison.Ordinal);
+ Assert.DoesNotContain("this repository is whatsbox", intro, StringComparison.OrdinalIgnoreCase);
+
+ Assert.Contains("whatsbox [--store ABSOLUTE_PATH]", text, StringComparison.Ordinal);
+ }
+
+ [Fact]
+ public void Osmf_software_name_is_Inbox()
+ {
+ var text = File.ReadAllText(Path.Combine(Repo, "osmfeula.txt"));
+ Assert.Contains("Inbox (\"Software\")", text, StringComparison.Ordinal);
+ Assert.DoesNotContain("whatsbox (\"Software\")", text, StringComparison.Ordinal);
+ }
+
+ [Fact]
+ public void Current_facing_docs_have_no_devlooped_whatsbox_links()
+ {
+ var hits = new List();
+ foreach (var file in CurrentFacingDocs())
+ {
+ var n = 0;
+ foreach (var line in File.ReadAllLines(file))
+ {
+ n++;
+ if (line.Contains("devlooped/whatsbox", StringComparison.Ordinal))
+ hits.Add($"{Path.GetRelativePath(Repo, file)}:{n}:{line.Trim()}");
+ }
+ }
+
+ Assert.True(hits.Count == 0, string.Join(Environment.NewLine, hits));
+ }
+
+ static string Heading(string readme)
+ {
+ var end = readme.IndexOf("============", StringComparison.Ordinal);
+ Assert.True(end > 0, "readme.md missing underline heading");
+ return readme[..end];
+ }
+
+ static string Fragment(string readme, string name)
+ {
+ var marker = "";
+ var start = readme.IndexOf(marker, StringComparison.Ordinal);
+ Assert.True(start >= 0, "readme.md missing opening " + marker);
+ start += marker.Length;
+ var end = readme.IndexOf(marker, start, StringComparison.Ordinal);
+ Assert.True(end > start, "readme.md missing closing " + marker);
+ return readme[start..end];
+ }
+
+ static IEnumerable CurrentFacingDocs()
+ {
+ yield return Path.Combine(Repo, "readme.md");
+ yield return Path.Combine(Repo, "AGENTS.md");
+ yield return Path.Combine(Repo, "osmfeula.txt");
+ yield return Path.Combine(Repo, "changelog.md");
+
+ foreach (var file in Directory.EnumerateFiles(Path.Combine(Repo, "src"), "readme.md", SearchOption.AllDirectories))
+ {
+ if (IsBuildOutput(file))
+ continue;
+ yield return file;
+ }
+
+ foreach (var file in Directory.EnumerateFiles(Path.Combine(Repo, "docs"), "*.md", SearchOption.AllDirectories))
+ {
+ if (string.Equals(Path.GetFileName(file), "RFC-1.md", StringComparison.OrdinalIgnoreCase))
+ continue;
+ yield return file;
+ }
+ }
+
+ static bool IsBuildOutput(string path)
+ {
+ var parts = path.Split(Path.DirectorySeparatorChar, Path.AltDirectorySeparatorChar);
+ return parts.Any(p => p.Equals("bin", StringComparison.OrdinalIgnoreCase)
+ || p.Equals("obj", StringComparison.OrdinalIgnoreCase));
+ }
+
+ static string FindRepoRoot()
+ {
+ var dir = new DirectoryInfo(AppContext.BaseDirectory);
+ while (dir is not null)
+ {
+ if (File.Exists(Path.Combine(dir.FullName, "WhatsBox.slnx")))
+ return dir.FullName;
+ dir = dir.Parent;
+ }
+
+ throw new InvalidOperationException("Could not find WhatsBox.slnx from " + AppContext.BaseDirectory);
+ }
+}
diff --git a/src/WhatsBox/WhatsBox.csproj b/src/WhatsBox/WhatsBox.csproj
index 30b1692..f5dc1da 100644
--- a/src/WhatsBox/WhatsBox.csproj
+++ b/src/WhatsBox/WhatsBox.csproj
@@ -4,7 +4,7 @@
WhatsBox
WhatsBox
WhatsBox
- WhatsApp Inbox Protocol host: PackageReference WhatsBox and publish for your RID to get the native whatsbox sidecar plus the managed Inbox client.
+ WhatsApp Inbox Client Protocol (ICP) host: PackageReference WhatsBox and publish for your RID to get the native whatsbox sidecar plus the managed Inbox client.
true
win-x64;win-arm64;linux-x64;linux-arm64;osx-x64;osx-arm64
true
diff --git a/src/WhatsBox/readme.md b/src/WhatsBox/readme.md
index 9b523d8..bcd5ded 100644
--- a/src/WhatsBox/readme.md
+++ b/src/WhatsBox/readme.md
@@ -1,8 +1,8 @@
[](osmfeula.txt)
[](license.txt)
-[](https://github.com/devlooped/whatsbox)
+[](https://github.com/devlooped/Inbox)
-
+