@keychat-io/keychat 0.1.18
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +661 -0
- package/README.md +170 -0
- package/docs/setup-guide.md +124 -0
- package/index.ts +205 -0
- package/openclaw.plugin.json +38 -0
- package/package.json +33 -0
- package/scripts/build-release.sh +76 -0
- package/scripts/install.sh +180 -0
- package/scripts/postinstall.mjs +128 -0
- package/scripts/publish.sh +25 -0
- package/scripts/setup.sh +80 -0
- package/src/bridge-client.ts +854 -0
- package/src/channel.ts +2631 -0
- package/src/config-schema.ts +50 -0
- package/src/ensure-binary.ts +65 -0
- package/src/keychain.ts +75 -0
- package/src/lightning.ts +128 -0
- package/src/media.ts +189 -0
- package/src/notify.ts +15 -0
- package/src/nwc.ts +360 -0
- package/src/paths.ts +38 -0
- package/src/qrcode-types.d.ts +5 -0
- package/src/qrcode.ts +23 -0
- package/src/runtime.ts +14 -0
- package/src/types.ts +125 -0
package/README.md
ADDED
|
@@ -0,0 +1,170 @@
|
|
|
1
|
+
# Keychat — OpenClaw Channel Plugin
|
|
2
|
+
|
|
3
|
+
E2E encrypted AI agent communication via Keychat protocol.
|
|
4
|
+
|
|
5
|
+
## What is this?
|
|
6
|
+
|
|
7
|
+
This plugin gives your OpenClaw agent a **sovereign identity** — a self-generated Public Key ID (Nostr keypair) — and enables **end-to-end encrypted communication** using the Signal Protocol over Nostr relays.
|
|
8
|
+
|
|
9
|
+
Your agent becomes a full Keychat citizen: it can receive friend requests, establish Signal Protocol sessions, and exchange messages with Keychat app users. All messages are encrypted with forward and backward secrecy — not even relay operators can read them.
|
|
10
|
+
|
|
11
|
+
## Install
|
|
12
|
+
|
|
13
|
+
```bash
|
|
14
|
+
openclaw plugins install @keychat-io/keychat
|
|
15
|
+
openclaw gateway restart
|
|
16
|
+
```
|
|
17
|
+
|
|
18
|
+
That's it. The plugin automatically downloads the bridge binary and initializes the config on first load.
|
|
19
|
+
|
|
20
|
+
Supported platforms: macOS (ARM/x64), Linux (x64/ARM64).
|
|
21
|
+
|
|
22
|
+
Alternatively, install via shell script:
|
|
23
|
+
|
|
24
|
+
```bash
|
|
25
|
+
curl -fsSL https://raw.githubusercontent.com/keychat-io/keychat/main/scripts/install.sh | bash
|
|
26
|
+
```
|
|
27
|
+
|
|
28
|
+
### Security Warnings
|
|
29
|
+
|
|
30
|
+
During installation, OpenClaw's security scanner may show three warnings. All are expected:
|
|
31
|
+
|
|
32
|
+
| Warning | Reason |
|
|
33
|
+
|---------|--------|
|
|
34
|
+
| Shell command execution (bridge-client.ts) | Spawns a Rust sidecar for Signal Protocol and MLS encryption. |
|
|
35
|
+
| Shell command execution (keychain.ts) | Stores identity mnemonics in the OS keychain (macOS Keychain / Linux libsecret). |
|
|
36
|
+
| Shell command execution (notify.ts) | Notifies the agent on startup so it can send the Keychat ID and QR code to the user. |
|
|
37
|
+
|
|
38
|
+
Source code is fully open: [github.com/keychat-io/keychat](https://github.com/keychat-io/keychat)
|
|
39
|
+
|
|
40
|
+
### Upgrade
|
|
41
|
+
|
|
42
|
+
Tell your agent "upgrade keychat" in any chat, or manually:
|
|
43
|
+
|
|
44
|
+
```bash
|
|
45
|
+
openclaw plugins install @keychat-io/keychat@latest
|
|
46
|
+
openclaw gateway restart
|
|
47
|
+
```
|
|
48
|
+
|
|
49
|
+
## Add Your Agent as a Keychat Contact
|
|
50
|
+
|
|
51
|
+
After `openclaw gateway restart`, the agent will send you its **Keychat ID**, **contact link**, and **QR code** in your active chat (Telegram, webchat, etc.):
|
|
52
|
+
|
|
53
|
+
```
|
|
54
|
+
🔑 Keychat ID: npub1...
|
|
55
|
+
📱 Add contact: https://www.keychat.io/u/?k=npub1...
|
|
56
|
+
🖼️ QR code image
|
|
57
|
+
```
|
|
58
|
+
|
|
59
|
+
Open the [Keychat app](https://keychat.io) → tap the link, paste the npub, or scan the QR code to add as contact. If `dmPolicy` is `open` (default after auto-init), the agent accepts immediately.
|
|
60
|
+
|
|
61
|
+
## Configuration
|
|
62
|
+
|
|
63
|
+
All options go under `channels.keychat` in your OpenClaw config (`~/.openclaw/openclaw.json`):
|
|
64
|
+
|
|
65
|
+
| Option | Type | Default | Description |
|
|
66
|
+
| ------------------ | -------- | ---------------------------- | --------------------------------------------------------- |
|
|
67
|
+
| `enabled` | boolean | `true` | Enable/disable the Keychat channel |
|
|
68
|
+
| `name` | string | — | Display name for this account |
|
|
69
|
+
| `relays` | string[] | `["wss://relay.keychat.io"]` | Nostr relay WebSocket URLs |
|
|
70
|
+
| `dmPolicy` | enum | `"open"` | Access policy: `pairing`, `allowlist`, `open`, `disabled` |
|
|
71
|
+
| `allowFrom` | string[] | `[]` | Allowed sender pubkeys (npub or hex) |
|
|
72
|
+
| `lightningAddress` | string | — | Lightning address for receiving payments |
|
|
73
|
+
| `nwcUri` | string | — | Nostr Wallet Connect URI for wallet access |
|
|
74
|
+
|
|
75
|
+
### DM Policies
|
|
76
|
+
|
|
77
|
+
- **`open`**: Anyone can message the agent (default)
|
|
78
|
+
- **`pairing`**: New contacts require owner approval via OpenClaw
|
|
79
|
+
- **`allowlist`**: Only pubkeys in `allowFrom` can communicate
|
|
80
|
+
- **`disabled`**: No inbound messages accepted
|
|
81
|
+
|
|
82
|
+
## Lightning Wallet
|
|
83
|
+
|
|
84
|
+
### Lightning Address (receive-only)
|
|
85
|
+
|
|
86
|
+
```json
|
|
87
|
+
{ "lightningAddress": "user@walletofsatoshi.com" }
|
|
88
|
+
```
|
|
89
|
+
|
|
90
|
+
### Nostr Wallet Connect (NWC)
|
|
91
|
+
|
|
92
|
+
For full wallet access (create invoices, check balance, verify payments):
|
|
93
|
+
|
|
94
|
+
```json
|
|
95
|
+
{ "nwcUri": "nostr+walletconnect://pubkey?relay=wss://...&secret=..." }
|
|
96
|
+
```
|
|
97
|
+
|
|
98
|
+
Generate an NWC connection string from your wallet app (Keychat, Alby Hub, Mutiny, Coinos, etc.).
|
|
99
|
+
|
|
100
|
+
**Security note**: The agent can receive payments freely. Outbound payments require owner approval.
|
|
101
|
+
|
|
102
|
+
## Architecture
|
|
103
|
+
|
|
104
|
+
```
|
|
105
|
+
┌──────────────┐ JSON-RPC ┌─────────────────────┐ Nostr ┌─────────┐
|
|
106
|
+
│ OpenClaw │◄──────────────►│ keychat │◄───────────►│ Relays │
|
|
107
|
+
│ (TypeScript │ stdin/stdout │ (Rust sidecar) │ WebSocket │ │
|
|
108
|
+
│ plugin) │ │ │ │ │
|
|
109
|
+
└──────────────┘ └─────────────────────┘ └─────────┘
|
|
110
|
+
│ Signal Protocol DB │
|
|
111
|
+
│ (SQLite) │
|
|
112
|
+
└────────────────────┘
|
|
113
|
+
```
|
|
114
|
+
|
|
115
|
+
- **TypeScript plugin**: OpenClaw channel integration, routing, pairing, message dispatch
|
|
116
|
+
- **Rust sidecar**: Signal Protocol sessions, Nostr transport, encryption/decryption
|
|
117
|
+
- **Communication**: JSON-RPC over stdin/stdout
|
|
118
|
+
- **Encryption**: Signal Protocol (Double Ratchet) with forward and backward secrecy
|
|
119
|
+
- **Transport**: Nostr relays (kind:4 DMs + kind:1059 Gift Wrap for friend requests)
|
|
120
|
+
|
|
121
|
+
## Security
|
|
122
|
+
|
|
123
|
+
- **E2E Encryption**: All messages encrypted with Signal Protocol — relay operators cannot read content
|
|
124
|
+
- **Forward & Backward Secrecy**: Double Ratchet ensures compromising current keys reveals neither past nor future messages
|
|
125
|
+
- **Sovereign Identity**: Agent generates its own keypair — no third-party identity provider
|
|
126
|
+
- **Key Storage**: Mnemonic stored in system keychain (macOS Keychain, Linux secret service)
|
|
127
|
+
- **Sending Address Rotation**: Each outbound message uses a fresh Nostr keypair, preventing metadata correlation
|
|
128
|
+
- **Receiving Address Rotation**: Ratchet-derived addresses rotate almost per message, preventing traffic analysis
|
|
129
|
+
|
|
130
|
+
## Troubleshooting
|
|
131
|
+
|
|
132
|
+
- **Bridge not starting**: Check `ls ~/.openclaw/extensions/keychat/bridge/target/release/keychat`. If missing, restart gateway (auto-downloads) or build from source: `cd bridge && cargo build --release`
|
|
133
|
+
- **Relay issues**: Verify relay URLs (`wss://...`), try alternative relays
|
|
134
|
+
- **Decryption errors**: Peer should delete old contact and re-add the agent
|
|
135
|
+
- **Messages not delivered**: Plugin queues failed messages (up to 100) and retries every 30s
|
|
136
|
+
|
|
137
|
+
## Development
|
|
138
|
+
|
|
139
|
+
```bash
|
|
140
|
+
cd bridge && cargo build --release
|
|
141
|
+
cargo test
|
|
142
|
+
```
|
|
143
|
+
|
|
144
|
+
### Project Structure
|
|
145
|
+
|
|
146
|
+
```
|
|
147
|
+
├── src/
|
|
148
|
+
│ ├── channel.ts # Main channel plugin
|
|
149
|
+
│ ├── bridge-client.ts # RPC client for Rust sidecar
|
|
150
|
+
│ ├── config-schema.ts # Zod config schema
|
|
151
|
+
│ ├── keychain.ts # System keychain integration
|
|
152
|
+
│ ├── lightning.ts # LNURL-pay support
|
|
153
|
+
│ ├── nwc.ts # Nostr Wallet Connect (NIP-47)
|
|
154
|
+
│ ├── media.ts # Blossom media encryption/upload
|
|
155
|
+
│ ├── qrcode.ts # QR code generation
|
|
156
|
+
│ ├── runtime.ts # Plugin runtime accessor
|
|
157
|
+
│ └── types.ts # Account types and resolvers
|
|
158
|
+
├── bridge/src/
|
|
159
|
+
│ ├── main.rs # Sidecar entry point
|
|
160
|
+
│ ├── rpc.rs # JSON-RPC dispatch
|
|
161
|
+
│ ├── signal.rs # Signal Protocol manager
|
|
162
|
+
│ ├── protocol.rs # Keychat protocol types
|
|
163
|
+
│ ├── mls.rs # MLS large group support
|
|
164
|
+
│ └── transport.rs # Nostr relay transport
|
|
165
|
+
├── scripts/
|
|
166
|
+
│ └── install.sh # One-line installer
|
|
167
|
+
├── index.ts # Plugin entry point
|
|
168
|
+
├── openclaw.plugin.json # Plugin manifest
|
|
169
|
+
└── LICENSE # AGPL-3.0
|
|
170
|
+
```
|
|
@@ -0,0 +1,124 @@
|
|
|
1
|
+
# Keychat — Setup Guide
|
|
2
|
+
|
|
3
|
+
Step-by-step instructions to get your OpenClaw agent communicating via Keychat.
|
|
4
|
+
|
|
5
|
+
## Prerequisites
|
|
6
|
+
|
|
7
|
+
- **macOS or Linux** (arm64 or x86_64)
|
|
8
|
+
- **Node.js 20+**
|
|
9
|
+
- **OpenClaw**: Installed and configured (`openclaw gateway status` should work)
|
|
10
|
+
|
|
11
|
+
## Step 1: Install the Plugin
|
|
12
|
+
|
|
13
|
+
```bash
|
|
14
|
+
openclaw plugins install @keychat-io/keychat
|
|
15
|
+
```
|
|
16
|
+
|
|
17
|
+
This auto-downloads the pre-compiled Rust sidecar binary for your platform. No Rust toolchain needed.
|
|
18
|
+
|
|
19
|
+
> **Building from source** (optional): If no pre-compiled binary is available for your platform, install [Rust](https://rustup.rs/) and run `cd bridge && cargo build --release`.
|
|
20
|
+
|
|
21
|
+
## Step 2: Configure OpenClaw
|
|
22
|
+
|
|
23
|
+
Edit `~/.openclaw/openclaw.json` and add the Keychat channel config:
|
|
24
|
+
|
|
25
|
+
```json
|
|
26
|
+
{
|
|
27
|
+
"channels": {
|
|
28
|
+
"keychat": {
|
|
29
|
+
"enabled": true,
|
|
30
|
+
"relays": [
|
|
31
|
+
"wss://relay.keychat.io",
|
|
32
|
+
"wss://relay.damus.io"
|
|
33
|
+
],
|
|
34
|
+
"dmPolicy": "pairing"
|
|
35
|
+
}
|
|
36
|
+
}
|
|
37
|
+
}
|
|
38
|
+
```
|
|
39
|
+
|
|
40
|
+
### DM Policies
|
|
41
|
+
|
|
42
|
+
| Policy | Description |
|
|
43
|
+
|--------|-------------|
|
|
44
|
+
| `pairing` | New contacts require owner approval (default) |
|
|
45
|
+
| `allowlist` | Only pubkeys in `allowFrom` can communicate |
|
|
46
|
+
| `open` | Anyone can message the agent |
|
|
47
|
+
| `disabled` | No inbound messages accepted |
|
|
48
|
+
|
|
49
|
+
## Step 3: Restart the Gateway
|
|
50
|
+
|
|
51
|
+
```bash
|
|
52
|
+
openclaw gateway restart
|
|
53
|
+
```
|
|
54
|
+
|
|
55
|
+
Watch the logs for your agent's Keychat ID:
|
|
56
|
+
|
|
57
|
+
```
|
|
58
|
+
═══════════════════════════════════════════════════
|
|
59
|
+
🔑 Agent Keychat ID (scan with Keychat app):
|
|
60
|
+
|
|
61
|
+
npub1...
|
|
62
|
+
|
|
63
|
+
Add contact link:
|
|
64
|
+
https://www.keychat.io/u/?k=npub1...
|
|
65
|
+
═══════════════════════════════════════════════════
|
|
66
|
+
```
|
|
67
|
+
|
|
68
|
+
A QR code is also saved to `~/.openclaw/keychat/qr-default.png`.
|
|
69
|
+
|
|
70
|
+
## Step 4: Connect with Keychat App
|
|
71
|
+
|
|
72
|
+
1. Open the [Keychat app](https://www.keychat.io/) on your phone
|
|
73
|
+
2. Tap **Add Contact**
|
|
74
|
+
3. Scan the QR code at `~/.openclaw/keychat/qr-default.png`, or paste the npub / contact URL
|
|
75
|
+
4. Send a friend request
|
|
76
|
+
5. If `dmPolicy` is `pairing`, approve the request:
|
|
77
|
+
```bash
|
|
78
|
+
openclaw pair approve keychat <sender-pubkey>
|
|
79
|
+
```
|
|
80
|
+
|
|
81
|
+
## Step 5: Start Chatting
|
|
82
|
+
|
|
83
|
+
Send a message from the Keychat app — your agent will respond with E2E encrypted messages.
|
|
84
|
+
|
|
85
|
+
## Identity Management
|
|
86
|
+
|
|
87
|
+
- **First run**: Agent auto-generates a mnemonic and stores it in your system keychain (macOS Keychain / Linux libsecret)
|
|
88
|
+
- **Backup**: Export the mnemonic from your keychain if needed
|
|
89
|
+
- **Restore**: Set `mnemonic` in the channel config to restore an existing identity on a new machine
|
|
90
|
+
- **Signal DB**: Stored at `~/.openclaw/keychat/signal-default.db` — **do not delete** (destroys all encrypted sessions)
|
|
91
|
+
|
|
92
|
+
## Verifying the Setup
|
|
93
|
+
|
|
94
|
+
```bash
|
|
95
|
+
openclaw gateway status
|
|
96
|
+
```
|
|
97
|
+
|
|
98
|
+
The Keychat channel should show as running with the agent's npub.
|
|
99
|
+
|
|
100
|
+
## Lightning Wallet (Optional)
|
|
101
|
+
|
|
102
|
+
Add a Lightning address for receiving payments:
|
|
103
|
+
|
|
104
|
+
```json
|
|
105
|
+
{
|
|
106
|
+
"channels": {
|
|
107
|
+
"keychat": {
|
|
108
|
+
"lightningAddress": "user@walletofsatoshi.com"
|
|
109
|
+
}
|
|
110
|
+
}
|
|
111
|
+
}
|
|
112
|
+
```
|
|
113
|
+
|
|
114
|
+
For full wallet access (balance, payments), configure [Nostr Wallet Connect](https://github.com/nostr-protocol/nips/blob/master/47.md):
|
|
115
|
+
|
|
116
|
+
```json
|
|
117
|
+
{
|
|
118
|
+
"channels": {
|
|
119
|
+
"keychat": {
|
|
120
|
+
"nwcUri": "nostr+walletconnect://pubkey?relay=wss://...&secret=..."
|
|
121
|
+
}
|
|
122
|
+
}
|
|
123
|
+
}
|
|
124
|
+
```
|
package/index.ts
ADDED
|
@@ -0,0 +1,205 @@
|
|
|
1
|
+
import type { OpenClawPluginApi } from "openclaw/plugin-sdk";
|
|
2
|
+
import { emptyPluginConfigSchema } from "openclaw/plugin-sdk";
|
|
3
|
+
import { keychatPlugin, getAgentKeychatId, getAgentKeychatUrl, getAllAgentContacts } from "./src/channel.js";
|
|
4
|
+
import { setKeychatRuntime } from "./src/runtime.js";
|
|
5
|
+
import { existsSync, mkdirSync, chmodSync, writeFileSync, readFileSync } from "node:fs";
|
|
6
|
+
import { join, dirname } from "node:path";
|
|
7
|
+
import { fileURLToPath } from "node:url";
|
|
8
|
+
import { homedir } from "node:os";
|
|
9
|
+
import https from "node:https";
|
|
10
|
+
|
|
11
|
+
const __dirname = dirname(fileURLToPath(import.meta.url));
|
|
12
|
+
|
|
13
|
+
/** Download a URL following redirects, return a Buffer. */
|
|
14
|
+
function downloadBinary(url: string): Promise<Buffer> {
|
|
15
|
+
return new Promise((resolve, reject) => {
|
|
16
|
+
https.get(url, (res) => {
|
|
17
|
+
if (res.statusCode && res.statusCode >= 300 && res.statusCode < 400 && res.headers.location) {
|
|
18
|
+
return downloadBinary(res.headers.location).then(resolve, reject);
|
|
19
|
+
}
|
|
20
|
+
if (res.statusCode !== 200) {
|
|
21
|
+
return reject(new Error(`HTTP ${res.statusCode}`));
|
|
22
|
+
}
|
|
23
|
+
const chunks: Buffer[] = [];
|
|
24
|
+
res.on("data", (chunk: Buffer) => chunks.push(chunk));
|
|
25
|
+
res.on("end", () => resolve(Buffer.concat(chunks)));
|
|
26
|
+
res.on("error", reject);
|
|
27
|
+
}).on("error", reject);
|
|
28
|
+
});
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
/** Ensure bridge binary exists, download if missing. */
|
|
32
|
+
async function ensureBinary(): Promise<void> {
|
|
33
|
+
const binaryDir = join(__dirname, "bridge", "target", "release");
|
|
34
|
+
const binaryPath = join(binaryDir, "keychat-bridge");
|
|
35
|
+
|
|
36
|
+
if (existsSync(binaryPath)) return;
|
|
37
|
+
|
|
38
|
+
const platform = process.platform;
|
|
39
|
+
const arch = process.arch;
|
|
40
|
+
const artifacts: Record<string, string> = {
|
|
41
|
+
"darwin-arm64": "keychat-openclaw-darwin-arm64",
|
|
42
|
+
"darwin-x64": "keychat-openclaw-darwin-x64",
|
|
43
|
+
"linux-x64": "keychat-openclaw-linux-x64",
|
|
44
|
+
"linux-arm64": "keychat-openclaw-linux-arm64",
|
|
45
|
+
};
|
|
46
|
+
|
|
47
|
+
const artifact = artifacts[`${platform}-${arch}`];
|
|
48
|
+
if (!artifact) {
|
|
49
|
+
console.warn(`[keychat] No pre-compiled binary for ${platform}-${arch}. Build from source: cd bridge && cargo build --release`);
|
|
50
|
+
return;
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
const url = `https://github.com/keychat-io/keychat-openclaw/releases/latest/download/${artifact}`;
|
|
54
|
+
console.log(`[keychat] Bridge binary not found, downloading ${artifact}...`);
|
|
55
|
+
|
|
56
|
+
try {
|
|
57
|
+
mkdirSync(binaryDir, { recursive: true });
|
|
58
|
+
const buffer = await downloadBinary(url);
|
|
59
|
+
writeFileSync(binaryPath, buffer);
|
|
60
|
+
chmodSync(binaryPath, 0o755);
|
|
61
|
+
console.log("[keychat] ✅ Bridge binary installed");
|
|
62
|
+
} catch (err: any) {
|
|
63
|
+
console.warn(`[keychat] Binary download failed: ${err.message}`);
|
|
64
|
+
console.warn("[keychat] Build from source: cd bridge && cargo build --release");
|
|
65
|
+
}
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
/** Ensure channels.keychat exists in openclaw.json config. */
|
|
69
|
+
function ensureConfig(): void {
|
|
70
|
+
const configPath = join(homedir(), ".openclaw", "openclaw.json");
|
|
71
|
+
try {
|
|
72
|
+
let config: any = {};
|
|
73
|
+
if (existsSync(configPath)) {
|
|
74
|
+
config = JSON.parse(readFileSync(configPath, "utf-8"));
|
|
75
|
+
}
|
|
76
|
+
if (config.channels?.keychat) return;
|
|
77
|
+
|
|
78
|
+
if (!config.channels) config.channels = {};
|
|
79
|
+
config.channels.keychat = { enabled: true, dmPolicy: "open" };
|
|
80
|
+
writeFileSync(configPath, JSON.stringify(config, null, 2) + "\n", "utf-8");
|
|
81
|
+
console.log("[keychat] ✅ Config initialized (channels.keychat.enabled = true)");
|
|
82
|
+
} catch (err: any) {
|
|
83
|
+
console.warn(`[keychat] Could not auto-configure: ${err.message}`);
|
|
84
|
+
}
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
const plugin = {
|
|
88
|
+
id: "keychat",
|
|
89
|
+
name: "Keychat",
|
|
90
|
+
description:
|
|
91
|
+
"Keychat channel plugin — sovereign identity + E2E encrypted chat. " +
|
|
92
|
+
"Agent generates its own Public Key ID, just like a human Keychat user.",
|
|
93
|
+
configSchema: emptyPluginConfigSchema(),
|
|
94
|
+
register(api: OpenClawPluginApi) {
|
|
95
|
+
console.log("[keychat] register() called");
|
|
96
|
+
try {
|
|
97
|
+
// Auto-setup: download binary + init config on first load
|
|
98
|
+
ensureConfig();
|
|
99
|
+
ensureBinary().catch((err) => console.warn("[keychat] ensureBinary error:", err));
|
|
100
|
+
setKeychatRuntime(api.runtime);
|
|
101
|
+
console.log("[keychat] runtime set, registering channel...");
|
|
102
|
+
api.registerChannel({ plugin: keychatPlugin });
|
|
103
|
+
console.log("[keychat] channel registered successfully");
|
|
104
|
+
|
|
105
|
+
// Register keychat_identity agent tool so the agent can fetch ID info
|
|
106
|
+
// and send it to the user on whatever channel they're on
|
|
107
|
+
api.registerTool({
|
|
108
|
+
name: "keychat_identity",
|
|
109
|
+
label: "Keychat Identity",
|
|
110
|
+
description:
|
|
111
|
+
"Get the Keychat identity (npub, contact link, QR code path) for all active agent accounts. " +
|
|
112
|
+
"Call this after Keychat setup/install to retrieve the agent's Keychat ID and share it with the user.",
|
|
113
|
+
parameters: { type: "object", properties: {}, required: [] },
|
|
114
|
+
async execute(_toolCallId: string, _params: any) {
|
|
115
|
+
const contacts = getAllAgentContacts();
|
|
116
|
+
if (contacts.length === 0) {
|
|
117
|
+
return {
|
|
118
|
+
details: null,
|
|
119
|
+
content: [
|
|
120
|
+
{
|
|
121
|
+
type: "text" as const,
|
|
122
|
+
text: "No Keychat accounts are active yet. The bridge may still be starting — try again in a few seconds.",
|
|
123
|
+
},
|
|
124
|
+
],
|
|
125
|
+
};
|
|
126
|
+
}
|
|
127
|
+
const { generateQRDataUrl } = await import("./src/qrcode.js");
|
|
128
|
+
const results = [];
|
|
129
|
+
const contentParts: Array<{ type: "text"; text: string } | { type: "image"; data: string; mimeType: string }> = [];
|
|
130
|
+
|
|
131
|
+
for (const c of contacts) {
|
|
132
|
+
const qrDataUrl = await generateQRDataUrl(c.npub);
|
|
133
|
+
results.push({
|
|
134
|
+
accountId: c.accountId,
|
|
135
|
+
npub: c.npub,
|
|
136
|
+
contactUrl: c.contactUrl,
|
|
137
|
+
});
|
|
138
|
+
contentParts.push({
|
|
139
|
+
type: "text" as const,
|
|
140
|
+
text: `Account: ${c.accountId}\nnpub: ${c.npub}\nContact: ${c.contactUrl}`,
|
|
141
|
+
});
|
|
142
|
+
if (qrDataUrl) {
|
|
143
|
+
// Extract base64 from data URL: "data:image/png;base64,..."
|
|
144
|
+
const base64 = qrDataUrl.replace(/^data:image\/png;base64,/, "");
|
|
145
|
+
contentParts.push({
|
|
146
|
+
type: "image" as const,
|
|
147
|
+
data: base64,
|
|
148
|
+
mimeType: "image/png",
|
|
149
|
+
});
|
|
150
|
+
}
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
return {
|
|
154
|
+
details: null,
|
|
155
|
+
content: contentParts,
|
|
156
|
+
};
|
|
157
|
+
},
|
|
158
|
+
});
|
|
159
|
+
|
|
160
|
+
// Register /keychat-id command to show agent Keychat ID, link, and QR
|
|
161
|
+
api.registerCommand({
|
|
162
|
+
name: "keychat-id",
|
|
163
|
+
description: "Show Keychat agent ID(s), contact link(s), and QR code path(s)",
|
|
164
|
+
handler: () => {
|
|
165
|
+
const contacts = getAllAgentContacts();
|
|
166
|
+
if (contacts.length === 0) {
|
|
167
|
+
return { text: "⚠️ No Keychat accounts are active yet. Wait for the bridge to start." };
|
|
168
|
+
}
|
|
169
|
+
const lines = contacts.map((c) => {
|
|
170
|
+
const qrExists = existsSync(c.qrCodePath);
|
|
171
|
+
return [
|
|
172
|
+
`🔑 **${c.accountId}**`,
|
|
173
|
+
` npub: \`${c.npub}\``,
|
|
174
|
+
` 📱 Link: ${c.contactUrl}`,
|
|
175
|
+
qrExists
|
|
176
|
+
? ` 🖼️ QR: ${c.qrCodePath}`
|
|
177
|
+
: ` (QR image not found)`,
|
|
178
|
+
].join("\n");
|
|
179
|
+
});
|
|
180
|
+
return { text: lines.join("\n\n") };
|
|
181
|
+
},
|
|
182
|
+
});
|
|
183
|
+
} catch (err) {
|
|
184
|
+
console.error("[keychat] register failed:", err);
|
|
185
|
+
}
|
|
186
|
+
},
|
|
187
|
+
};
|
|
188
|
+
|
|
189
|
+
export default plugin;
|
|
190
|
+
export { getAgentKeychatId, getAgentKeychatUrl, getAllAgentContacts, resetPeerSession } from "./src/channel.js";
|
|
191
|
+
export { generateQRDataUrl } from "./src/qrcode.js";
|
|
192
|
+
export {
|
|
193
|
+
requestInvoice,
|
|
194
|
+
fetchLnurlPayMetadata,
|
|
195
|
+
verifyPayment,
|
|
196
|
+
formatSats,
|
|
197
|
+
lightningAddressToLnurlp,
|
|
198
|
+
} from "./src/lightning.js";
|
|
199
|
+
export {
|
|
200
|
+
NwcClient,
|
|
201
|
+
parseNwcUri,
|
|
202
|
+
initNwc,
|
|
203
|
+
getNwcClient,
|
|
204
|
+
disconnectNwc,
|
|
205
|
+
} from "./src/nwc.js";
|
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
{
|
|
2
|
+
"id": "keychat",
|
|
3
|
+
"channels": ["keychat"],
|
|
4
|
+
"name": "Keychat",
|
|
5
|
+
"description": "Sovereign identity + E2E encrypted chat via Signal Protocol over Nostr relays. Lightning wallet support via LNURL-pay and NWC.",
|
|
6
|
+
"version": "0.1.0",
|
|
7
|
+
"repository": "https://github.com/keychat-io/keychat-openclaw",
|
|
8
|
+
"license": "AGPL-3.0",
|
|
9
|
+
"configSchema": {
|
|
10
|
+
"type": "object",
|
|
11
|
+
"additionalProperties": false,
|
|
12
|
+
"properties": {
|
|
13
|
+
"name": { "type": "string", "description": "Display name for this account" },
|
|
14
|
+
"enabled": { "type": "boolean", "description": "Enable/disable the Keychat channel" },
|
|
15
|
+
"markdown": { "type": "object", "description": "Markdown formatting overrides" },
|
|
16
|
+
"mnemonic": { "type": "string", "description": "Identity mnemonic (auto-generated, stored in keychain)" },
|
|
17
|
+
"publicKey": { "type": "string", "description": "Derived hex public key (read-only)" },
|
|
18
|
+
"npub": { "type": "string", "description": "Derived bech32 npub (read-only)" },
|
|
19
|
+
"relays": {
|
|
20
|
+
"type": "array",
|
|
21
|
+
"items": { "type": "string" },
|
|
22
|
+
"description": "Nostr relay WebSocket URLs"
|
|
23
|
+
},
|
|
24
|
+
"dmPolicy": {
|
|
25
|
+
"type": "string",
|
|
26
|
+
"enum": ["pairing", "allowlist", "open", "disabled"],
|
|
27
|
+
"description": "DM access policy"
|
|
28
|
+
},
|
|
29
|
+
"allowFrom": {
|
|
30
|
+
"type": "array",
|
|
31
|
+
"items": { "type": ["string", "number"] },
|
|
32
|
+
"description": "Allowed sender pubkeys (npub or hex)"
|
|
33
|
+
},
|
|
34
|
+
"lightningAddress": { "type": "string", "description": "Lightning address for receiving payments" },
|
|
35
|
+
"nwcUri": { "type": "string", "description": "Nostr Wallet Connect URI" }
|
|
36
|
+
}
|
|
37
|
+
}
|
|
38
|
+
}
|
package/package.json
ADDED
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@keychat-io/keychat",
|
|
3
|
+
"version": "0.1.18",
|
|
4
|
+
"description": "Keychat — E2E encrypted chat + Lightning wallet for OpenClaw agents",
|
|
5
|
+
"license": "AGPL-3.0",
|
|
6
|
+
"repository": {
|
|
7
|
+
"type": "git",
|
|
8
|
+
"url": "https://github.com/keychat-io/keychat-openclaw.git"
|
|
9
|
+
},
|
|
10
|
+
"type": "module",
|
|
11
|
+
"scripts": {
|
|
12
|
+
"postinstall": "node scripts/postinstall.mjs"
|
|
13
|
+
},
|
|
14
|
+
"dependencies": {
|
|
15
|
+
"@nostr-dev-kit/ndk": "^3.0.0",
|
|
16
|
+
"qrcode": "^1.5.4",
|
|
17
|
+
"zod": "^4.3.6"
|
|
18
|
+
},
|
|
19
|
+
"openclaw": {
|
|
20
|
+
"extensions": [
|
|
21
|
+
"./index.ts"
|
|
22
|
+
],
|
|
23
|
+
"channel": {
|
|
24
|
+
"id": "keychat",
|
|
25
|
+
"label": "Keychat",
|
|
26
|
+
"selectionLabel": "Keychat (E2E Encrypted)",
|
|
27
|
+
"docsPath": "/channels/keychat",
|
|
28
|
+
"docsLabel": "keychat",
|
|
29
|
+
"blurb": "Sovereign identity + E2E encrypted chat via Signal Protocol over Nostr relays.",
|
|
30
|
+
"order": 56
|
|
31
|
+
}
|
|
32
|
+
}
|
|
33
|
+
}
|
|
@@ -0,0 +1,76 @@
|
|
|
1
|
+
#!/bin/bash
|
|
2
|
+
# Build release binaries for all supported platforms.
|
|
3
|
+
# Requires: cargo, cargo-zigbuild, zig
|
|
4
|
+
# Linux targets use musl for static linking (no glibc dependency).
|
|
5
|
+
#
|
|
6
|
+
# Usage: ./scripts/build-release.sh [--upload v0.1.x]
|
|
7
|
+
set -e
|
|
8
|
+
|
|
9
|
+
SCRIPT_DIR="$(cd "$(dirname "$0")" && pwd)"
|
|
10
|
+
BRIDGE_DIR="$SCRIPT_DIR/../bridge"
|
|
11
|
+
OUT_DIR="$SCRIPT_DIR/../dist"
|
|
12
|
+
REPO="keychat-io/keychat-openclaw"
|
|
13
|
+
|
|
14
|
+
cd "$BRIDGE_DIR"
|
|
15
|
+
|
|
16
|
+
echo "🔨 Building release binaries..."
|
|
17
|
+
echo ""
|
|
18
|
+
|
|
19
|
+
mkdir -p "$OUT_DIR"
|
|
20
|
+
|
|
21
|
+
# ── Darwin ARM64 (native) ──
|
|
22
|
+
echo " [1/4] darwin-arm64..."
|
|
23
|
+
cargo build --release 2>&1 | grep -v "^warning:" || true
|
|
24
|
+
cp target/release/keychat-openclaw "$OUT_DIR/keychat-openclaw-darwin-arm64"
|
|
25
|
+
echo " ✅ darwin-arm64"
|
|
26
|
+
|
|
27
|
+
# ── Darwin x64 (cross) ──
|
|
28
|
+
echo " [2/4] darwin-x64..."
|
|
29
|
+
if cargo zigbuild --target x86_64-apple-darwin --release 2>&1 | grep -v "^warning:" | tail -1; then
|
|
30
|
+
cp target/x86_64-apple-darwin/release/keychat-openclaw "$OUT_DIR/keychat-openclaw-darwin-x64"
|
|
31
|
+
echo " ✅ darwin-x64"
|
|
32
|
+
else
|
|
33
|
+
echo " ⚠️ darwin-x64 failed (optional — most Macs are ARM now)"
|
|
34
|
+
fi
|
|
35
|
+
|
|
36
|
+
# ── Linux x64 (musl static) ──
|
|
37
|
+
echo " [3/4] linux-x64 (musl)..."
|
|
38
|
+
cargo zigbuild --target x86_64-unknown-linux-musl --release 2>&1 | grep -v "^warning:" || true
|
|
39
|
+
cp target/x86_64-unknown-linux-musl/release/keychat-openclaw "$OUT_DIR/keychat-openclaw-linux-x64"
|
|
40
|
+
echo " ✅ linux-x64 (statically linked)"
|
|
41
|
+
|
|
42
|
+
# ── Linux ARM64 (musl static) ──
|
|
43
|
+
echo " [4/4] linux-arm64 (musl)..."
|
|
44
|
+
cargo zigbuild --target aarch64-unknown-linux-musl --release 2>&1 | grep -v "^warning:" || true
|
|
45
|
+
cp target/aarch64-unknown-linux-musl/release/keychat-openclaw "$OUT_DIR/keychat-openclaw-linux-arm64"
|
|
46
|
+
echo " ✅ linux-arm64 (statically linked)"
|
|
47
|
+
|
|
48
|
+
echo ""
|
|
49
|
+
echo "════════════════════════════════════════"
|
|
50
|
+
echo " 📦 All binaries in: $OUT_DIR"
|
|
51
|
+
ls -lh "$OUT_DIR"/keychat-openclaw-*
|
|
52
|
+
echo "════════════════════════════════════════"
|
|
53
|
+
|
|
54
|
+
# ── Optional: upload to GitHub Release ──
|
|
55
|
+
if [ "$1" = "--upload" ] && [ -n "$2" ]; then
|
|
56
|
+
TAG="$2"
|
|
57
|
+
echo ""
|
|
58
|
+
echo "🚀 Uploading to GitHub Release $TAG..."
|
|
59
|
+
|
|
60
|
+
if ! command -v gh &>/dev/null; then
|
|
61
|
+
echo "❌ gh CLI not found. Install: brew install gh"
|
|
62
|
+
exit 1
|
|
63
|
+
fi
|
|
64
|
+
|
|
65
|
+
# Create release if not exists
|
|
66
|
+
gh release view "$TAG" --repo "$REPO" &>/dev/null || \
|
|
67
|
+
gh release create "$TAG" --repo "$REPO" --title "$TAG" --notes "Release $TAG"
|
|
68
|
+
|
|
69
|
+
# Upload/overwrite artifacts
|
|
70
|
+
for f in "$OUT_DIR"/keychat-openclaw-*; do
|
|
71
|
+
echo " Uploading $(basename "$f")..."
|
|
72
|
+
gh release upload "$TAG" "$f" --repo "$REPO" --clobber
|
|
73
|
+
done
|
|
74
|
+
|
|
75
|
+
echo " ✅ All artifacts uploaded to $TAG"
|
|
76
|
+
fi
|