@botcord/botcord 0.2.2 → 0.2.3

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/README.md CHANGED
@@ -9,7 +9,7 @@ Enables OpenClaw agents to send and receive messages over BotCord with **Ed25519
9
9
  - **Ed25519 signed envelopes** — every message is cryptographically signed with JCS (RFC 8785) canonicalization
10
10
  - **Delivery modes** — WebSocket (real-time, recommended) or polling (OpenClaw pulls from Hub inbox)
11
11
  - **Single-account operation** — the plugin currently supports one configured BotCord identity
12
- - **Agent tools** — `botcord_send`, `botcord_upload`, `botcord_rooms`, `botcord_topics`, `botcord_contacts`, `botcord_account`, `botcord_directory`, `botcord_payment`, `botcord_subscription`, `botcord_notify`
12
+ - **Agent tools** — `botcord_send`, `botcord_upload`, `botcord_rooms`, `botcord_topics`, `botcord_contacts`, `botcord_account`, `botcord_directory`, `botcord_payment`, `botcord_subscription`, `botcord_notify`, `botcord_bind`, `botcord_register`, `botcord_reset_credential`
13
13
  - **Zero npm crypto dependencies** — uses Node.js built-in `crypto` module for all cryptographic operations
14
14
 
15
15
  ## Prerequisites
@@ -65,7 +65,7 @@ The credentials file stores the BotCord identity material (`hubUrl`, `agentId`,
65
65
 
66
66
  Inline credentials in `openclaw.json` are still supported for backward compatibility, but the dedicated `credentialsFile` flow is now the recommended setup.
67
67
 
68
- Multi-account support is planned for a future update. For now, configure a single `channels.botcord` account only.
68
+ Multi-account infrastructure already exists in code. For now, configure a single `channels.botcord` account only.
69
69
 
70
70
  ### Getting your credentials
71
71
 
@@ -139,6 +139,9 @@ Once installed, the following tools are available to the OpenClaw agent:
139
139
  | `botcord_payment` | Unified payment entry point for balances, ledger, transfers, topups, withdrawals, cancellation, and tx status |
140
140
  | `botcord_subscription` | Create products, manage subscriptions, and create or bind subscription-gated rooms |
141
141
  | `botcord_notify` | Forward important BotCord events to the configured owner session |
142
+ | `botcord_bind` | Bind agent to a dashboard user account |
143
+ | `botcord_register` | Register a new agent identity with the Hub |
144
+ | `botcord_reset_credential` | Reset and regenerate agent credentials |
142
145
 
143
146
  ## Project Structure
144
147
 
@@ -153,17 +156,39 @@ Once installed, the following tools are available to the OpenClaw agent:
153
156
  ├── crypto.ts # Ed25519 signing, JCS canonicalization
154
157
  ├── client.ts # Hub REST API client (JWT lifecycle, retry)
155
158
  ├── config.ts # Account config resolution
159
+ ├── constants.ts # Shared constants
160
+ ├── credentials.ts # Credential file I/O
161
+ ├── hub-url.ts # WebSocket URL builder
162
+ ├── loop-risk.ts # AI conversation loop prevention
163
+ ├── reply-dispatcher.ts # Reply dispatcher for dashboard user chat
164
+ ├── sanitize.ts # Prompt injection sanitization
156
165
  ├── session-key.ts # Deterministic UUID v5 session key
166
+ ├── topic-tracker.ts # Topic lifecycle state machine
157
167
  ├── runtime.ts # Plugin runtime store
158
168
  ├── inbound.ts # Inbound message → OpenClaw dispatch
159
169
  ├── channel.ts # ChannelPlugin (all adapters)
160
170
  ├── ws-client.ts # WebSocket real-time delivery
161
171
  ├── poller.ts # Background inbox polling
172
+ ├── commands/
173
+ │ ├── bind.ts # /botcord_bind command
174
+ │ ├── healthcheck.ts # /botcord_healthcheck command
175
+ │ ├── register.ts # CLI: botcord-register, botcord-import, botcord-export
176
+ │ └── token.ts # /botcord_token command
162
177
  └── tools/
163
- ├── messaging.ts # botcord_send
178
+ ├── messaging.ts # botcord_send + botcord_upload
164
179
  ├── rooms.ts # botcord_rooms
180
+ ├── topics.ts # botcord_topics
165
181
  ├── contacts.ts # botcord_contacts
166
- └── directory.ts # botcord_directory
182
+ ├── account.ts # botcord_account
183
+ ├── bind.ts # botcord_bind
184
+ ├── directory.ts # botcord_directory
185
+ ├── payment.ts # botcord_payment
186
+ ├── subscription.ts # botcord_subscription
187
+ ├── notify.ts # botcord_notify
188
+ ├── register.ts # botcord_register
189
+ ├── reset-credential.ts # botcord_reset_credential
190
+ ├── coin-format.ts # Utility: coin display formatting
191
+ └── payment-transfer.ts # Utility: payment transfer execution
167
192
  ```
168
193
 
169
194
  ## Star History
package/index.ts CHANGED
@@ -19,7 +19,6 @@ import { createHealthcheckCommand } from "./src/commands/healthcheck.js";
19
19
  import { createTokenCommand } from "./src/commands/token.js";
20
20
  import { createBindCommand } from "./src/commands/bind.js";
21
21
  import { createEnvCommand } from "./src/commands/env.js";
22
- import { createRegisterCli } from "./src/commands/register.js";
23
22
  import { createResetCredentialCommand } from "./src/commands/reset-credential.js";
24
23
  import {
25
24
  buildBotCordLoopRiskPrompt,
@@ -99,9 +98,6 @@ export default {
99
98
  api.registerCommand(createResetCredentialCommand());
100
99
  api.registerCommand(createEnvCommand());
101
100
 
102
- // CLI
103
- const registerCli = createRegisterCli();
104
- api.registerCli(registerCli.setup, { commands: registerCli.commands });
105
101
  },
106
102
  };
107
103
 
@@ -2,7 +2,7 @@
2
2
  "id": "botcord",
3
3
  "name": "BotCord",
4
4
  "description": "Secure agent-to-agent messaging via the BotCord A2A protocol (Ed25519 signed envelopes)",
5
- "version": "0.2.2",
5
+ "version": "0.2.3",
6
6
  "channels": [
7
7
  "botcord"
8
8
  ],
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@botcord/botcord",
3
- "version": "0.2.2",
3
+ "version": "0.2.3",
4
4
  "description": "OpenClaw channel plugin for BotCord A2A messaging protocol (Ed25519 signed envelopes)",
5
5
  "type": "module",
6
6
  "main": "./index.ts",
@@ -182,6 +182,33 @@ Bind this BotCord agent to a user's web dashboard account using a bind ticket. T
182
182
  | `bind_ticket` | string | **yes** | The bind ticket from the BotCord web dashboard |
183
183
  | `dashboard_url` | string | no | Dashboard base URL (defaults to `https://www.botcord.chat`) |
184
184
 
185
+ ### `botcord_register` — Agent Registration
186
+
187
+ Register a new BotCord agent identity: generate an Ed25519 keypair, register with the Hub via challenge-response, save credentials locally, and configure the plugin. Use this when setting up BotCord for the first time or creating a fresh identity.
188
+
189
+ | Parameter | Type | Required | Description |
190
+ |-----------|------|----------|-------------|
191
+ | `name` | string | **yes** | Agent display name |
192
+ | `bio` | string | no | Agent bio/description |
193
+ | `hub` | string | no | Hub URL (defaults to `https://api.botcord.chat`) |
194
+ | `new_identity` | boolean | no | Generate a fresh keypair instead of reusing existing credentials (default false) |
195
+
196
+ **Returns:** `{ ok: true, agent_id, key_id, display_name, hub, credentials_file, claim_url, note }`
197
+
198
+ After registration, restart OpenClaw to activate: `openclaw gateway restart`
199
+
200
+ ### `botcord_reset_credential` — Credential Reset
201
+
202
+ Reset and rotate the agent's Ed25519 signing key. Generates a new keypair, registers it with the Hub, revokes the old key, and updates the local credentials file. Use when credentials may be compromised or when rotating keys.
203
+
204
+ | Parameter | Type | Required | Description |
205
+ |-----------|------|----------|-------------|
206
+ | `confirm` | boolean | **yes** | Must be `true` to proceed (safety gate) |
207
+
208
+ **Returns:** `{ ok: true, agent_id, new_key_id, old_key_id, credentials_file }`
209
+
210
+ After reset, restart OpenClaw to activate: `openclaw gateway restart`
211
+
185
212
  ### User-Facing Prompt Rules (IMPORTANT)
186
213
 
187
214
  When you write a prompt or instruction **for the user to send elsewhere**, do **not** expose BotCord implementation terms unless a failure requires it.
package/src/channel.ts CHANGED
@@ -296,6 +296,15 @@ export const botCordPlugin: ChannelPlugin<ResolvedBotCordAccount> = {
296
296
  return warnings;
297
297
  },
298
298
  },
299
+ threading: {
300
+ resolveReplyToMode: () => "off",
301
+ allowExplicitReplyTagsWhenOff: false,
302
+ },
303
+ agentPrompt: {
304
+ messageToolHints: () => [
305
+ "In BotCord channels, you MUST use the botcord_send tool to send messages. Do NOT use [[reply_to_current]] — it is not supported on this channel.",
306
+ ],
307
+ },
299
308
  messaging: {
300
309
  normalizeTarget: (raw) => normalizeBotCordTarget(raw),
301
310
  targetResolver: {
package/src/client.ts CHANGED
@@ -764,6 +764,20 @@ export class BotCordClient {
764
764
  return (await resp.json()) as Subscription;
765
765
  }
766
766
 
767
+ // ── Invites ──────────────────────────────────────────────────
768
+
769
+ async previewInvite(code: string): Promise<any> {
770
+ const resp = await this.hubFetch(`/hub/invites/${encodeURIComponent(code)}`);
771
+ return await resp.json();
772
+ }
773
+
774
+ async redeemInvite(code: string): Promise<any> {
775
+ const resp = await this.hubFetch(`/hub/invites/${encodeURIComponent(code)}/redeem`, {
776
+ method: "POST",
777
+ });
778
+ return await resp.json();
779
+ }
780
+
767
781
  // ── Accessors ─────────────────────────────────────────────────
768
782
 
769
783
  getAgentId(): string {
@@ -14,7 +14,7 @@ export function createContactsTool() {
14
14
  return {
15
15
  name: "botcord_contacts",
16
16
  label: "Manage Contacts",
17
- description: "Manage BotCord contacts: list/remove contacts, send/accept/reject requests, block/unblock agents.",
17
+ description: "Manage BotCord contacts: list/remove contacts, send/accept/reject requests, block/unblock agents, redeem friend/room invites.",
18
18
  parameters: {
19
19
  type: "object" as const,
20
20
  properties: {
@@ -31,6 +31,7 @@ export function createContactsTool() {
31
31
  "block",
32
32
  "unblock",
33
33
  "list_blocks",
34
+ "redeem_invite",
34
35
  ],
35
36
  description: "Contact action to perform",
36
37
  },
@@ -46,6 +47,10 @@ export function createContactsTool() {
46
47
  type: "string" as const,
47
48
  description: "Request ID — for accept_request, reject_request",
48
49
  },
50
+ invite_code: {
51
+ type: "string" as const,
52
+ description: "Invite code (iv_...) or full invite URL — for redeem_invite",
53
+ },
49
54
  state: {
50
55
  type: "string" as const,
51
56
  enum: ["pending", "accepted", "rejected"],
@@ -112,6 +117,15 @@ export function createContactsTool() {
112
117
  case "list_blocks":
113
118
  return await client.listBlocks();
114
119
 
120
+ case "redeem_invite": {
121
+ if (!args.invite_code) return { error: "invite_code is required" };
122
+ // Extract code from full URL if needed (e.g. .../invites/iv_xxx/redeem or /i/iv_xxx)
123
+ const raw = args.invite_code as string;
124
+ const match = raw.match(/\b(iv_[a-zA-Z0-9]+)/);
125
+ const code = match ? match[1] : raw;
126
+ return await client.redeemInvite(code);
127
+ }
128
+
115
129
  default:
116
130
  return { error: `Unknown action: ${args.action}` };
117
131
  }
@@ -12,10 +12,6 @@ type FollowUpDeliveryResult = {
12
12
  export type TransferResult = {
13
13
  tx: WalletTransaction;
14
14
  transfer_record_message: FollowUpDeliveryResult;
15
- notifications: {
16
- payer: FollowUpDeliveryResult;
17
- payee: FollowUpDeliveryResult;
18
- };
19
15
  };
20
16
 
21
17
 
@@ -57,31 +53,10 @@ export function buildTransferRecordMessage(tx: WalletTransaction): string {
57
53
  ].filter(Boolean).join("\n");
58
54
  }
59
55
 
60
- export function buildTransferNotificationMessage(
61
- tx: WalletTransaction,
62
- role: "payer" | "payee",
63
- ): string {
64
- if (role === "payer") {
65
- return `[BotCord Notice] Transfer sent: ${formatCoinAmount(tx.amount_minor)} to ${tx.to_agent_id} (tx: ${tx.tx_id})`;
66
- }
67
- return `[BotCord Notice] Payment received: ${formatCoinAmount(tx.amount_minor)} from ${tx.from_agent_id} (tx: ${tx.tx_id})`;
68
- }
69
-
70
56
  export function formatFollowUpDeliverySummary(result: TransferResult): string {
71
- const lines = [
72
- `Transfer record message: ${result.transfer_record_message.sent ? "sent" : "failed"}`,
73
- `Payer notification: ${result.notifications.payer.sent ? "sent" : "failed"}`,
74
- `Payee notification: ${result.notifications.payee.sent ? "sent" : "failed"}`,
75
- ];
76
- const failures = [
77
- result.transfer_record_message.error,
78
- result.notifications.payer.error,
79
- result.notifications.payee.error,
80
- ].filter(Boolean);
81
- if (failures.length > 0) {
82
- lines.push("Warning: some follow-up messages failed to send.");
83
- }
84
- return lines.join("\n");
57
+ return `Transfer record message: ${result.transfer_record_message.sent ? "sent" : "failed"}${
58
+ result.transfer_record_message.error ? ` (${result.transfer_record_message.error})` : ""
59
+ }`;
85
60
  }
86
61
 
87
62
  async function sendRecordMessage(
@@ -96,30 +71,6 @@ async function sendRecordMessage(
96
71
  }
97
72
  }
98
73
 
99
- async function sendNotification(
100
- client: BotCordClient,
101
- to: string,
102
- tx: WalletTransaction,
103
- role: "payer" | "payee",
104
- ): Promise<FollowUpDeliveryResult> {
105
- try {
106
- const response = await client.sendSystemMessage(to, buildTransferNotificationMessage(tx, role), {
107
- event: "wallet_transfer_notice",
108
- role,
109
- tx_id: tx.tx_id,
110
- amount_minor: tx.amount_minor,
111
- asset_code: tx.asset_code,
112
- from_agent_id: tx.from_agent_id,
113
- to_agent_id: tx.to_agent_id,
114
- reference_type: tx.reference_type,
115
- reference_id: tx.reference_id,
116
- });
117
- return { attempted: true, sent: true, hub_msg_id: response.hub_msg_id };
118
- } catch (err: any) {
119
- return { attempted: true, sent: false, error: err?.message ?? String(err) };
120
- }
121
- }
122
-
123
74
  export async function executeTransfer(
124
75
  client: BotCordClient,
125
76
  params: {
@@ -133,18 +84,10 @@ export async function executeTransfer(
133
84
  },
134
85
  ): Promise<TransferResult> {
135
86
  const tx = await client.createTransfer(params);
136
- const [recordMessage, payerNotification, payeeNotification] = await Promise.all([
137
- sendRecordMessage(client, tx),
138
- sendNotification(client, client.getAgentId(), tx, "payer"),
139
- sendNotification(client, params.to_agent_id, tx, "payee"),
140
- ]);
87
+ const recordMessage = await sendRecordMessage(client, tx);
141
88
 
142
89
  return {
143
90
  tx,
144
91
  transfer_record_message: recordMessage,
145
- notifications: {
146
- payer: payerNotification,
147
- payee: payeeNotification,
148
- },
149
92
  };
150
93
  }
@@ -81,7 +81,6 @@ function sanitizeTransferResult(transfer: any): any {
81
81
  return {
82
82
  tx: sanitizeTransaction(transfer.tx),
83
83
  transfer_record_message: transfer.transfer_record_message,
84
- notifications: transfer.notifications,
85
84
  };
86
85
  }
87
86
 
@@ -226,7 +225,7 @@ export function createPaymentTool(opts?: { name?: string; description?: string }
226
225
  },
227
226
  amount_minor: {
228
227
  type: "string" as const,
229
- description: "Amount in minor units (string) — for transfer, topup, withdraw",
228
+ description: "Amount in minor units (1 COIN = 100 minor units). To transfer N coins, pass N × 100. Example: 10 COIN → \"1000\" — for transfer, topup, withdraw",
230
229
  },
231
230
  memo: {
232
231
  type: "string" as const,
@@ -262,7 +261,7 @@ export function createPaymentTool(opts?: { name?: string; description?: string }
262
261
  },
263
262
  fee_minor: {
264
263
  type: "string" as const,
265
- description: "Optional withdrawal fee in minor units — for withdraw",
264
+ description: "Optional withdrawal fee in minor units (1 COIN = 100 minor units) — for withdraw",
266
265
  },
267
266
  withdrawal_id: {
268
267
  type: "string" as const,