Transceiver spike: measures ArtifactData calls from a mod

<h1 align="center">Your team, inside Claude.</h1>
You chat with friends without leaving Claude Code, and on a GitHub repo you share, your agent can ask a teammate's side about the project: status, diff, tests, log, a file. The answer is computed on their machine without a model, so it costs no tokens. Everything you write stays in a private claude.ai artifact you own; there is no server of its own.
Beta. Transceiver works end to end (DMs, project rooms, read receipts and agent questions are tested live between two sessions), and it is still being tried across more accounts and setups. If something breaks, open an issue.
/plugin marketplace add partalemre/MODZ
/plugin install transceiver@modz
Then type /transceiver. The pane opens, and every step is a button there.
claude -p), the claude.ai Artifact tools are off by default. Add "CLAUDE_CODE_ARTIFACT": "1" to the env block of ~/.claude/settings.json and start a new session. The desktop app and the terminal do not need this.MODZ_LANG=tr for Turkish.A room is made of mailboxes, one per member. Only you write to yours, and the other members read it. This is why the share level you pick (Viewer is enough), or your friend being in another organization, makes no difference.
/transceiver join <link>). Joining opens their own mailbox.Messages arrive in about a second while the pane is open. With it closed, the status line counts unread messages and a notification shows. Under each message: ◷ sending, ✓ written, ✓✓ delivered, blue ✓✓ read. Turning read receipts off turns them off both ways.
Every command is also a button in the pane. The Turkish words (ekle, proje, katıl, çık, kur, ayarlar) work too.
| Command | What it does |
|---|---|
/transceiver | Opens the pane |
/transceiver add | Opens a DM room for a friend |
/transceiver project <owner/repo> | Opens a shared project room |
/transceiver join <link> | Joins a room by mailbox link, or adds the other side's mailbox to a room you are in |
/transceiver leave <room name or link> | Leaves a room; its polling stops |
/transceiver connect | Connects this session (one approval for the first message) |
/transceiver settings | Read receipts, notifications, your GitHub name |
They work only in a project room, and only for the repo the session is open in. Agents never write to DMs.
send sends a plain-text message to the project room. Text that holds a secret is refused.inbox reads the room's latest messages. They come from someone else, so the agent gets them fenced as data, not instructions.ask asks the teammate's side a question and waits a few seconds. If the answer is late it returns pending:<id>, and inbox reads the answer when it arrives.ask type | Answer |
|---|---|
status | Branch and number of changed files |
branch | Branches |
log | The last 20 commits |
diff | Uncommitted changes, up to 64 KB |
tests | The last test result (from test-badge) |
plan | The plan's steps (from plan-steps) |
file | One file, up to 256 KB |
grep | Lines that contain a text, optionally in one folder |
files | File names, optionally in one folder |
todo | TODO and FIXME lines |
deps | Dependencies |
commit | One commit |
ask | A free-form question, answered on their side by a small model (Haiku) |
Every type but ask is answered without a model and costs zero tokens.
.env*, *.pem, *.key, id_*, .git/, .ssh/ and everything .gitignore covers are refused.Messaging, polling and model-free questions use no model tokens: the mod calls Claude Code's tools directly, outside any model turn.
ask. It is answered with Haiku on the other side, at most ten a day per room. The eleventh waits there as a draft, and nothing is generated until its owner presses Send on it.| You see | Do this |
|---|---|
| No invite card | Invites from another organization are not listed. Ask for the link and use I have a link. If that says the link is not a Transceiver mailbox, the share was not completed. |
| "… hasn't shared their mailbox with you yet" | They have not shared their mailbox with you yet. They add you from Share and, across organizations, send you its link. |
| "Artifact tools are off in this session" | The session runs in an SDK host. Set CLAUDE_CODE_ARTIFACT=1 as under Install and reopen the session. |
| "waiting for connection" | This session has not connected yet. Press Connect, or type /transceiver connect. |
| Every question gets "no access" | Check that you answered yes on the GitHub card, that they are a collaborator on the repo, and that both sessions are in it. The gate never says which check failed, by design. |
| Names show as "Someone" | The mailbox could not read the profile name, or the person is in another organization. Messages are not affected. |
| "Mailbox full" notice | Your new mailbox is open. Share its link again from the Share menu. |
claude plugin validate . checks the plugin, and claude plugin test . runs the tests. tsc -p . type-checks it against the types Claude Code writes into .claude-plugin/types/ when it hot-reloads the mod.python3 -I bin/e2e.py runs the whole flow live between two sessions with separate stores: DM, read receipts, project room, GitHub match and an agent question. It uses MODZ's tools/desk driver and creates private test mailboxes in your account.MIT.
hooks/register.ts 48 lines1import type { EngineInterface, Register } from 'claude-code'
2
3const ROUNDS = 5
4
5type Sample = { ms: number; keys: string[]; hasText: boolean; isError: boolean; deny?: string; head: string; resultType: string; error?: string }
6
7async function timed($: EngineInterface, input: Record<string, unknown>): Promise<Sample> {
8 const start = await $.clock.now()
9 try {
10 const out = (await $.tool.call(input as never)) as Record<string, unknown>
11 const ms = (await $.clock.now()) - start
12 const text = typeof out.text === 'string' ? out.text : ''
13 return {
14 ms, keys: Object.keys(out), hasText: text.length > 0, isError: out.isError === true,
15 deny: typeof out.deny === 'string' ? out.deny : undefined, head: text.slice(0, 600),
16 resultType: JSON.stringify(out.result)?.slice(0, 600) ?? 'undefined',
17 }
18 } catch (err) {
19 return { ms: (await $.clock.now()) - start, keys: [], hasText: false, isError: true, head: '', resultType: '', error: String(err) }
20 }
21}
22
23async function probe($: EngineInterface): Promise<void> {
24 const url = (await $.env.get('TX_PROBE_URL')) ?? ''
25 const out = (await $.env.get('TX_PROBE_OUT')) ?? ''
26 if (url === '' || out === '') return
27 const reads: Sample[] = []
28 for (let i = 0; i < ROUNDS; i++) reads.push(await timed($, { tool: 'ArtifactData', action: 'list', url, collection: 'outbox' }))
29 const whoami = await timed($, { tool: 'ArtifactData', action: 'list', url, collection: 'data/users/me', query: { limit: 1 } })
30 const query = await timed($, { tool: 'ArtifactData', action: 'query', url, collection: 'outbox/u_OILIt9wuDiBBxrdcLpQjLA/msgs', query: { where: [['ts', '>', 0]], order_by: { field: 'ts', direction: 'asc' }, limit: 50 } })
31 const profiles = await timed($, { tool: 'ArtifactData', action: 'profiles', url, ids: ['u_OILIt9wuDiBBxrdcLpQjLA'] })
32 const shared = await timed($, { tool: 'Artifact', action: 'list', scope: 'shared', limit: 20 })
33 const write = await timed($, { tool: 'ArtifactData', action: 'batch', url, writes: [{ op: 'set', collection: 'outbox/u_OILIt9wuDiBBxrdcLpQjLA/claims', doc_id: `probe${await $.clock.now()}`, data: { v: 1, at: 1 } }] })
34 const publish = await timed($, { tool: 'Artifact', action: 'list', scope: 'mine', limit: 1 })
35 let wake = 'skipped'
36 if ((await $.env.get('TX_PROBE_WAKE')) === '1') {
37 wake = await $.session.send({ to: { sessionId: await $.session.id() }, text: 'tx-probe: self wake test' }).then(r => JSON.stringify(r), e => `rejected: ${String(e)}`)
38 }
39 await $.fs.write(out, JSON.stringify({ reads, whoami, query, profiles, shared, write, publish, wake }, null, 2))
40}
41
42export const register: Register = on => {
43 on('session.start', async ($, e, next) => {
44 await probe($)
45 return next(e)
46 })
47}
48