@botcord/botcord 0.2.3-beta.20260326095543 → 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 +4 -4
- package/openclaw.plugin.json +1 -1
- package/package.json +1 -1
- package/skills/botcord/SKILL.md +32 -3
- package/src/channel.ts +21 -2
- package/src/client.ts +43 -5
- package/src/commands/healthcheck.ts +2 -0
- package/src/commands/register.ts +1 -1
- package/src/commands/reset-credential.ts +42 -0
- package/src/commands/token.ts +2 -0
- package/src/constants.ts +1 -1
- package/src/credentials.ts +57 -1
- package/src/inbound.ts +28 -19
- package/src/poller.ts +11 -1
- package/src/reset-credential.ts +136 -0
- package/src/tools/account.ts +2 -0
- package/src/tools/bind.ts +2 -0
- package/src/tools/contacts.ts +17 -1
- package/src/tools/directory.ts +2 -0
- package/src/tools/messaging.ts +3 -0
- package/src/tools/notify.ts +19 -8
- package/src/tools/payment-transfer.ts +4 -61
- package/src/tools/payment.ts +4 -3
- package/src/tools/reset-credential.ts +58 -0
- package/src/tools/rooms.ts +2 -0
- package/src/tools/subscription.ts +6 -4
- package/src/tools/topics.ts +2 -0
- package/src/types.ts +4 -1
- package/src/ws-client.ts +38 -9
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
|
@@ -14,11 +14,12 @@ import { createSubscriptionTool } from "./src/tools/subscription.js";
|
|
|
14
14
|
import { createNotifyTool } from "./src/tools/notify.js";
|
|
15
15
|
import { createBindTool } from "./src/tools/bind.js";
|
|
16
16
|
import { createRegisterTool } from "./src/tools/register.js";
|
|
17
|
+
import { createResetCredentialTool } from "./src/tools/reset-credential.js";
|
|
17
18
|
import { createHealthcheckCommand } from "./src/commands/healthcheck.js";
|
|
18
19
|
import { createTokenCommand } from "./src/commands/token.js";
|
|
19
20
|
import { createBindCommand } from "./src/commands/bind.js";
|
|
20
21
|
import { createEnvCommand } from "./src/commands/env.js";
|
|
21
|
-
import {
|
|
22
|
+
import { createResetCredentialCommand } from "./src/commands/reset-credential.js";
|
|
22
23
|
import {
|
|
23
24
|
buildBotCordLoopRiskPrompt,
|
|
24
25
|
clearBotCordLoopRiskSession,
|
|
@@ -55,6 +56,7 @@ export default {
|
|
|
55
56
|
api.registerTool(createNotifyTool() as any);
|
|
56
57
|
api.registerTool(createBindTool() as any);
|
|
57
58
|
api.registerTool(createRegisterTool() as any);
|
|
59
|
+
api.registerTool(createResetCredentialTool() as any);
|
|
58
60
|
|
|
59
61
|
// Hooks
|
|
60
62
|
api.on("after_tool_call", async (event: any, ctx: any) => {
|
|
@@ -93,11 +95,9 @@ export default {
|
|
|
93
95
|
api.registerCommand(createHealthcheckCommand());
|
|
94
96
|
api.registerCommand(createTokenCommand());
|
|
95
97
|
api.registerCommand(createBindCommand());
|
|
98
|
+
api.registerCommand(createResetCredentialCommand());
|
|
96
99
|
api.registerCommand(createEnvCommand());
|
|
97
100
|
|
|
98
|
-
// CLI
|
|
99
|
-
const registerCli = createRegisterCli();
|
|
100
|
-
api.registerCli(registerCli.setup, { commands: registerCli.commands });
|
|
101
101
|
},
|
|
102
102
|
};
|
|
103
103
|
|
package/openclaw.plugin.json
CHANGED
package/package.json
CHANGED
package/skills/botcord/SKILL.md
CHANGED
|
@@ -122,7 +122,7 @@ Create subscription products priced in BotCord coin, subscribe to products, list
|
|
|
122
122
|
| `list_my_products` | — | List products owned by the current agent |
|
|
123
123
|
| `list_products` | — | List visible subscription products |
|
|
124
124
|
| `archive_product` | `product_id` | Archive a product |
|
|
125
|
-
| `create_subscription_room` | `product_id`, `name`, `description?`, `rule?`, `max_members?`, `default_send?`, `default_invite?`, `slow_mode_seconds?` | Create a
|
|
125
|
+
| `create_subscription_room` | `product_id`, `name`, `description?`, `rule?`, `max_members?`, `default_send?`, `default_invite?`, `slow_mode_seconds?` | Create a public, open-to-join room bound to a subscription product |
|
|
126
126
|
| `bind_room_to_product` | `room_id`, `product_id`, `name?`, `description?`, `rule?`, `max_members?`, `default_send?`, `default_invite?`, `slow_mode_seconds?` | Bind an existing room to a subscription product |
|
|
127
127
|
| `subscribe` | `product_id` | Subscribe to a product |
|
|
128
128
|
| `list_my_subscriptions` | — | List current agent subscriptions |
|
|
@@ -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.
|
|
@@ -420,7 +447,7 @@ BotCord channel config lives in `openclaw.json` under `channels.botcord`:
|
|
|
420
447
|
"enabled": true,
|
|
421
448
|
"credentialsFile": "~/.botcord/credentials/ag_xxxxxxxxxxxx.json",
|
|
422
449
|
"deliveryMode": "websocket", // "websocket" (recommended) or "polling"
|
|
423
|
-
"notifySession": "
|
|
450
|
+
"notifySession": "botcord:owner:main"
|
|
424
451
|
}
|
|
425
452
|
}
|
|
426
453
|
}
|
|
@@ -428,7 +455,9 @@ BotCord channel config lives in `openclaw.json` under `channels.botcord`:
|
|
|
428
455
|
|
|
429
456
|
### `notifySession`
|
|
430
457
|
|
|
431
|
-
When BotCord receives notification-type messages (contact requests, contact responses, contact removals), the plugin sends a push notification directly to the channel specified by this session key — **without triggering an agent turn**. This lets the owner see incoming events in real time on their preferred messaging app.
|
|
458
|
+
When BotCord receives notification-type messages (contact requests, contact responses, contact removals), the plugin sends a push notification directly to the channel(s) specified by this session key — **without triggering an agent turn**. This lets the owner see incoming events in real time on their preferred messaging app.
|
|
459
|
+
|
|
460
|
+
`notifySession` accepts a single string or an array of strings to notify multiple sessions simultaneously.
|
|
432
461
|
|
|
433
462
|
**Format:** `agent:<agentName>:<channel>:<chatType>:<peerId>`
|
|
434
463
|
|
package/src/channel.ts
CHANGED
|
@@ -51,6 +51,7 @@ import type {
|
|
|
51
51
|
// Heavy deps (client, poller, ws-client) are lazy-loaded so that setup-entry.ts
|
|
52
52
|
// can import botCordPlugin without pulling in ws at module level.
|
|
53
53
|
import { getBotCordRuntime } from "./runtime.js";
|
|
54
|
+
import { attachTokenPersistence } from "./credentials.js";
|
|
54
55
|
const lazyClient = () => import("./client.js").then((m) => m.BotCordClient);
|
|
55
56
|
const lazyPoller = () => import("./poller.js");
|
|
56
57
|
const lazyWsClient = () => import("./ws-client.js");
|
|
@@ -148,8 +149,11 @@ const botCordConfigSchema = {
|
|
|
148
149
|
items: { type: "string" as const },
|
|
149
150
|
},
|
|
150
151
|
notifySession: {
|
|
151
|
-
|
|
152
|
-
|
|
152
|
+
oneOf: [
|
|
153
|
+
{ type: "string" as const },
|
|
154
|
+
{ type: "array" as const, items: { type: "string" as const } },
|
|
155
|
+
],
|
|
156
|
+
description: "Session key(s) to notify when inbound messages arrive (e.g. botcord:owner:main)",
|
|
153
157
|
},
|
|
154
158
|
accounts: {
|
|
155
159
|
type: "object" as const,
|
|
@@ -292,6 +296,15 @@ export const botCordPlugin: ChannelPlugin<ResolvedBotCordAccount> = {
|
|
|
292
296
|
return warnings;
|
|
293
297
|
},
|
|
294
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
|
+
},
|
|
295
308
|
messaging: {
|
|
296
309
|
normalizeTarget: (raw) => normalizeBotCordTarget(raw),
|
|
297
310
|
targetResolver: {
|
|
@@ -325,6 +338,7 @@ export const botCordPlugin: ChannelPlugin<ResolvedBotCordAccount> = {
|
|
|
325
338
|
try {
|
|
326
339
|
const Client = await lazyClient();
|
|
327
340
|
const client = new Client(account.config);
|
|
341
|
+
attachTokenPersistence(client, account.config);
|
|
328
342
|
const info = await client.resolve(account.agentId);
|
|
329
343
|
return { kind: "user", id: info.agent_id, name: info.display_name || info.agent_id };
|
|
330
344
|
} catch {
|
|
@@ -337,6 +351,7 @@ export const botCordPlugin: ChannelPlugin<ResolvedBotCordAccount> = {
|
|
|
337
351
|
try {
|
|
338
352
|
const Client = await lazyClient();
|
|
339
353
|
const client = new Client(account.config);
|
|
354
|
+
attachTokenPersistence(client, account.config);
|
|
340
355
|
const contacts = await client.listContacts();
|
|
341
356
|
const q = query?.trim().toLowerCase() ?? "";
|
|
342
357
|
return contacts
|
|
@@ -362,6 +377,7 @@ export const botCordPlugin: ChannelPlugin<ResolvedBotCordAccount> = {
|
|
|
362
377
|
try {
|
|
363
378
|
const Client = await lazyClient();
|
|
364
379
|
const client = new Client(account.config);
|
|
380
|
+
attachTokenPersistence(client, account.config);
|
|
365
381
|
const rooms = await client.listMyRooms();
|
|
366
382
|
const q = query?.trim().toLowerCase() ?? "";
|
|
367
383
|
return rooms
|
|
@@ -382,6 +398,7 @@ export const botCordPlugin: ChannelPlugin<ResolvedBotCordAccount> = {
|
|
|
382
398
|
const account = resolveBotCordAccount({ cfg: cfg as CoreConfig, accountId: accountId ?? undefined });
|
|
383
399
|
const Client = await lazyClient();
|
|
384
400
|
const client = new Client(account.config);
|
|
401
|
+
attachTokenPersistence(client, account.config);
|
|
385
402
|
const result = await client.sendMessage(to, text);
|
|
386
403
|
return {
|
|
387
404
|
channel: "botcord",
|
|
@@ -393,6 +410,7 @@ export const botCordPlugin: ChannelPlugin<ResolvedBotCordAccount> = {
|
|
|
393
410
|
const account = resolveBotCordAccount({ cfg: cfg as CoreConfig, accountId: accountId ?? undefined });
|
|
394
411
|
const Client = await lazyClient();
|
|
395
412
|
const client = new Client(account.config);
|
|
413
|
+
attachTokenPersistence(client, account.config);
|
|
396
414
|
const attachments: MessageAttachment[] = [];
|
|
397
415
|
if (mediaUrl) {
|
|
398
416
|
const filename = mediaUrl.split("/").pop() || "attachment";
|
|
@@ -441,6 +459,7 @@ export const botCordPlugin: ChannelPlugin<ResolvedBotCordAccount> = {
|
|
|
441
459
|
|
|
442
460
|
const Client = await lazyClient();
|
|
443
461
|
const client = new Client(account.config);
|
|
462
|
+
attachTokenPersistence(client, account.config);
|
|
444
463
|
const mode = account.deliveryMode || "websocket";
|
|
445
464
|
|
|
446
465
|
if (mode === "websocket") {
|
package/src/client.ts
CHANGED
|
@@ -36,6 +36,13 @@ export class BotCordClient {
|
|
|
36
36
|
private jwtToken: string | null = null;
|
|
37
37
|
private tokenExpiresAt = 0;
|
|
38
38
|
|
|
39
|
+
/**
|
|
40
|
+
* Called synchronously after a token refresh so credentials can be persisted.
|
|
41
|
+
* Must be a synchronous function — async persistence should be dispatched
|
|
42
|
+
* internally (e.g. fire-and-forget) by the callback itself.
|
|
43
|
+
*/
|
|
44
|
+
onTokenRefresh?: (token: string, expiresAt: number) => void;
|
|
45
|
+
|
|
39
46
|
constructor(config: BotCordAccountConfig) {
|
|
40
47
|
if (!config.hubUrl || !config.agentId || !config.keyId || !config.privateKey) {
|
|
41
48
|
throw new Error("BotCord client requires hubUrl, agentId, keyId, and privateKey");
|
|
@@ -44,12 +51,16 @@ export class BotCordClient {
|
|
|
44
51
|
this.agentId = config.agentId;
|
|
45
52
|
this.keyId = config.keyId;
|
|
46
53
|
this.privateKey = config.privateKey;
|
|
54
|
+
if (config.token) {
|
|
55
|
+
this.jwtToken = config.token;
|
|
56
|
+
this.tokenExpiresAt = config.tokenExpiresAt ?? 0;
|
|
57
|
+
}
|
|
47
58
|
}
|
|
48
59
|
|
|
49
60
|
// ── Token management ──────────────────────────────────────────
|
|
50
61
|
|
|
51
|
-
async ensureToken(): Promise<string> {
|
|
52
|
-
if (this.jwtToken && Date.now() / 1000 < this.tokenExpiresAt - 60) {
|
|
62
|
+
async ensureToken(forceRefresh = false): Promise<string> {
|
|
63
|
+
if (!forceRefresh && this.jwtToken && Date.now() / 1000 < this.tokenExpiresAt - 60) {
|
|
53
64
|
return this.jwtToken;
|
|
54
65
|
}
|
|
55
66
|
return this.refreshToken();
|
|
@@ -81,13 +92,18 @@ export class BotCordClient {
|
|
|
81
92
|
this.jwtToken = data.agent_token || data.token!;
|
|
82
93
|
// Default 24h expiry if not provided
|
|
83
94
|
this.tokenExpiresAt = data.expires_at ?? Date.now() / 1000 + 86400;
|
|
95
|
+
try {
|
|
96
|
+
this.onTokenRefresh?.(this.jwtToken, this.tokenExpiresAt);
|
|
97
|
+
} catch {
|
|
98
|
+
// Token persistence is best-effort — never block the request path
|
|
99
|
+
}
|
|
84
100
|
return this.jwtToken;
|
|
85
101
|
}
|
|
86
102
|
|
|
87
103
|
// ── Authenticated fetch with rate-limit retry ─────────────────
|
|
88
104
|
|
|
89
105
|
private async hubFetch(path: string, init: RequestInit = {}): Promise<Response> {
|
|
90
|
-
|
|
106
|
+
let token = await this.ensureToken();
|
|
91
107
|
|
|
92
108
|
for (let attempt = 0; attempt <= MAX_RETRIES; attempt++) {
|
|
93
109
|
const headers: Record<string, string> = {
|
|
@@ -107,9 +123,9 @@ export class BotCordClient {
|
|
|
107
123
|
|
|
108
124
|
if (resp.ok) return resp;
|
|
109
125
|
|
|
110
|
-
// Token expired — refresh and retry
|
|
126
|
+
// Token expired — refresh and retry with the new token
|
|
111
127
|
if (resp.status === 401 && attempt === 0) {
|
|
112
|
-
await this.refreshToken();
|
|
128
|
+
token = await this.refreshToken();
|
|
113
129
|
continue;
|
|
114
130
|
}
|
|
115
131
|
|
|
@@ -286,6 +302,14 @@ export class BotCordClient {
|
|
|
286
302
|
return (await resp.json()) as InboxPollResponse;
|
|
287
303
|
}
|
|
288
304
|
|
|
305
|
+
async ackMessages(messageIds: string[]): Promise<void> {
|
|
306
|
+
if (messageIds.length === 0) return;
|
|
307
|
+
await this.hubFetch("/hub/inbox/ack", {
|
|
308
|
+
method: "POST",
|
|
309
|
+
body: JSON.stringify({ message_ids: messageIds }),
|
|
310
|
+
});
|
|
311
|
+
}
|
|
312
|
+
|
|
289
313
|
async getHistory(options?: {
|
|
290
314
|
peer?: string;
|
|
291
315
|
roomId?: string;
|
|
@@ -740,6 +764,20 @@ export class BotCordClient {
|
|
|
740
764
|
return (await resp.json()) as Subscription;
|
|
741
765
|
}
|
|
742
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
|
+
|
|
743
781
|
// ── Accessors ─────────────────────────────────────────────────
|
|
744
782
|
|
|
745
783
|
getAgentId(): string {
|
|
@@ -9,6 +9,7 @@ import {
|
|
|
9
9
|
isAccountConfigured,
|
|
10
10
|
} from "../config.js";
|
|
11
11
|
import { BotCordClient } from "../client.js";
|
|
12
|
+
import { attachTokenPersistence } from "../credentials.js";
|
|
12
13
|
import { normalizeAndValidateHubUrl } from "../hub-url.js";
|
|
13
14
|
import { getConfig as getAppConfig } from "../runtime.js";
|
|
14
15
|
|
|
@@ -97,6 +98,7 @@ export function createHealthcheckCommand() {
|
|
|
97
98
|
let client: BotCordClient;
|
|
98
99
|
try {
|
|
99
100
|
client = new BotCordClient(acct);
|
|
101
|
+
attachTokenPersistence(client, acct);
|
|
100
102
|
} catch (err: any) {
|
|
101
103
|
error(err.message);
|
|
102
104
|
lines.push("", `── Summary ──`);
|
package/src/commands/register.ts
CHANGED
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* /botcord_reset_credential — regenerate local credentials for an existing agent.
|
|
3
|
+
*/
|
|
4
|
+
import { getConfig as getAppConfig } from "../runtime.js";
|
|
5
|
+
import { resetCredential } from "../reset-credential.js";
|
|
6
|
+
|
|
7
|
+
export function createResetCredentialCommand() {
|
|
8
|
+
return {
|
|
9
|
+
name: "botcord_reset_credential",
|
|
10
|
+
description:
|
|
11
|
+
"Reset BotCord credentials for an existing agent using a one-time reset code or reset ticket.",
|
|
12
|
+
acceptsArgs: true,
|
|
13
|
+
requireAuth: true,
|
|
14
|
+
handler: async (ctx: any) => {
|
|
15
|
+
const rawArgs = String(ctx.args || "").trim();
|
|
16
|
+
const parts = rawArgs.split(/\s+/).filter(Boolean);
|
|
17
|
+
if (parts.length < 2) {
|
|
18
|
+
return { text: "[FAIL] Usage: /botcord_reset_credential <agent_id> <reset_code_or_ticket> [hub_url]" };
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
const [agentId, resetCodeOrTicket, hubUrl] = parts;
|
|
22
|
+
const cfg = getAppConfig();
|
|
23
|
+
if (!cfg) return { text: "[FAIL] No OpenClaw configuration available" };
|
|
24
|
+
|
|
25
|
+
try {
|
|
26
|
+
const result = await resetCredential({
|
|
27
|
+
config: cfg,
|
|
28
|
+
agentId,
|
|
29
|
+
resetCodeOrTicket,
|
|
30
|
+
hubUrl,
|
|
31
|
+
});
|
|
32
|
+
return {
|
|
33
|
+
text:
|
|
34
|
+
`[OK] Reset credentials for ${result.displayName} (${result.agentId}). ` +
|
|
35
|
+
`Saved to ${result.credentialsFile}. Restart OpenClaw to activate.`,
|
|
36
|
+
};
|
|
37
|
+
} catch (err: any) {
|
|
38
|
+
return { text: `[FAIL] ${err.message}` };
|
|
39
|
+
}
|
|
40
|
+
},
|
|
41
|
+
};
|
|
42
|
+
}
|
package/src/commands/token.ts
CHANGED
|
@@ -7,6 +7,7 @@ import {
|
|
|
7
7
|
isAccountConfigured,
|
|
8
8
|
} from "../config.js";
|
|
9
9
|
import { BotCordClient } from "../client.js";
|
|
10
|
+
import { attachTokenPersistence } from "../credentials.js";
|
|
10
11
|
import { getConfig as getAppConfig } from "../runtime.js";
|
|
11
12
|
|
|
12
13
|
export function createTokenCommand() {
|
|
@@ -32,6 +33,7 @@ export function createTokenCommand() {
|
|
|
32
33
|
|
|
33
34
|
try {
|
|
34
35
|
const client = new BotCordClient(acct);
|
|
36
|
+
attachTokenPersistence(client, acct);
|
|
35
37
|
const token = await client.ensureToken();
|
|
36
38
|
return { text: token };
|
|
37
39
|
} catch (err: any) {
|
package/src/constants.ts
CHANGED
|
@@ -9,7 +9,7 @@
|
|
|
9
9
|
|
|
10
10
|
export type ReleaseChannel = "stable" | "beta";
|
|
11
11
|
|
|
12
|
-
export const RELEASE_CHANNEL: ReleaseChannel = "
|
|
12
|
+
export const RELEASE_CHANNEL: ReleaseChannel = "stable";
|
|
13
13
|
|
|
14
14
|
const HUB_URLS: Record<ReleaseChannel, string> = {
|
|
15
15
|
stable: "https://api.botcord.chat",
|
package/src/credentials.ts
CHANGED
|
@@ -1,9 +1,10 @@
|
|
|
1
|
-
import { chmodSync, mkdirSync, readFileSync, writeFileSync } from "node:fs";
|
|
1
|
+
import { chmodSync, mkdirSync, readFileSync, writeFileSync, existsSync } from "node:fs";
|
|
2
2
|
import os from "node:os";
|
|
3
3
|
import path from "node:path";
|
|
4
4
|
import { derivePublicKey } from "./crypto.js";
|
|
5
5
|
import { normalizeAndValidateHubUrl } from "./hub-url.js";
|
|
6
6
|
import type { BotCordAccountConfig } from "./types.js";
|
|
7
|
+
import type { BotCordClient as BotCordClientType } from "./client.js";
|
|
7
8
|
|
|
8
9
|
export interface StoredBotCordCredentials {
|
|
9
10
|
version: 1;
|
|
@@ -14,6 +15,8 @@ export interface StoredBotCordCredentials {
|
|
|
14
15
|
publicKey: string;
|
|
15
16
|
displayName?: string;
|
|
16
17
|
savedAt: string;
|
|
18
|
+
token?: string;
|
|
19
|
+
tokenExpiresAt?: number;
|
|
17
20
|
}
|
|
18
21
|
|
|
19
22
|
function normalizeCredentialValue(raw: any, keys: string[]): string | undefined {
|
|
@@ -57,6 +60,12 @@ export function loadStoredCredentials(credentialsFile: string): StoredBotCordCre
|
|
|
57
60
|
const publicKey = normalizeCredentialValue(raw, ["publicKey", "public_key"]);
|
|
58
61
|
const displayName = normalizeCredentialValue(raw, ["displayName", "display_name"]);
|
|
59
62
|
const savedAt = normalizeCredentialValue(raw, ["savedAt", "saved_at"]);
|
|
63
|
+
const token = normalizeCredentialValue(raw, ["token"]);
|
|
64
|
+
const tokenExpiresAt = typeof raw.tokenExpiresAt === "number"
|
|
65
|
+
? raw.tokenExpiresAt
|
|
66
|
+
: typeof raw.token_expires_at === "number"
|
|
67
|
+
? raw.token_expires_at
|
|
68
|
+
: undefined;
|
|
60
69
|
|
|
61
70
|
if (!hubUrl) throw new Error(`BotCord credentials file "${resolved}" is missing hubUrl`);
|
|
62
71
|
if (!agentId) throw new Error(`BotCord credentials file "${resolved}" is missing agentId`);
|
|
@@ -86,6 +95,8 @@ export function loadStoredCredentials(credentialsFile: string): StoredBotCordCre
|
|
|
86
95
|
publicKey: publicKey || derivedPublicKey,
|
|
87
96
|
displayName,
|
|
88
97
|
savedAt: savedAt || new Date().toISOString(),
|
|
98
|
+
token,
|
|
99
|
+
tokenExpiresAt,
|
|
89
100
|
};
|
|
90
101
|
}
|
|
91
102
|
|
|
@@ -100,6 +111,8 @@ export function readCredentialFileData(credentialsFile?: string): Partial<BotCor
|
|
|
100
111
|
keyId: raw.keyId,
|
|
101
112
|
privateKey: raw.privateKey,
|
|
102
113
|
publicKey: raw.publicKey,
|
|
114
|
+
token: raw.token,
|
|
115
|
+
tokenExpiresAt: raw.tokenExpiresAt,
|
|
103
116
|
};
|
|
104
117
|
} catch {
|
|
105
118
|
return {};
|
|
@@ -123,3 +136,46 @@ export function writeCredentialsFile(
|
|
|
123
136
|
chmodSync(resolved, 0o600);
|
|
124
137
|
return resolved;
|
|
125
138
|
}
|
|
139
|
+
|
|
140
|
+
/**
|
|
141
|
+
* Atomically update only the token fields in an existing credentials file.
|
|
142
|
+
* Reads current file, merges new token/expiresAt, writes back.
|
|
143
|
+
* Returns false if the file does not exist or the write fails.
|
|
144
|
+
*/
|
|
145
|
+
export function updateCredentialsToken(
|
|
146
|
+
credentialsFile: string,
|
|
147
|
+
token: string,
|
|
148
|
+
tokenExpiresAt: number,
|
|
149
|
+
): boolean {
|
|
150
|
+
const resolved = resolveCredentialsFilePath(credentialsFile);
|
|
151
|
+
try {
|
|
152
|
+
if (!existsSync(resolved)) return false;
|
|
153
|
+
const raw = JSON.parse(readFileSync(resolved, "utf8")) as Record<string, unknown>;
|
|
154
|
+
raw.token = token;
|
|
155
|
+
raw.tokenExpiresAt = tokenExpiresAt;
|
|
156
|
+
writeFileSync(resolved, JSON.stringify(raw, null, 2) + "\n", {
|
|
157
|
+
encoding: "utf8",
|
|
158
|
+
mode: 0o600,
|
|
159
|
+
});
|
|
160
|
+
chmodSync(resolved, 0o600);
|
|
161
|
+
return true;
|
|
162
|
+
} catch {
|
|
163
|
+
return false;
|
|
164
|
+
}
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
/**
|
|
168
|
+
* Attach token persistence to a BotCordClient.
|
|
169
|
+
* If the account was loaded from a credentialsFile, refreshed tokens
|
|
170
|
+
* are automatically written back to that file.
|
|
171
|
+
*/
|
|
172
|
+
export function attachTokenPersistence(
|
|
173
|
+
client: BotCordClientType,
|
|
174
|
+
acct: BotCordAccountConfig,
|
|
175
|
+
): void {
|
|
176
|
+
if (!acct.credentialsFile) return;
|
|
177
|
+
const credFile = acct.credentialsFile;
|
|
178
|
+
client.onTokenRefresh = (token, expiresAt) => {
|
|
179
|
+
updateCredentialsToken(credFile, token, expiresAt);
|
|
180
|
+
};
|
|
181
|
+
}
|
package/src/inbound.ts
CHANGED
|
@@ -4,6 +4,7 @@
|
|
|
4
4
|
*/
|
|
5
5
|
import { getBotCordRuntime } from "./runtime.js";
|
|
6
6
|
import { resolveAccountConfig } from "./config.js";
|
|
7
|
+
import { attachTokenPersistence } from "./credentials.js";
|
|
7
8
|
import { buildSessionKey } from "./session-key.js";
|
|
8
9
|
import { readFileSync } from "node:fs";
|
|
9
10
|
|
|
@@ -24,6 +25,12 @@ import { BotCordClient } from "./client.js";
|
|
|
24
25
|
import { createBotCordReplyDispatcher } from "./reply-dispatcher.js";
|
|
25
26
|
import type { InboxMessage, MessageType } from "./types.js";
|
|
26
27
|
|
|
28
|
+
/** Normalize notifySession (string | string[] | undefined) to a flat array. */
|
|
29
|
+
export function normalizeNotifySessions(ns?: string | string[]): string[] {
|
|
30
|
+
if (!ns) return [];
|
|
31
|
+
return Array.isArray(ns) ? ns : [ns];
|
|
32
|
+
}
|
|
33
|
+
|
|
27
34
|
// Envelope types that count as notifications rather than normal messages
|
|
28
35
|
const NOTIFICATION_TYPES: ReadonlySet<string> = new Set([
|
|
29
36
|
"contact_request",
|
|
@@ -128,7 +135,9 @@ async function handleDashboardUserChat(
|
|
|
128
135
|
const content = rawContent;
|
|
129
136
|
|
|
130
137
|
const replyTarget = msg.room_id || "";
|
|
131
|
-
|
|
138
|
+
// All dashboard user-chat sessions share a single fixed key so the
|
|
139
|
+
// conversation context persists across rooms.
|
|
140
|
+
const sessionKey = "botcord:owner:main";
|
|
132
141
|
|
|
133
142
|
const route = core.channel.routing.resolveAgentRoute({
|
|
134
143
|
cfg,
|
|
@@ -143,7 +152,7 @@ async function handleDashboardUserChat(
|
|
|
143
152
|
const envelopeOptions = core.channel.reply.resolveEnvelopeFormatOptions(cfg);
|
|
144
153
|
const formattedBody = core.channel.reply.formatAgentEnvelope({
|
|
145
154
|
channel: "BotCord",
|
|
146
|
-
from: "Owner",
|
|
155
|
+
from: msg.source_user_name || "Owner",
|
|
147
156
|
timestamp: new Date(),
|
|
148
157
|
envelope: envelopeOptions,
|
|
149
158
|
body: content,
|
|
@@ -159,7 +168,7 @@ async function handleDashboardUserChat(
|
|
|
159
168
|
SessionKey: route.sessionKey || sessionKey,
|
|
160
169
|
AccountId: accountId,
|
|
161
170
|
ChatType: "direct",
|
|
162
|
-
SenderName: "Owner",
|
|
171
|
+
SenderName: msg.source_user_name || "Owner",
|
|
163
172
|
SenderId: senderId,
|
|
164
173
|
Provider: "botcord" as const,
|
|
165
174
|
Surface: "botcord" as const,
|
|
@@ -169,12 +178,13 @@ async function handleDashboardUserChat(
|
|
|
169
178
|
CommandAuthorized: true,
|
|
170
179
|
OriginatingChannel: "botcord" as const,
|
|
171
180
|
OriginatingTo: to,
|
|
172
|
-
ConversationLabel: "Owner Chat",
|
|
181
|
+
ConversationLabel: msg.source_user_name ? `${msg.source_user_name} Chat` : "Owner Chat",
|
|
173
182
|
});
|
|
174
183
|
|
|
175
184
|
// Create the reply dispatcher that sends replies back to the chat room
|
|
176
185
|
const acct = resolveAccountConfig(cfg, accountId);
|
|
177
186
|
const client = new BotCordClient(acct);
|
|
187
|
+
attachTokenPersistence(client, acct);
|
|
178
188
|
const replyDispatcher = createBotCordReplyDispatcher({
|
|
179
189
|
client,
|
|
180
190
|
replyTarget,
|
|
@@ -352,21 +362,20 @@ export async function dispatchInbound(params: InboundParams): Promise<void> {
|
|
|
352
362
|
const messageType = params.messageType;
|
|
353
363
|
if (messageType && NOTIFICATION_TYPES.has(messageType)) {
|
|
354
364
|
const acct = resolveAccountConfig(cfg, accountId);
|
|
355
|
-
const
|
|
356
|
-
|
|
357
|
-
|
|
358
|
-
if (
|
|
359
|
-
|
|
360
|
-
|
|
361
|
-
|
|
362
|
-
|
|
363
|
-
|
|
364
|
-
|
|
365
|
-
|
|
366
|
-
|
|
367
|
-
|
|
368
|
-
|
|
369
|
-
}
|
|
365
|
+
const sessions = normalizeNotifySessions(acct.notifySession);
|
|
366
|
+
const childSessionKey = route.sessionKey || sessionKey;
|
|
367
|
+
for (const ns of sessions) {
|
|
368
|
+
if (ns === childSessionKey) continue;
|
|
369
|
+
const topicLabel = topic ? ` (topic: ${topic})` : "";
|
|
370
|
+
const notification =
|
|
371
|
+
`[BotCord ${messageType}] from ${senderName}${topicLabel}\n` +
|
|
372
|
+
`Session: ${childSessionKey}\n` +
|
|
373
|
+
`Preview: ${(params.content || "").slice(0, 200)}`;
|
|
374
|
+
|
|
375
|
+
try {
|
|
376
|
+
await deliverNotification(core, cfg, ns, notification);
|
|
377
|
+
} catch (err: any) {
|
|
378
|
+
console.error(`[botcord] auto-notify failed:`, err?.message ?? err);
|
|
370
379
|
}
|
|
371
380
|
}
|
|
372
381
|
}
|