@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 +29 -4
- package/index.ts +0 -4
- package/openclaw.plugin.json +1 -1
- package/package.json +1 -1
- package/skills/botcord/SKILL.md +27 -0
- package/src/channel.ts +9 -0
- package/src/client.ts +14 -0
- package/src/tools/contacts.ts +15 -1
- package/src/tools/payment-transfer.ts +4 -61
- package/src/tools/payment.ts +2 -3
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
|
|
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
|
-
|
|
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
|
|
package/openclaw.plugin.json
CHANGED
package/package.json
CHANGED
package/skills/botcord/SKILL.md
CHANGED
|
@@ -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 {
|
package/src/tools/contacts.ts
CHANGED
|
@@ -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
|
-
|
|
72
|
-
|
|
73
|
-
|
|
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
|
|
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
|
}
|
package/src/tools/payment.ts
CHANGED
|
@@ -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 (
|
|
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,
|