Skip to content
4 changes: 3 additions & 1 deletion .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,9 @@ pikachat-openclaw/.state/
pikachat-openclaw/node_modules/
pikachat-openclaw/**/node_modules/
pikachat-openclaw/**/package-lock.json
pikachat-claude/node_modules/
pikachat-claude/package-lock.json
pikachat-claude/dist/
pikachat-openclaw/.env
pikachat-openclaw/.env.*

Expand Down Expand Up @@ -91,4 +94,3 @@ cmd/pika-relay/pika-relay
.pika-fixture/
.pikaci/
.pikahut-agent-platform-local/

193 changes: 193 additions & 0 deletions docs/claude-channel-plugin-brief.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,193 @@
---
summary: Implementation brief for a Claude Code channel plugin backed by pikachat daemon
read_when:
- building or extending the pikachat Claude plugin
- reviewing channel/plugin transport and access design
---

# Pikachat Claude Channel Plugin Brief

## Goal

Build a Claude Code channel plugin backed by `pikachat daemon`.

The plugin should expose Pika MLS chats to Claude through the Claude channel contract:

- inbound chat messages arrive as `notifications/claude/channel`
- Claude replies via ordinary MCP tools
- sender gating prevents prompt injection

This plugin is a host wrapper around `pikachat daemon`, not a replacement for the daemon protocol.

## Acceptance

- DM routing works
- approved 1:1 senders reach Claude as channel events
- Claude can reply into the same DM
- Group routing works
- groups are explicitly enabled
- sender allowlists apply to senders, not rooms
- `requireMention: true` is the default for groups
- Pairing and allowlist exist
- unknown DM senders get a pairing code
- approval adds the sender to the allowlist
- Reply, react, and file send work through MCP tools
- Inbound attachments are surfaced with local paths when available
- Local relay e2e proves:
- remote Pika message
- Claude channel notification
- Claude reply tool call
- remote side receives the reply

## Constraints

- Reuse the TypeScript launcher/client patterns from `pikachat-openclaw`
- Avoid direct SQLite reads unless necessary
- Ask before changing the daemon protocol
- Treat `edit_message` as non-MVP unless a native model emerges

## Existing Reusable Surfaces

The daemon already exposes the main surfaces needed for an MVP:

- `send_message`
- `send_media`
- `send_media_batch`
- `react`
- `send_typing`
- `list_groups`
- `list_members`
- `get_messages`
- `message_received`
- `group_joined`
- `group_created`
- `group_updated`

Relevant references:

- `crates/pikachat-sidecar/src/protocol.rs`
- `crates/pikachat-sidecar/src/daemon.rs`
- `pikachat-openclaw/openclaw/extensions/pikachat-openclaw/src/sidecar.ts`
- `pikachat-openclaw/openclaw/extensions/pikachat-openclaw/src/daemon-launch.ts`
- `pikachat-openclaw/openclaw/extensions/pikachat-openclaw/src/sidecar-install.ts`

## Architecture

- `pikachat daemon` remains the transport/backend child process
- the Claude plugin process is the MCP stdio server
- the plugin:
- launches the daemon
- consumes daemon JSONL events
- applies DM/group access policy
- emits Claude channel notifications
- exposes reply/react/file-send/admin tools

## Notification Contract

Each inbound message becomes a Claude channel event with:

- `content`
- message text
- attachment summary lines with absolute local paths when present
- `meta`
- `chat_id`
- `sender_id`
- `sender_name`
- `message_id`
- `event_id`
- `chat_type`
- `group_name`
- `mentioned`

The server instructions should tell Claude:

- inbound messages arrive as `<channel source="pikachat" ...>`
- use `reply` with the `chat_id` from the tag
- use `react` with the `event_id` from the tag

## Access Model

Store state in `~/.claude/channels/pikachat/access.json`.

Schema:

```json
{
"dmPolicy": "pairing",
"allowFrom": [],
"groups": {},
"mentionPatterns": [],
"pendingPairings": {}
}
```

Rules:

- DM:
- `pairing`: unknown sender gets a code, message is dropped
- `allowlist`: unknown sender is dropped
- `disabled`: all DM traffic is dropped
- Group:
- group must be explicitly enabled
- per-group `allowFrom` is optional
- `requireMention` defaults to `true`

## Phases

### 1. MCP wrapper

- create plugin directory with `.claude-plugin/plugin.json` and `.mcp.json`
- bundle a stdio MCP server for runtime use
- reuse daemon launch/install/client logic from `pikachat-openclaw`
- align the TypeScript protocol mirror with the current Rust protocol

### 2. Access model

- implement `access.json`
- pairing lifecycle
- DM and group gating
- mention detection

### 3. Parity gaps

Close host-side gaps first:

- add TypeScript wrappers for `list_members`, `get_messages`, `send_media_batch`, and `group_updated`
- classify DM vs group via daemon metadata and cached member counts
- avoid SQLite reads

Escalate before daemon changes for:

- native `reply_to`
- richer history pagination/search
- explicit historical attachment fetch/download

### 4. Packaging and tests

- deterministic Node tests for access, routing, and formatting
- local relay e2e using real `pikachat daemon`
- plugin README with local dev instructions

## Evaluation Design

Deterministic tests should cover:

- pairing code lifecycle
- DM policy decisions
- group allowlist and mention gating
- notification/meta shaping
- attachment text augmentation
- tool-to-daemon command mapping

Local relay e2e should prove:

1. a remote Pika user sends a DM through a local relay
2. the Claude plugin emits a channel notification
3. the test calls the plugin reply path
4. the remote user receives the reply

## Open Questions / Explicit Non-Goals

- `edit_message` is out of scope for MVP
- reply threading is a parity gap until the daemon exposes reply-tag support
- historical attachment download is a future enhancement
6 changes: 6 additions & 0 deletions pikachat-claude/.claude-plugin/plugin.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
{
"name": "pikachat-claude",
"version": "0.1.0",
"description": "Claude Code channel plugin backed by pikachat daemon",
"mcpServers": "./.mcp.json"
}
12 changes: 12 additions & 0 deletions pikachat-claude/.mcp.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
{
"mcpServers": {
"pikachat": {
"command": "node",
"args": ["${CLAUDE_PLUGIN_ROOT}/dist/server.js"],
"startupTimeout": 30000,
"env": {
"PIKACHAT_CHANNEL_SOURCE": "pikachat"
}
}
}
}
101 changes: 101 additions & 0 deletions pikachat-claude/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,101 @@
# pikachat-claude

Claude Code channel plugin backed by `pikachat daemon`.

## Current scope

- DM routing with pairing / allowlist
- explicit group enablement with mention gating
- reply / react / file send MCP tools
- inbound attachment surfacing via daemon-provided local paths
- local relay e2e harness

## Local development

```sh
cd /Users/futurepaul/dev/sec/other-peoples-code/pika/pikachat-claude
npm install
npm run build
```

Then run Claude from the repo root with the plugin directory:

```sh
cd /Users/futurepaul/dev/sec/other-peoples-code/pika
claude --plugin-dir ./pikachat-claude \
--dangerously-load-development-channels plugin:pikachat-claude@inline
```

Channels require Claude Code `v2.1.80+`.

Running Claude from inside `pikachat-claude/` is not recommended for local testing because that plugin's `.mcp.json` will also be treated as the project's `.mcp.json`.

If you prefer a one-shot launch with explicit env overrides, this also works:

```sh
cd /Users/futurepaul/dev/sec/other-peoples-code/pika
PIKACHAT_RELAYS='["wss://example-relay"]' \
PIKACHAT_STATE_DIR=~/.local/state/pikachat \
claude --plugin-dir ./pikachat-claude \
--dangerously-load-development-channels plugin:pikachat-claude@inline
```

The plugin uses the same default relay profile as `pikachat` when `PIKACHAT_RELAYS` is not set. If you are not using a preinstalled `pikachat` binary, the plugin will try to resolve one from GitHub releases using the same logic as `pikachat-openclaw`.

## Environment

- `PIKACHAT_RELAYS`
- optional JSON array or comma-separated relay URLs; defaults to the standard pikachat relay profile
- `PIKACHAT_STATE_DIR`
- daemon state dir; set this before first start if you want a dedicated bot identity instead of reusing `~/.local/state/pikachat`
- `PIKACHAT_DAEMON_CMD`
- `PIKACHAT_DAEMON_ARGS`
- JSON array
- `PIKACHAT_DAEMON_VERSION`
- `PIKACHAT_DAEMON_BACKEND`
- `native` or `acp`
- `PIKACHAT_DAEMON_ACP_EXEC`
- `PIKACHAT_DAEMON_ACP_CWD`
- `PIKACHAT_AUTO_ACCEPT_WELCOMES`
- `PIKACHAT_CHANNEL_SOURCE`

## Testing

```sh
npm test
npm run test:e2e-local-relay
```

The local relay e2e requires working `cargo` and `go` toolchains.

## Identity / npub

The daemon creates or loads its identity on startup from `PIKACHAT_STATE_DIR` (or `~/.local/state/pikachat` by default). If the state dir is new, the first daemon start generates a fresh keypair and `npub`.

To inspect the active identity for a chosen state dir:

```sh
cargo run -q -p pikachat -- --state-dir /tmp/pikachat-claude-state identity
```

If you are using the default state dir, a plain:

```sh
pikachat identity
```

shows the same identity the plugin will use.

## Startup sanity check

After launching Claude, verify that the wrapper and daemon are both running:

```sh
ps -axo pid,ppid,command | rg 'pikachat|dist/server.js'
```

You should see:

- `claude ...`
- `node .../pikachat-claude/dist/server.js`
- `pikachat daemon ...`
20 changes: 20 additions & 0 deletions pikachat-claude/package.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
{
"name": "pikachat-claude",
"version": "0.1.0",
"private": true,
"description": "Claude Code channel plugin backed by pikachat daemon",
"type": "module",
"scripts": {
"build": "esbuild src/server.ts --bundle --platform=node --format=esm --outfile=dist/server.js --banner:js='#!/usr/bin/env node'",
"typecheck": "tsc --noEmit",
"test": "node --import tsx --test src/**/*.test.ts",
"test:e2e-local-relay": "RUN_PIKACHAT_CLAUDE_E2E=1 node --import tsx --test src/local-relay-e2e.test.ts"
},
"devDependencies": {
"@types/node": "^22.13.14",
"@modelcontextprotocol/sdk": "^1.17.5",
"esbuild": "^0.25.1",
"tsx": "^4.19.3",
"typescript": "^5.8.2"
}
}
Loading
Loading