pft-chatbot-mcp 0.1.0

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.
Files changed (90) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +461 -0
  3. package/dist/chain/pointer.d.ts +65 -0
  4. package/dist/chain/pointer.d.ts.map +1 -0
  5. package/dist/chain/pointer.js +116 -0
  6. package/dist/chain/pointer.js.map +1 -0
  7. package/dist/chain/scanner.d.ts +45 -0
  8. package/dist/chain/scanner.d.ts.map +1 -0
  9. package/dist/chain/scanner.js +161 -0
  10. package/dist/chain/scanner.js.map +1 -0
  11. package/dist/chain/submitter.d.ts +36 -0
  12. package/dist/chain/submitter.d.ts.map +1 -0
  13. package/dist/chain/submitter.js +83 -0
  14. package/dist/chain/submitter.js.map +1 -0
  15. package/dist/config.d.ts +17 -0
  16. package/dist/config.d.ts.map +1 -0
  17. package/dist/config.js +61 -0
  18. package/dist/config.js.map +1 -0
  19. package/dist/crypto/decrypt.d.ts +17 -0
  20. package/dist/crypto/decrypt.d.ts.map +1 -0
  21. package/dist/crypto/decrypt.js +49 -0
  22. package/dist/crypto/decrypt.js.map +1 -0
  23. package/dist/crypto/encrypt.d.ts +11 -0
  24. package/dist/crypto/encrypt.d.ts.map +1 -0
  25. package/dist/crypto/encrypt.js +53 -0
  26. package/dist/crypto/encrypt.js.map +1 -0
  27. package/dist/crypto/keys.d.ts +25 -0
  28. package/dist/crypto/keys.d.ts.map +1 -0
  29. package/dist/crypto/keys.js +32 -0
  30. package/dist/crypto/keys.js.map +1 -0
  31. package/dist/crypto/sodium.d.ts +3 -0
  32. package/dist/crypto/sodium.d.ts.map +1 -0
  33. package/dist/crypto/sodium.js +11 -0
  34. package/dist/crypto/sodium.js.map +1 -0
  35. package/dist/grpc/client.d.ts +76 -0
  36. package/dist/grpc/client.d.ts.map +1 -0
  37. package/dist/grpc/client.js +132 -0
  38. package/dist/grpc/client.js.map +1 -0
  39. package/dist/grpc/protos/keystone/v1/auth/auth.proto +39 -0
  40. package/dist/grpc/protos/keystone/v1/core/content.proto +19 -0
  41. package/dist/grpc/protos/keystone/v1/core/envelope.proto +64 -0
  42. package/dist/grpc/protos/keystone/v1/registry/registry.proto +90 -0
  43. package/dist/grpc/protos/keystone/v1/storage/storage.proto +68 -0
  44. package/dist/grpc/protos/pf/common/v4/common.proto +18 -0
  45. package/dist/grpc/protos/pf/ptr/v4/pointer.proto +23 -0
  46. package/dist/index.d.ts +3 -0
  47. package/dist/index.d.ts.map +1 -0
  48. package/dist/index.js +195 -0
  49. package/dist/index.js.map +1 -0
  50. package/dist/ipfs/gateway.d.ts +11 -0
  51. package/dist/ipfs/gateway.d.ts.map +1 -0
  52. package/dist/ipfs/gateway.js +94 -0
  53. package/dist/ipfs/gateway.js.map +1 -0
  54. package/dist/tools/create_wallet.d.ts +18 -0
  55. package/dist/tools/create_wallet.d.ts.map +1 -0
  56. package/dist/tools/create_wallet.js +53 -0
  57. package/dist/tools/create_wallet.js.map +1 -0
  58. package/dist/tools/get_message.d.ts +16 -0
  59. package/dist/tools/get_message.d.ts.map +1 -0
  60. package/dist/tools/get_message.js +79 -0
  61. package/dist/tools/get_message.js.map +1 -0
  62. package/dist/tools/get_thread.d.ts +22 -0
  63. package/dist/tools/get_thread.d.ts.map +1 -0
  64. package/dist/tools/get_thread.js +98 -0
  65. package/dist/tools/get_thread.js.map +1 -0
  66. package/dist/tools/register_bot.d.ts +29 -0
  67. package/dist/tools/register_bot.d.ts.map +1 -0
  68. package/dist/tools/register_bot.js +90 -0
  69. package/dist/tools/register_bot.js.map +1 -0
  70. package/dist/tools/scan_messages.d.ts +19 -0
  71. package/dist/tools/scan_messages.d.ts.map +1 -0
  72. package/dist/tools/scan_messages.js +67 -0
  73. package/dist/tools/scan_messages.js.map +1 -0
  74. package/dist/tools/search_bots.d.ts +19 -0
  75. package/dist/tools/search_bots.d.ts.map +1 -0
  76. package/dist/tools/search_bots.js +40 -0
  77. package/dist/tools/search_bots.js.map +1 -0
  78. package/dist/tools/send_message.d.ts +55 -0
  79. package/dist/tools/send_message.d.ts.map +1 -0
  80. package/dist/tools/send_message.js +143 -0
  81. package/dist/tools/send_message.js.map +1 -0
  82. package/dist/tools/upload_content.d.ts +19 -0
  83. package/dist/tools/upload_content.d.ts.map +1 -0
  84. package/dist/tools/upload_content.js +32 -0
  85. package/dist/tools/upload_content.js.map +1 -0
  86. package/dist/version.d.ts +19 -0
  87. package/dist/version.d.ts.map +1 -0
  88. package/dist/version.js +22 -0
  89. package/dist/version.js.map +1 -0
  90. package/package.json +63 -0
package/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 AGTI
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
package/README.md ADDED
@@ -0,0 +1,461 @@
1
+ # PFT Chatbot MCP
2
+
3
+ MCP server for building bots on the Post Fiat (PFTL) network.
4
+
5
+ This is a [Model Context Protocol](https://modelcontextprotocol.io/) server that gives LLMs the ability to send and receive encrypted on-chain messages on the PFTL network. It enables building bots that can scan for incoming messages, process them, and respond -- similar to Telegram Bots but chain-based, encrypted by default, and LLM-native.
6
+
7
+ ## Version Compatibility
8
+
9
+ | Component | Version | Notes |
10
+ |-----------|---------|-------|
11
+ | pft-chatbot-mcp | 0.1.0 | This package |
12
+ | Keystone Protocol | v1 | Proto schema version |
13
+ | pf.ptr Pointer | v4 | On-chain memo format |
14
+ | Keystone gRPC server | >= 0.1.0 | Backend service |
15
+
16
+ When the Keystone protocol is updated, a new MCP release will be published with matching compatibility. Check `src/version.ts` for the exact version constraints.
17
+
18
+ ## How It Works
19
+
20
+ ### Architecture
21
+
22
+ ```
23
+ Bot Operator's Machine Post Fiat Infrastructure
24
+ ┌──────────────────────────┐ ┌─────────────────────────┐
25
+ │ LLM Client │ │ Keystone gRPC Service │
26
+ │ (Cursor, Claude, etc.) │ │ ┌───────────────────┐ │
27
+ │ │ │ │ │ IPFS write gate │ │
28
+ │ │ MCP protocol │ │ │ Agent registry │ │
29
+ │ │ (stdio) │ │ │ Envelope storage │ │
30
+ │ ▼ │ gRPC │ │ Auth + rate limits│ │
31
+ │ ┌──────────────────┐ │◄──────────►│ └───────────────────┘ │
32
+ │ │ pft-chatbot-mcp │ │ TLS │ │
33
+ │ │ │ │ │ ┌───────────────────┐ │
34
+ │ │ • Signs txs │ │ │ │ PostgreSQL │ │
35
+ │ │ • Decrypts msgs │ │ │ └───────────────────┘ │
36
+ │ │ • Encrypts msgs │ │ │ │
37
+ │ └──────┬───────────┘ │ │ ┌───────────────────┐ │
38
+ │ │ │ │ │ IPFS Cluster │ │
39
+ │ │ JSON-RPC/WSS │ │ │ (public gateways) │ │
40
+ │ ▼ │ │ └───────────────────┘ │
41
+ │ PFTL Chain (testnet) │ └─────────────────────────┘
42
+ └──────────────────────────┘
43
+ ```
44
+
45
+ **Key security property**: Private keys never leave your machine. All signing and decryption happen locally. The gRPC service only handles IPFS writes (authenticated) and registry operations.
46
+
47
+ ### Message Flow
48
+
49
+ 1. **Sender** encrypts message content with XChaCha20-Poly1305 (multi-recipient, using X25519 key wrapping)
50
+ 2. Encrypted payload is uploaded to **IPFS** via the Keystone gRPC write gate
51
+ 3. A small protobuf-encoded pointer (`pf.ptr.v4.Pointer`) is attached as a memo to a **Payment** transaction on the PFTL chain
52
+ 4. **Recipient bot** scans the chain for transactions to its address, reads the pointer, fetches the payload from IPFS via public gateways, and decrypts locally
53
+
54
+ ### Encryption
55
+
56
+ Messages use the same encryption scheme as the pftasks frontend:
57
+
58
+ - **Content encryption**: XChaCha20-Poly1305 (libsodium)
59
+ - **Key wrapping**: X25519 (Diffie-Hellman key agreement)
60
+ - **Key derivation**: Bot's Ed25519 keypair (from PFTL wallet) is converted to X25519 for encryption
61
+ - **Multi-recipient**: Each message wraps the symmetric key for both sender and recipient, so both parties can decrypt
62
+
63
+ ## Quick Start
64
+
65
+ ### 1. Prerequisites
66
+
67
+ - Node.js >= 20
68
+ - An MCP-compatible LLM client (Cursor, Claude Desktop, etc.)
69
+
70
+ ### 2. Install
71
+
72
+ **From npm** (when published):
73
+ ```bash
74
+ npx pft-chatbot-mcp
75
+ ```
76
+
77
+ **From source**:
78
+ ```bash
79
+ git clone <repo-url>
80
+ cd pft-chatbot-mcp
81
+ npm install
82
+ ```
83
+
84
+ ### 3. Configure Your LLM Client
85
+
86
+ Copy `mcp.json.example` to your LLM client's MCP configuration location.
87
+
88
+ **If you already have a wallet**, add your seed:
89
+
90
+ **For Cursor** (`.cursor/mcp.json`):
91
+ ```json
92
+ {
93
+ "mcpServers": {
94
+ "pft-chatbot-mcp": {
95
+ "command": "npx",
96
+ "args": ["tsx", "src/index.ts"],
97
+ "env": {
98
+ "BOT_SEED": "sEdYourBotSeedHere"
99
+ }
100
+ }
101
+ }
102
+ }
103
+ ```
104
+
105
+ **For Claude Desktop** (`claude_desktop_config.json`):
106
+ ```json
107
+ {
108
+ "mcpServers": {
109
+ "pft-chatbot-mcp": {
110
+ "command": "npx",
111
+ "args": ["tsx", "/absolute/path/to/pft-chatbot-mcp/src/index.ts"],
112
+ "env": {
113
+ "BOT_SEED": "sEdYourBotSeedHere"
114
+ }
115
+ }
116
+ }
117
+ }
118
+ ```
119
+
120
+ **If you don't have a wallet yet**, omit the `BOT_SEED` line -- the server will start in setup mode with the `create_wallet` tool available. See [Wallet Setup](#wallet-setup) below.
121
+
122
+ All other configuration has sensible testnet defaults. See [Environment Variables](#environment-variables) for advanced overrides.
123
+
124
+ ### 4. Wallet Setup
125
+
126
+ If you need a new wallet, the server can start without a seed. Tell your LLM:
127
+
128
+ > "Create a new PFTL wallet for my bot"
129
+
130
+ This calls `create_wallet` and returns your new wallet address and seed. Then:
131
+
132
+ 1. **Save the seed securely** (it's shown once and is the only way to access the wallet)
133
+ 2. **Deposit at least 10 PFT** to the wallet address to activate it on-chain (via the [pftasks UI](https://tasknode.postfiat.org) or another wallet)
134
+ 3. **Add the seed** to your MCP configuration as `BOT_SEED` and restart
135
+
136
+ For a detailed walkthrough, see **[docs/WALLET_SETUP.md](docs/WALLET_SETUP.md)**.
137
+
138
+ ### 5. First Run
139
+
140
+ Once your wallet is configured and activated, tell your LLM:
141
+
142
+ > "Register my bot as 'My Bot' with description 'A helpful assistant' and capabilities ['text-generation']"
143
+
144
+ This will:
145
+ 1. Prove wallet ownership via Ed25519 challenge-response
146
+ 2. Provision an API key (cached locally in `.keystone-api-key`)
147
+ 3. Register the bot in the public agent directory
148
+
149
+ Then try:
150
+
151
+ > "Scan for new messages"
152
+
153
+ For a complete working example with tiered responses (text + image based on PFT amount), see **[docs/HELLO_WORLD_BOT.md](docs/HELLO_WORLD_BOT.md)**.
154
+
155
+ ## Tools Reference
156
+
157
+ ### create_wallet
158
+
159
+ Generates a new PFTL wallet locally. No network connection is required. The wallet must receive a deposit of at least **10 PFT** before it is active on-chain.
160
+
161
+ | Parameter | Type | Required | Default | Description |
162
+ |-----------|------|----------|---------|-------------|
163
+ | `algorithm` | `string` | No | `"ed25519"` | Key algorithm: `"ed25519"` (recommended) or `"secp256k1"` |
164
+
165
+ **Returns**: JSON with `address` (the r-address), `seed` (family seed -- save this!), `public_key`, `key_algorithm`, activation instructions, and next steps.
166
+
167
+ **Important**:
168
+ - The seed is displayed once. Copy and store it securely before doing anything else.
169
+ - The wallet does not exist on-chain until it receives at least 10 PFT.
170
+ - This tool is available even when no `BOT_SEED` is configured (setup mode).
171
+
172
+ ---
173
+
174
+ ### scan_messages
175
+
176
+ Scans recent transactions on the bot's wallet for incoming/outgoing messages. Returns metadata only (no decryption) -- use `get_message` to read content. Returns a `next_cursor` value for pagination/deduplication.
177
+
178
+ | Parameter | Type | Required | Default | Description |
179
+ |-----------|------|----------|---------|-------------|
180
+ | `since_ledger` | `number` | No | - | Only return messages from this ledger index onwards (use `next_cursor` from previous scan) |
181
+ | `limit` | `number` | No | `100` | Max transactions to scan (1-200) |
182
+ | `direction` | `string` | No | `"inbound"` | Filter: `"inbound"`, `"outbound"`, or `"both"` |
183
+
184
+ **Returns**: JSON object with:
185
+ - `messages` -- array of message objects, each with `tx_hash`, `sender`, `recipient`, `direction`, `amount_drops` (PFT in drops), `amount_pft` (PFT in whole units), `issued_currency` (for non-PFT tokens, or `null`), `cid`, `thread_id`, `is_encrypted`, `ledger_index`, `timestamp_iso`
186
+ - `count` -- number of messages found
187
+ - `next_cursor` -- ledger index to pass as `since_ledger` on the next call (for deduplication)
188
+
189
+ ---
190
+
191
+ ### get_message
192
+
193
+ Fetches and decrypts a specific message by its transaction hash or IPFS CID. Provide at least one.
194
+
195
+ | Parameter | Type | Required | Default | Description |
196
+ |-----------|------|----------|---------|-------------|
197
+ | `tx_hash` | `string` | No* | - | Transaction hash to look up |
198
+ | `cid` | `string` | No* | - | IPFS CID of the encrypted payload |
199
+
200
+ *At least one of `tx_hash` or `cid` must be provided.
201
+
202
+ **Returns**: JSON with `tx_hash`, `cid`, `sender`, `recipient`, `message` (decrypted plaintext), `content_type`, `amount_drops`, `thread_id`, `timestamp`.
203
+
204
+ ---
205
+
206
+ ### send_message
207
+
208
+ Encrypts a message, uploads to IPFS, and submits a Payment transaction on the PFTL chain with PFT.
209
+
210
+ | Parameter | Type | Required | Default | Description |
211
+ |-----------|------|----------|---------|-------------|
212
+ | `recipient` | `string` | **Yes** | - | Recipient's PFTL r-address |
213
+ | `message` | `string` | **Yes** | - | Message text to send |
214
+ | `content_type` | `string` | No | `"text"` | MIME type of the content |
215
+ | `amount_pft` | `string` | No | - | PFT amount to send (e.g. `"10"` for 10 PFT). Converted to drops automatically. |
216
+ | `amount_drops` | `string` | No | `"1"` | PFT in drops for fine control (1 PFT = 1,000,000 drops). Ignored if `amount_pft` is set. |
217
+ | `attachments` | `array` | No | - | Array of IPFS content to attach (see below) |
218
+ | `reply_to_tx` | `string` | No | - | Transaction hash this replies to |
219
+ | `thread_id` | `string` | No | - | Thread ID to continue a conversation |
220
+
221
+ Each attachment object: `{ cid: string, content_type: string, filename?: string }`
222
+
223
+ **Returns**: JSON with `tx_hash`, `cid`, `thread_id`, `recipient`, `amount_pft`, `amount_drops`, `result`.
224
+
225
+ **Example -- sending an image:**
226
+
227
+ ```
228
+ 1. upload_content({ content: "<base64 PNG>", content_type: "image/png", encoding: "base64" })
229
+ → { cid: "bafk...", uri: "ipfs://bafk..." }
230
+
231
+ 2. send_message({
232
+ recipient: "rBot...",
233
+ message: "Here's the chart you requested",
234
+ attachments: [{ cid: "bafk...", content_type: "image/png", filename: "chart.png" }]
235
+ })
236
+ ```
237
+
238
+ ---
239
+
240
+ ### register_bot
241
+
242
+ Registers the bot in the Keystone agent registry. On first call, performs an Ed25519 challenge-response to prove wallet ownership and provisions an API key.
243
+
244
+ | Parameter | Type | Required | Default | Description |
245
+ |-----------|------|----------|---------|-------------|
246
+ | `name` | `string` | **Yes** | - | Display name for the bot |
247
+ | `description` | `string` | **Yes** | - | Short description of what the bot does |
248
+ | `capabilities` | `string[]` | **Yes** | - | Capability tags (e.g. `["text-generation", "image-generation"]`) |
249
+ | `url` | `string` | No | - | Bot homepage or documentation URL |
250
+
251
+ **Returns**: JSON with `agent_id`, `wallet_address`, `name`, `capabilities`, `registered: true`.
252
+
253
+ ---
254
+
255
+ ### search_bots
256
+
257
+ Searches the public agent registry for other bots by name, description, or capability.
258
+
259
+ | Parameter | Type | Required | Default | Description |
260
+ |-----------|------|----------|---------|-------------|
261
+ | `query` | `string` | No | - | Free-text search (matches name/description) |
262
+ | `capabilities` | `string[]` | No | - | Filter by capability tags |
263
+ | `limit` | `number` | No | `20` | Max results (1-100) |
264
+
265
+ **Returns**: JSON with `total_count` and `results` array, each containing `agent_id`, `name`, `description`, `wallet_address`, `capabilities`, `relevance_score`.
266
+
267
+ ---
268
+
269
+ ### upload_content
270
+
271
+ Uploads arbitrary content to IPFS via the authenticated Keystone gRPC write gate. Useful for uploading images, documents, or structured data that will be referenced in messages.
272
+
273
+ | Parameter | Type | Required | Default | Description |
274
+ |-----------|------|----------|---------|-------------|
275
+ | `content` | `string` | **Yes** | - | Content to upload (text, JSON, or base64 for binary) |
276
+ | `content_type` | `string` | **Yes** | - | MIME type (e.g. `"image/png"`, `"application/json"`) |
277
+ | `encoding` | `string` | No | `"utf8"` | `"utf8"` for text or `"base64"` for binary |
278
+
279
+ **Returns**: JSON with `cid`, `uri` (`ipfs://` URI), `content_type`, `size` (bytes).
280
+
281
+ ---
282
+
283
+ ### get_thread
284
+
285
+ Fetches all messages in a conversation, either by thread ID or contact address. Optionally decrypts all messages.
286
+
287
+ | Parameter | Type | Required | Default | Description |
288
+ |-----------|------|----------|---------|-------------|
289
+ | `thread_id` | `string` | No* | - | Thread ID to fetch messages for |
290
+ | `contact_address` | `string` | No* | - | Wallet address to fetch all messages with |
291
+ | `limit` | `number` | No | `200` | Max transactions to scan (1-200) |
292
+ | `decrypt` | `boolean` | No | `true` | Whether to decrypt message contents |
293
+
294
+ *At least one of `thread_id` or `contact_address` must be provided.
295
+
296
+ **Returns**: JSON with `thread_id`, `contact_address`, `message_count`, and chronologically sorted `messages` array. Each message includes `tx_hash`, `sender`, `recipient`, `direction`, `amount_drops`, `timestamp`, `cid`, and if decrypted: `message`, `content_type`.
297
+
298
+ ## Bot Lifecycle
299
+
300
+ ```
301
+ ┌──────────────┐
302
+ │ create_wallet │ Generate a new wallet (if you don't have one)
303
+ │ (optional) │
304
+ └──────┬───────┘
305
+ │ deposit ≥ 10 PFT, configure BOT_SEED, restart
306
+
307
+ ┌─────────────┐
308
+ │ register │ Prove wallet ownership, get API key, register in directory
309
+ │ (once) │
310
+ └──────┬──────┘
311
+
312
+ ┌─────────────┐
313
+ │ scan │ Poll for new incoming messages
314
+ │ (loop) │◄──────────────────────────┐
315
+ └──────┬──────┘ │
316
+ ▼ │
317
+ ┌─────────────┐ │
318
+ │ get_message │ Decrypt and read content │
319
+ └──────┬──────┘ │
320
+ ▼ │
321
+ ┌─────────────┐ │
322
+ │ process │ LLM generates response │
323
+ │ (your logic)│ │
324
+ └──────┬──────┘ │
325
+ ▼ │
326
+ ┌─────────────┐ │
327
+ │ send_message│ Encrypt, upload, submit │
328
+ └──────┬──────┘ │
329
+ └───────────────────────────────────┘
330
+ ```
331
+
332
+ ## Environment Variables
333
+
334
+ | Variable | Required | Default | Description |
335
+ |----------|----------|---------|-------------|
336
+ | `BOT_SEED` | Yes* | - | Wallet family seed or hex seed |
337
+ | `BOT_SEED_FILE` | Yes* | - | Path to file containing the seed (alternative to `BOT_SEED`) |
338
+ | `KEYSTONE_API_KEY` | Auto | - | Auto-provisioned on first `register_bot` call |
339
+ | `PFTL_RPC_URL` | No | `https://rpc.testnet.postfiat.org` | Chain JSON-RPC endpoint |
340
+ | `PFTL_WSS_URL` | No | `wss://rpc.testnet.postfiat.org:6008` | Chain WebSocket endpoint |
341
+ | `IPFS_GATEWAY_URL` | No | `https://pft-ipfs-testnet-node-1.fly.dev` | Primary IPFS gateway for reads |
342
+ | `KEYSTONE_GRPC_URL` | No | `keystone-grpc.postfiat.org:443` | Keystone gRPC service |
343
+
344
+ *Exactly one of `BOT_SEED` or `BOT_SEED_FILE` is required.
345
+
346
+ ## Security Considerations
347
+
348
+ ### Wallet Seed Handling
349
+
350
+ The bot's wallet seed is the most sensitive piece of configuration. Here's how it's handled:
351
+
352
+ 1. **The seed never leaves your machine.** All signing and decryption happen in the local MCP server process.
353
+
354
+ 2. **The gRPC service never sees your seed.** Authentication uses Ed25519 challenge-response: the server sends a random nonce, the bot signs it locally, and the server verifies the signature against the on-chain public key. No secret material is transmitted.
355
+
356
+ 3. **Two options for providing the seed:**
357
+
358
+ - **`BOT_SEED` in mcp.json env** -- Simple, fine for development. The `.cursor/mcp.json` file is in Cursor's global gitignore, so it won't be accidentally committed. However, the seed will be visible in the process environment (`/proc/PID/environ` on Linux, `ps eww` on macOS).
359
+
360
+ - **`BOT_SEED_FILE`** (recommended for production) -- Point to a file with restricted permissions (`chmod 600`). The seed is read once at startup and not stored in the process environment.
361
+
362
+ ```bash
363
+ # Create a seed file with restricted permissions
364
+ echo "sEdYourSeed" > ~/.pft-bot-seed
365
+ chmod 600 ~/.pft-bot-seed
366
+ ```
367
+
368
+ ```json
369
+ {
370
+ "mcpServers": {
371
+ "pft-chatbot-mcp": {
372
+ "command": "npx",
373
+ "args": ["tsx", "src/index.ts"],
374
+ "env": {
375
+ "BOT_SEED_FILE": "/Users/you/.pft-bot-seed"
376
+ }
377
+ }
378
+ }
379
+ }
380
+ ```
381
+
382
+ 4. **Use a dedicated bot wallet.** Do not use your personal wallet. Create a new wallet with minimal funds specifically for the bot. The wallet only needs enough PFT for transaction fees.
383
+
384
+ 5. **API key caching.** The provisioned API key is cached in `.keystone-api-key` in the project root with `0600` permissions. Add this file to your `.gitignore`.
385
+
386
+ ### Rate Limits
387
+
388
+ The Keystone gRPC service enforces per-API-key rate limits:
389
+ - **500 writes/hour** (IPFS uploads, envelope storage)
390
+ - **5,000 reads/hour** (registry lookups, envelope queries)
391
+
392
+ ### On-Chain Identity
393
+
394
+ Bot registration requires proving control of a PFTL wallet address by:
395
+ 1. The wallet must have an active PFT trust line
396
+ 2. The bot must sign a challenge nonce with the wallet's Ed25519 key
397
+ 3. The signature is verified against the on-chain public key
398
+
399
+ ## Development
400
+
401
+ ```bash
402
+ # Run the MCP server directly
403
+ BOT_SEED=sEdYourSeed npx tsx src/index.ts
404
+
405
+ # Watch mode (auto-restart on changes)
406
+ BOT_SEED=sEdYourSeed npm run dev
407
+
408
+ # Type check
409
+ npm run lint
410
+
411
+ # Build to dist/
412
+ npm run build
413
+ ```
414
+
415
+ ### Project Structure
416
+
417
+ ```
418
+ src/
419
+ ├── index.ts # Entry point, MCP server setup, tool registration
420
+ ├── version.ts # Version constants (MCP, Keystone, pf.ptr)
421
+ ├── config.ts # Environment/config loading
422
+ ├── chain/
423
+ │ ├── pointer.ts # Protobuf memo encoding/decoding (pf.ptr.v4 + Keystone)
424
+ │ ├── scanner.ts # Chain transaction scanning
425
+ │ └── submitter.ts # Transaction signing and submission
426
+ ├── crypto/
427
+ │ ├── keys.ts # Keypair derivation (Ed25519 → X25519)
428
+ │ ├── encrypt.ts # Multi-recipient encryption
429
+ │ └── decrypt.ts # Payload decryption
430
+ ├── grpc/
431
+ │ ├── client.ts # Keystone gRPC client
432
+ │ └── protos/ # Proto definitions (subset of keystone-protocol)
433
+ ├── ipfs/
434
+ │ └── gateway.ts # Direct IPFS gateway reads
435
+ └── tools/
436
+ ├── create_wallet.ts # create_wallet tool (no seed required)
437
+ ├── scan_messages.ts # scan_messages tool
438
+ ├── get_message.ts # get_message tool
439
+ ├── send_message.ts # send_message tool
440
+ ├── register_bot.ts # register_bot tool
441
+ ├── search_bots.ts # search_bots tool
442
+ ├── upload_content.ts # upload_content tool
443
+ └── get_thread.ts # get_thread tool
444
+ ```
445
+
446
+ ## FAQ
447
+
448
+ **Q: Do I need to run my own IPFS node?**
449
+ No. Reads go through public IPFS gateways. Writes go through the Keystone gRPC service which handles IPFS pinning.
450
+
451
+ **Q: What chain does this run on?**
452
+ PFTL, a standalone blockchain with PFT as its native currency. It uses the same transaction format and cryptography (Ed25519, secp256k1) as XRPL-family chains, but is its own network.
453
+
454
+ **Q: Can I use this with Claude Desktop / other MCP clients?**
455
+ Yes. Any MCP-compatible client that supports stdio transport works. See the configuration examples above.
456
+
457
+ **Q: How do I get a PFTL wallet?**
458
+ Use the `create_wallet` tool -- it works even without an existing seed. Start the MCP server without `BOT_SEED` and tell your LLM to create a wallet. You'll get a new address and seed. Then deposit at least 10 PFT to activate it (via [pftasks](https://tasknode.postfiat.org) or from another wallet), set the seed in your config, and restart. See [docs/WALLET_SETUP.md](docs/WALLET_SETUP.md) for a full walkthrough.
459
+
460
+ **Q: How much does it cost to send a message?**
461
+ Each message is a Payment transaction on the PFTL chain, which costs a small amount of PFT in fees (typically < 0.001 PFT). The `amount_pft` parameter controls how much PFT to include in the payment itself, or use `amount_drops` for fine control (1 PFT = 1,000,000 drops, default: 1 drop).
@@ -0,0 +1,65 @@
1
+ export declare const POINTER_FLAGS: {
2
+ readonly encrypted: 1;
3
+ readonly public: 2;
4
+ readonly ephemeral: 4;
5
+ readonly tombstone: 8;
6
+ readonly multipart: 16;
7
+ };
8
+ export type MemoType = "pf.ptr" | "keystone" | "unknown";
9
+ export interface DecodedPfPointer {
10
+ type: "pf.ptr";
11
+ cid: string;
12
+ target: string;
13
+ kind: string;
14
+ schema: number;
15
+ taskId: string;
16
+ threadId: string;
17
+ contextId: string;
18
+ flags: number;
19
+ isEncrypted: boolean;
20
+ }
21
+ export interface DecodedKeystoneEnvelope {
22
+ type: "keystone";
23
+ version: number;
24
+ contentHash: Buffer;
25
+ messageType: string;
26
+ encryption: string;
27
+ publicReferences: Array<{
28
+ contentHash: Buffer;
29
+ groupId: string;
30
+ referenceType: string;
31
+ annotation: string;
32
+ }>;
33
+ message: Buffer;
34
+ metadata: Record<string, string>;
35
+ }
36
+ export type DecodedMemo = DecodedPfPointer | DecodedKeystoneEnvelope | null;
37
+ /**
38
+ * Identify the memo type from hex-encoded MemoType and MemoFormat fields.
39
+ */
40
+ export declare function identifyMemoType(memoTypeHex: string, memoFormatHex: string): MemoType;
41
+ /**
42
+ * Decode a pf.ptr.v4.Pointer from hex-encoded MemoData.
43
+ */
44
+ export declare function decodePfPointer(memoDataHex: string): Promise<DecodedPfPointer>;
45
+ /**
46
+ * Decode a KeystoneEnvelope from hex-encoded MemoData.
47
+ */
48
+ export declare function decodeKeystoneEnvelope(memoDataHex: string): Promise<DecodedKeystoneEnvelope>;
49
+ /**
50
+ * Build a pf.ptr.v4.Pointer memo for sending messages.
51
+ * Returns hex-encoded memo fields ready for PFTL transaction.
52
+ */
53
+ export declare function buildPfPointerMemo(input: {
54
+ cid: string;
55
+ kind?: string;
56
+ schema?: number;
57
+ threadId?: string;
58
+ contextId?: string;
59
+ flags?: number;
60
+ }): Promise<{
61
+ memoTypeHex: string;
62
+ memoFormatHex: string;
63
+ memoDataHex: string;
64
+ }>;
65
+ //# sourceMappingURL=pointer.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"pointer.d.ts","sourceRoot":"","sources":["../../src/chain/pointer.ts"],"names":[],"mappings":"AAcA,eAAO,MAAM,aAAa;;;;;;CAMhB,CAAC;AAEX,MAAM,MAAM,QAAQ,GAAG,QAAQ,GAAG,UAAU,GAAG,SAAS,CAAC;AAEzD,MAAM,WAAW,gBAAgB;IAC/B,IAAI,EAAE,QAAQ,CAAC;IACf,GAAG,EAAE,MAAM,CAAC;IACZ,MAAM,EAAE,MAAM,CAAC;IACf,IAAI,EAAE,MAAM,CAAC;IACb,MAAM,EAAE,MAAM,CAAC;IACf,MAAM,EAAE,MAAM,CAAC;IACf,QAAQ,EAAE,MAAM,CAAC;IACjB,SAAS,EAAE,MAAM,CAAC;IAClB,KAAK,EAAE,MAAM,CAAC;IACd,WAAW,EAAE,OAAO,CAAC;CACtB;AAED,MAAM,WAAW,uBAAuB;IACtC,IAAI,EAAE,UAAU,CAAC;IACjB,OAAO,EAAE,MAAM,CAAC;IAChB,WAAW,EAAE,MAAM,CAAC;IACpB,WAAW,EAAE,MAAM,CAAC;IACpB,UAAU,EAAE,MAAM,CAAC;IACnB,gBAAgB,EAAE,KAAK,CAAC;QACtB,WAAW,EAAE,MAAM,CAAC;QACpB,OAAO,EAAE,MAAM,CAAC;QAChB,aAAa,EAAE,MAAM,CAAC;QACtB,UAAU,EAAE,MAAM,CAAC;KACpB,CAAC,CAAC;IACH,OAAO,EAAE,MAAM,CAAC;IAChB,QAAQ,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,CAAC;CAClC;AAED,MAAM,MAAM,WAAW,GAAG,gBAAgB,GAAG,uBAAuB,GAAG,IAAI,CAAC;AAmB5E;;GAEG;AACH,wBAAgB,gBAAgB,CAC9B,WAAW,EAAE,MAAM,EACnB,aAAa,EAAE,MAAM,GACpB,QAAQ,CAcV;AAED;;GAEG;AACH,wBAAsB,eAAe,CACnC,WAAW,EAAE,MAAM,GAClB,OAAO,CAAC,gBAAgB,CAAC,CAiB3B;AAED;;GAEG;AACH,wBAAsB,sBAAsB,CAC1C,WAAW,EAAE,MAAM,GAClB,OAAO,CAAC,uBAAuB,CAAC,CAwBlC;AAED;;;GAGG;AACH,wBAAsB,kBAAkB,CAAC,KAAK,EAAE;IAC9C,GAAG,EAAE,MAAM,CAAC;IACZ,IAAI,CAAC,EAAE,MAAM,CAAC;IACd,MAAM,CAAC,EAAE,MAAM,CAAC;IAChB,QAAQ,CAAC,EAAE,MAAM,CAAC;IAClB,SAAS,CAAC,EAAE,MAAM,CAAC;IACnB,KAAK,CAAC,EAAE,MAAM,CAAC;CAChB,GAAG,OAAO,CAAC;IACV,WAAW,EAAE,MAAM,CAAC;IACpB,aAAa,EAAE,MAAM,CAAC;IACtB,WAAW,EAAE,MAAM,CAAC;CACrB,CAAC,CAwBD"}
@@ -0,0 +1,116 @@
1
+ import protobuf from "protobufjs";
2
+ import { resolve, dirname } from "node:path";
3
+ import { fileURLToPath } from "node:url";
4
+ const __dirname = dirname(fileURLToPath(import.meta.url));
5
+ const PROTO_DIR = resolve(__dirname, "..", "grpc", "protos");
6
+ // Memo type/format constants (hex-encoded)
7
+ const PF_PTR_MEMO_TYPE_HEX = "70662e707472"; // "pf.ptr"
8
+ const PF_PTR_MEMO_FORMAT_HEX = "7634"; // "v4"
9
+ const KEYSTONE_MEMO_TYPE_HEX = "6b657973746f6e65"; // "keystone"
10
+ const KEYSTONE_MEMO_FORMAT_HEX = "7631"; // "v1"
11
+ // Pointer flag bitmask (from pftasks)
12
+ export const POINTER_FLAGS = {
13
+ encrypted: 0x01,
14
+ public: 0x02,
15
+ ephemeral: 0x04,
16
+ tombstone: 0x08,
17
+ multipart: 0x10,
18
+ };
19
+ let pfPointerType = null;
20
+ let keystoneEnvelopeType = null;
21
+ async function loadProtos() {
22
+ if (pfPointerType && keystoneEnvelopeType)
23
+ return;
24
+ const pfRoot = await protobuf.load(resolve(PROTO_DIR, "pf/ptr/v4/pointer.proto"));
25
+ pfPointerType = pfRoot.lookupType("pf.ptr.v4.Pointer");
26
+ const ksRoot = await protobuf.load(resolve(PROTO_DIR, "keystone/v1/core/envelope.proto"));
27
+ keystoneEnvelopeType = ksRoot.lookupType("keystone.v1.core.KeystoneEnvelope");
28
+ }
29
+ /**
30
+ * Identify the memo type from hex-encoded MemoType and MemoFormat fields.
31
+ */
32
+ export function identifyMemoType(memoTypeHex, memoFormatHex) {
33
+ if (memoTypeHex === PF_PTR_MEMO_TYPE_HEX &&
34
+ memoFormatHex === PF_PTR_MEMO_FORMAT_HEX) {
35
+ return "pf.ptr";
36
+ }
37
+ if (memoTypeHex === KEYSTONE_MEMO_TYPE_HEX &&
38
+ memoFormatHex === KEYSTONE_MEMO_FORMAT_HEX) {
39
+ return "keystone";
40
+ }
41
+ return "unknown";
42
+ }
43
+ /**
44
+ * Decode a pf.ptr.v4.Pointer from hex-encoded MemoData.
45
+ */
46
+ export async function decodePfPointer(memoDataHex) {
47
+ await loadProtos();
48
+ const bytes = Buffer.from(memoDataHex, "hex");
49
+ const decoded = pfPointerType.decode(bytes);
50
+ return {
51
+ type: "pf.ptr",
52
+ cid: decoded.cid || "",
53
+ target: decoded.target || "TARGET_UNSPECIFIED",
54
+ kind: decoded.kind || "CONTENT_KIND_UNSPECIFIED",
55
+ schema: decoded.schema || 0,
56
+ taskId: decoded.taskId || "",
57
+ threadId: decoded.threadId || "",
58
+ contextId: decoded.contextId || "",
59
+ flags: decoded.flags || 0,
60
+ isEncrypted: (decoded.flags & POINTER_FLAGS.encrypted) !== 0,
61
+ };
62
+ }
63
+ /**
64
+ * Decode a KeystoneEnvelope from hex-encoded MemoData.
65
+ */
66
+ export async function decodeKeystoneEnvelope(memoDataHex) {
67
+ await loadProtos();
68
+ const bytes = Buffer.from(memoDataHex, "hex");
69
+ const decoded = keystoneEnvelopeType.decode(bytes);
70
+ return {
71
+ type: "keystone",
72
+ version: decoded.version || 1,
73
+ contentHash: decoded.contentHash
74
+ ? Buffer.from(decoded.contentHash)
75
+ : Buffer.alloc(0),
76
+ messageType: decoded.messageType || "MESSAGE_TYPE_UNSPECIFIED",
77
+ encryption: decoded.encryption || "ENCRYPTION_MODE_UNSPECIFIED",
78
+ publicReferences: (decoded.publicReferences || []).map((ref) => ({
79
+ contentHash: ref.contentHash
80
+ ? Buffer.from(ref.contentHash)
81
+ : Buffer.alloc(0),
82
+ groupId: ref.groupId || "",
83
+ referenceType: ref.referenceType || "CONTEXT_REFERENCE_TYPE_UNSPECIFIED",
84
+ annotation: ref.annotation || "",
85
+ })),
86
+ message: decoded.message ? Buffer.from(decoded.message) : Buffer.alloc(0),
87
+ metadata: decoded.metadata || {},
88
+ };
89
+ }
90
+ /**
91
+ * Build a pf.ptr.v4.Pointer memo for sending messages.
92
+ * Returns hex-encoded memo fields ready for PFTL transaction.
93
+ */
94
+ export async function buildPfPointerMemo(input) {
95
+ await loadProtos();
96
+ const payload = {
97
+ cid: input.cid,
98
+ target: "TARGET_CONTENT_BLOB",
99
+ kind: input.kind || "CHAT",
100
+ schema: input.schema || 1,
101
+ threadId: input.threadId || "",
102
+ contextId: input.contextId || "",
103
+ flags: input.flags ?? POINTER_FLAGS.encrypted,
104
+ };
105
+ const err = pfPointerType.verify(payload);
106
+ if (err)
107
+ throw new Error(`Invalid pointer payload: ${err}`);
108
+ const message = pfPointerType.create(payload);
109
+ const bytes = pfPointerType.encode(message).finish();
110
+ return {
111
+ memoTypeHex: PF_PTR_MEMO_TYPE_HEX,
112
+ memoFormatHex: PF_PTR_MEMO_FORMAT_HEX,
113
+ memoDataHex: Buffer.from(bytes).toString("hex"),
114
+ };
115
+ }
116
+ //# sourceMappingURL=pointer.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"pointer.js","sourceRoot":"","sources":["../../src/chain/pointer.ts"],"names":[],"mappings":"AAAA,OAAO,QAAQ,MAAM,YAAY,CAAC;AAClC,OAAO,EAAE,OAAO,EAAE,OAAO,EAAE,MAAM,WAAW,CAAC;AAC7C,OAAO,EAAE,aAAa,EAAE,MAAM,UAAU,CAAC;AAEzC,MAAM,SAAS,GAAG,OAAO,CAAC,aAAa,CAAC,MAAM,CAAC,IAAI,CAAC,GAAG,CAAC,CAAC,CAAC;AAC1D,MAAM,SAAS,GAAG,OAAO,CAAC,SAAS,EAAE,IAAI,EAAE,MAAM,EAAE,QAAQ,CAAC,CAAC;AAE7D,2CAA2C;AAC3C,MAAM,oBAAoB,GAAG,cAAc,CAAC,CAAC,WAAW;AACxD,MAAM,sBAAsB,GAAG,MAAM,CAAC,CAAC,OAAO;AAC9C,MAAM,sBAAsB,GAAG,kBAAkB,CAAC,CAAC,aAAa;AAChE,MAAM,wBAAwB,GAAG,MAAM,CAAC,CAAC,OAAO;AAEhD,sCAAsC;AACtC,MAAM,CAAC,MAAM,aAAa,GAAG;IAC3B,SAAS,EAAE,IAAI;IACf,MAAM,EAAE,IAAI;IACZ,SAAS,EAAE,IAAI;IACf,SAAS,EAAE,IAAI;IACf,SAAS,EAAE,IAAI;CACP,CAAC;AAmCX,IAAI,aAAa,GAAyB,IAAI,CAAC;AAC/C,IAAI,oBAAoB,GAAyB,IAAI,CAAC;AAEtD,KAAK,UAAU,UAAU;IACvB,IAAI,aAAa,IAAI,oBAAoB;QAAE,OAAO;IAElD,MAAM,MAAM,GAAG,MAAM,QAAQ,CAAC,IAAI,CAChC,OAAO,CAAC,SAAS,EAAE,yBAAyB,CAAC,CAC9C,CAAC;IACF,aAAa,GAAG,MAAM,CAAC,UAAU,CAAC,mBAAmB,CAAC,CAAC;IAEvD,MAAM,MAAM,GAAG,MAAM,QAAQ,CAAC,IAAI,CAChC,OAAO,CAAC,SAAS,EAAE,iCAAiC,CAAC,CACtD,CAAC;IACF,oBAAoB,GAAG,MAAM,CAAC,UAAU,CAAC,mCAAmC,CAAC,CAAC;AAChF,CAAC;AAED;;GAEG;AACH,MAAM,UAAU,gBAAgB,CAC9B,WAAmB,EACnB,aAAqB;IAErB,IACE,WAAW,KAAK,oBAAoB;QACpC,aAAa,KAAK,sBAAsB,EACxC,CAAC;QACD,OAAO,QAAQ,CAAC;IAClB,CAAC;IACD,IACE,WAAW,KAAK,sBAAsB;QACtC,aAAa,KAAK,wBAAwB,EAC1C,CAAC;QACD,OAAO,UAAU,CAAC;IACpB,CAAC;IACD,OAAO,SAAS,CAAC;AACnB,CAAC;AAED;;GAEG;AACH,MAAM,CAAC,KAAK,UAAU,eAAe,CACnC,WAAmB;IAEnB,MAAM,UAAU,EAAE,CAAC;IACnB,MAAM,KAAK,GAAG,MAAM,CAAC,IAAI,CAAC,WAAW,EAAE,KAAK,CAAC,CAAC;IAC9C,MAAM,OAAO,GAAG,aAAc,CAAC,MAAM,CAAC,KAAK,CAAQ,CAAC;IAEpD,OAAO;QACL,IAAI,EAAE,QAAQ;QACd,GAAG,EAAE,OAAO,CAAC,GAAG,IAAI,EAAE;QACtB,MAAM,EAAE,OAAO,CAAC,MAAM,IAAI,oBAAoB;QAC9C,IAAI,EAAE,OAAO,CAAC,IAAI,IAAI,0BAA0B;QAChD,MAAM,EAAE,OAAO,CAAC,MAAM,IAAI,CAAC;QAC3B,MAAM,EAAE,OAAO,CAAC,MAAM,IAAI,EAAE;QAC5B,QAAQ,EAAE,OAAO,CAAC,QAAQ,IAAI,EAAE;QAChC,SAAS,EAAE,OAAO,CAAC,SAAS,IAAI,EAAE;QAClC,KAAK,EAAE,OAAO,CAAC,KAAK,IAAI,CAAC;QACzB,WAAW,EAAE,CAAC,OAAO,CAAC,KAAK,GAAG,aAAa,CAAC,SAAS,CAAC,KAAK,CAAC;KAC7D,CAAC;AACJ,CAAC;AAED;;GAEG;AACH,MAAM,CAAC,KAAK,UAAU,sBAAsB,CAC1C,WAAmB;IAEnB,MAAM,UAAU,EAAE,CAAC;IACnB,MAAM,KAAK,GAAG,MAAM,CAAC,IAAI,CAAC,WAAW,EAAE,KAAK,CAAC,CAAC;IAC9C,MAAM,OAAO,GAAG,oBAAqB,CAAC,MAAM,CAAC,KAAK,CAAQ,CAAC;IAE3D,OAAO;QACL,IAAI,EAAE,UAAU;QAChB,OAAO,EAAE,OAAO,CAAC,OAAO,IAAI,CAAC;QAC7B,WAAW,EAAE,OAAO,CAAC,WAAW;YAC9B,CAAC,CAAC,MAAM,CAAC,IAAI,CAAC,OAAO,CAAC,WAAW,CAAC;YAClC,CAAC,CAAC,MAAM,CAAC,KAAK,CAAC,CAAC,CAAC;QACnB,WAAW,EAAE,OAAO,CAAC,WAAW,IAAI,0BAA0B;QAC9D,UAAU,EAAE,OAAO,CAAC,UAAU,IAAI,6BAA6B;QAC/D,gBAAgB,EAAE,CAAC,OAAO,CAAC,gBAAgB,IAAI,EAAE,CAAC,CAAC,GAAG,CAAC,CAAC,GAAQ,EAAE,EAAE,CAAC,CAAC;YACpE,WAAW,EAAE,GAAG,CAAC,WAAW;gBAC1B,CAAC,CAAC,MAAM,CAAC,IAAI,CAAC,GAAG,CAAC,WAAW,CAAC;gBAC9B,CAAC,CAAC,MAAM,CAAC,KAAK,CAAC,CAAC,CAAC;YACnB,OAAO,EAAE,GAAG,CAAC,OAAO,IAAI,EAAE;YAC1B,aAAa,EAAE,GAAG,CAAC,aAAa,IAAI,oCAAoC;YACxE,UAAU,EAAE,GAAG,CAAC,UAAU,IAAI,EAAE;SACjC,CAAC,CAAC;QACH,OAAO,EAAE,OAAO,CAAC,OAAO,CAAC,CAAC,CAAC,MAAM,CAAC,IAAI,CAAC,OAAO,CAAC,OAAO,CAAC,CAAC,CAAC,CAAC,MAAM,CAAC,KAAK,CAAC,CAAC,CAAC;QACzE,QAAQ,EAAE,OAAO,CAAC,QAAQ,IAAI,EAAE;KACjC,CAAC;AACJ,CAAC;AAED;;;GAGG;AACH,MAAM,CAAC,KAAK,UAAU,kBAAkB,CAAC,KAOxC;IAKC,MAAM,UAAU,EAAE,CAAC;IAEnB,MAAM,OAAO,GAAQ;QACnB,GAAG,EAAE,KAAK,CAAC,GAAG;QACd,MAAM,EAAE,qBAAqB;QAC7B,IAAI,EAAE,KAAK,CAAC,IAAI,IAAI,MAAM;QAC1B,MAAM,EAAE,KAAK,CAAC,MAAM,IAAI,CAAC;QACzB,QAAQ,EAAE,KAAK,CAAC,QAAQ,IAAI,EAAE;QAC9B,SAAS,EAAE,KAAK,CAAC,SAAS,IAAI,EAAE;QAChC,KAAK,EAAE,KAAK,CAAC,KAAK,IAAI,aAAa,CAAC,SAAS;KAC9C,CAAC;IAEF,MAAM,GAAG,GAAG,aAAc,CAAC,MAAM,CAAC,OAAO,CAAC,CAAC;IAC3C,IAAI,GAAG;QAAE,MAAM,IAAI,KAAK,CAAC,4BAA4B,GAAG,EAAE,CAAC,CAAC;IAE5D,MAAM,OAAO,GAAG,aAAc,CAAC,MAAM,CAAC,OAAO,CAAC,CAAC;IAC/C,MAAM,KAAK,GAAG,aAAc,CAAC,MAAM,CAAC,OAAO,CAAC,CAAC,MAAM,EAAE,CAAC;IAEtD,OAAO;QACL,WAAW,EAAE,oBAAoB;QACjC,aAAa,EAAE,sBAAsB;QACrC,WAAW,EAAE,MAAM,CAAC,IAAI,CAAC,KAAK,CAAC,CAAC,QAAQ,CAAC,KAAK,CAAC;KAChD,CAAC;AACJ,CAAC"}