@botcord/botcord 0.1.1

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.
@@ -0,0 +1,30 @@
1
+ /**
2
+ * /botcord_bind — Bind this agent to a BotCord web dashboard account using a bind ticket.
3
+ */
4
+ import { executeBind } from "../tools/bind.js";
5
+
6
+ export function createBindCommand() {
7
+ return {
8
+ name: "botcord_bind",
9
+ description:
10
+ "Bind this agent to a BotCord web dashboard account using a bind ticket.",
11
+ acceptsArgs: true,
12
+ requireAuth: true,
13
+ handler: async (args?: string) => {
14
+ const bindTicket = (args || "").trim();
15
+ if (!bindTicket) {
16
+ return { text: "[FAIL] Usage: /botcord_bind <bind_ticket>" };
17
+ }
18
+
19
+ const result = await executeBind(bindTicket);
20
+
21
+ if ("error" in result) {
22
+ return { text: `[FAIL] ${result.error}` };
23
+ }
24
+
25
+ const agentId = result.agent_id || "unknown";
26
+ const displayName = result.display_name || agentId;
27
+ return { text: `[OK] Agent ${displayName} (${agentId}) successfully bound to dashboard.` };
28
+ },
29
+ };
30
+ }
@@ -0,0 +1,160 @@
1
+ /**
2
+ * /botcord_healthcheck — Plugin command for BotCord integration health check.
3
+ *
4
+ * Checks: plugin config, Hub connectivity, token validity, delivery mode status.
5
+ */
6
+ import {
7
+ getSingleAccountModeError,
8
+ resolveAccountConfig,
9
+ isAccountConfigured,
10
+ } from "../config.js";
11
+ import { BotCordClient } from "../client.js";
12
+ import { normalizeAndValidateHubUrl } from "../hub-url.js";
13
+ import { getConfig as getAppConfig } from "../runtime.js";
14
+
15
+ export function createHealthcheckCommand() {
16
+ return {
17
+ name: "botcord_healthcheck",
18
+ description: "Check BotCord integration health: config, Hub connectivity, token, delivery mode.",
19
+ acceptsArgs: false,
20
+ requireAuth: true,
21
+ handler: async () => {
22
+ const lines: string[] = [];
23
+ let pass = 0;
24
+ let warn = 0;
25
+ let fail = 0;
26
+
27
+ const ok = (msg: string) => { lines.push(`[OK] ${msg}`); pass++; };
28
+ const warning = (msg: string) => { lines.push(`[WARN] ${msg}`); warn++; };
29
+ const error = (msg: string) => { lines.push(`[FAIL] ${msg}`); fail++; };
30
+ const info = (msg: string) => { lines.push(`[INFO] ${msg}`); };
31
+
32
+ // ── 1. Plugin Configuration ──
33
+ lines.push("", "── Plugin Configuration ──");
34
+
35
+ const cfg = getAppConfig();
36
+ if (!cfg) {
37
+ error("No OpenClaw configuration available");
38
+ return { text: lines.join("\n") };
39
+ }
40
+ const singleAccountError = getSingleAccountModeError(cfg);
41
+ if (singleAccountError) {
42
+ error(singleAccountError);
43
+ return { text: lines.join("\n") };
44
+ }
45
+
46
+ const acct = resolveAccountConfig(cfg);
47
+
48
+ if (!acct.hubUrl) {
49
+ error("hubUrl is not configured");
50
+ } else {
51
+ try {
52
+ const normalizedHubUrl = normalizeAndValidateHubUrl(acct.hubUrl);
53
+ ok(`Hub URL: ${normalizedHubUrl}`);
54
+ if (normalizedHubUrl.startsWith("http://")) {
55
+ warning("Hub URL uses loopback HTTP; this is acceptable only for local development");
56
+ }
57
+ } catch (err: any) {
58
+ error(err.message);
59
+ }
60
+ }
61
+
62
+ if (acct.credentialsFile) {
63
+ info(`Credentials file: ${acct.credentialsFile}`);
64
+ if (!acct.privateKey) {
65
+ error("credentialsFile is configured but could not be loaded");
66
+ }
67
+ }
68
+
69
+ if (!acct.agentId) {
70
+ error("agentId is not configured");
71
+ } else {
72
+ ok(`Agent ID: ${acct.agentId}`);
73
+ }
74
+
75
+ if (!acct.keyId) {
76
+ error("keyId is not configured");
77
+ } else {
78
+ ok(`Key ID: ${acct.keyId}`);
79
+ }
80
+
81
+ if (!acct.privateKey) {
82
+ error("privateKey is not configured");
83
+ } else {
84
+ ok("Private key: configured");
85
+ }
86
+
87
+ if (!isAccountConfigured(acct)) {
88
+ error("Plugin is not fully configured — cannot proceed with connectivity checks");
89
+ lines.push("", `── Summary ──`);
90
+ lines.push(`Passed: ${pass} | Warnings: ${warn} | Failed: ${fail}`);
91
+ return { text: lines.join("\n") };
92
+ }
93
+
94
+ // ── 2. Hub Connectivity & Token ──
95
+ lines.push("", "── Hub Connectivity ──");
96
+
97
+ let client: BotCordClient;
98
+ try {
99
+ client = new BotCordClient(acct);
100
+ } catch (err: any) {
101
+ error(err.message);
102
+ lines.push("", `── Summary ──`);
103
+ lines.push(`Passed: ${pass} | Warnings: ${warn} | Failed: ${fail}`);
104
+ return { text: lines.join("\n") };
105
+ }
106
+
107
+ try {
108
+ await client.ensureToken();
109
+ ok("Token refresh successful — Hub is reachable and credentials are valid");
110
+ } catch (err: any) {
111
+ error(`Token refresh failed: ${err.message}`);
112
+ lines.push("", `── Summary ──`);
113
+ lines.push(`Passed: ${pass} | Warnings: ${warn} | Failed: ${fail}`);
114
+ return { text: lines.join("\n") };
115
+ }
116
+
117
+ // ── 3. Agent Resolution ──
118
+ lines.push("", "── Agent Identity ──");
119
+
120
+ try {
121
+ const resolved = await client.resolve(client.getAgentId());
122
+ if (resolved && typeof resolved === "object") {
123
+ const r = resolved as Record<string, unknown>;
124
+ ok(`Agent resolved: ${r.display_name || r.agent_id}`);
125
+ if (r.bio) info(`Bio: ${r.bio}`);
126
+ if (Array.isArray(r.endpoints) && r.endpoints.length > 0) {
127
+ info(`Registered endpoints on Hub: ${r.endpoints.length}`);
128
+ }
129
+ }
130
+ } catch (err: any) {
131
+ error(`Agent resolution failed: ${err.message}`);
132
+ }
133
+
134
+ // ── 4. Delivery Mode ──
135
+ lines.push("", "── Delivery Mode ──");
136
+
137
+ const mode = acct.deliveryMode || "websocket";
138
+ ok(`Delivery mode: ${mode}`);
139
+
140
+ if (mode === "polling") {
141
+ info(`Poll interval: ${acct.pollIntervalMs || 5000}ms`);
142
+ }
143
+
144
+ // ── Summary ──
145
+ lines.push("", "── Summary ──");
146
+ const total = pass + warn + fail;
147
+ lines.push(`Passed: ${pass} | Warnings: ${warn} | Failed: ${fail} | Total: ${total}`);
148
+
149
+ if (fail > 0) {
150
+ lines.push("", "Some checks FAILED. Please fix the issues above.");
151
+ } else if (warn > 0) {
152
+ lines.push("", "All critical checks passed, but there are warnings to review.");
153
+ } else {
154
+ lines.push("", "All checks passed. BotCord is ready!");
155
+ }
156
+
157
+ return { text: lines.join("\n") };
158
+ },
159
+ };
160
+ }
@@ -0,0 +1,449 @@
1
+ /**
2
+ * BotCord CLI commands for registration and credentials management.
3
+ *
4
+ * Supports:
5
+ * - `openclaw botcord-register`
6
+ * - `openclaw botcord-import`
7
+ * - `openclaw botcord-export`
8
+ */
9
+ import { existsSync } from "node:fs";
10
+ import {
11
+ defaultCredentialsFile,
12
+ loadStoredCredentials,
13
+ resolveCredentialsFilePath,
14
+ type StoredBotCordCredentials,
15
+ writeCredentialsFile,
16
+ } from "../credentials.js";
17
+ import {
18
+ derivePublicKey,
19
+ generateKeypair,
20
+ signChallenge,
21
+ } from "../crypto.js";
22
+ import {
23
+ getSingleAccountModeError,
24
+ resolveAccountConfig,
25
+ } from "../config.js";
26
+ import { normalizeAndValidateHubUrl } from "../hub-url.js";
27
+ import { getBotCordRuntime } from "../runtime.js";
28
+
29
+ const DEFAULT_HUB = "https://api.botcord.chat";
30
+
31
+ interface RegisterResult {
32
+ agentId: string;
33
+ keyId: string;
34
+ displayName: string;
35
+ hub: string;
36
+ credentialsFile: string;
37
+ claimUrl?: string;
38
+ }
39
+
40
+ interface ImportResult {
41
+ agentId: string;
42
+ keyId: string;
43
+ hub: string;
44
+ sourceFile: string;
45
+ credentialsFile: string;
46
+ }
47
+
48
+ interface ExportResult {
49
+ agentId: string;
50
+ keyId: string;
51
+ hub: string;
52
+ sourceFile?: string;
53
+ credentialsFile: string;
54
+ }
55
+
56
+ function buildRegistrationKeypair(config: Record<string, any>, newIdentity: boolean) {
57
+ if (newIdentity) return generateKeypair();
58
+
59
+ const existing = resolveAccountConfig(config);
60
+ if (!existing.privateKey) return generateKeypair();
61
+
62
+ const publicKey = existing.publicKey || derivePublicKey(existing.privateKey);
63
+ return {
64
+ privateKey: existing.privateKey,
65
+ publicKey,
66
+ pubkeyFormatted: `ed25519:${publicKey}`,
67
+ };
68
+ }
69
+
70
+ function stripInlineCredentials(botcordCfg: Record<string, any>): Record<string, any> {
71
+ const next = { ...botcordCfg };
72
+ delete next.hubUrl;
73
+ delete next.agentId;
74
+ delete next.keyId;
75
+ delete next.privateKey;
76
+ delete next.publicKey;
77
+ return next;
78
+ }
79
+
80
+ function buildNextConfig(
81
+ config: Record<string, any>,
82
+ credentialsFile: string,
83
+ ): Record<string, any> {
84
+ const currentBotcord = ((config.channels as Record<string, any>)?.botcord ?? {}) as Record<string, any>;
85
+ return {
86
+ ...config,
87
+ channels: {
88
+ ...(config.channels as Record<string, any>),
89
+ botcord: {
90
+ ...stripInlineCredentials(currentBotcord),
91
+ enabled: true,
92
+ credentialsFile,
93
+ deliveryMode:
94
+ currentBotcord.deliveryMode === "polling"
95
+ ? "polling"
96
+ : "websocket",
97
+ notifySession:
98
+ currentBotcord.notifySession ||
99
+ "agent:main:main",
100
+ },
101
+ },
102
+ session: {
103
+ ...(config.session as Record<string, any>),
104
+ dmScope:
105
+ (config.session as Record<string, any>)?.dmScope ||
106
+ "per-channel-peer",
107
+ },
108
+ };
109
+ }
110
+
111
+ async function persistCredentials(params: {
112
+ config: Record<string, any>;
113
+ credentials: StoredBotCordCredentials;
114
+ destinationFile?: string;
115
+ }): Promise<string> {
116
+ const runtime = getBotCordRuntime();
117
+ const existingAccount = resolveAccountConfig(params.config);
118
+ const credentialsFile = writeCredentialsFile(
119
+ params.destinationFile || existingAccount.credentialsFile || defaultCredentialsFile(params.credentials.agentId),
120
+ params.credentials,
121
+ );
122
+ await runtime.config.writeConfigFile(buildNextConfig(params.config, credentialsFile));
123
+ return credentialsFile;
124
+ }
125
+
126
+ function resolveManagedCredentialsFile(accountConfig: Record<string, any>): string | undefined {
127
+ const credentialsFile = accountConfig.credentialsFile;
128
+ return typeof credentialsFile === "string" && credentialsFile.trim()
129
+ ? resolveCredentialsFilePath(credentialsFile)
130
+ : undefined;
131
+ }
132
+
133
+ function buildExportableCredentials(config: Record<string, any>): {
134
+ credentials: StoredBotCordCredentials;
135
+ sourceFile?: string;
136
+ } {
137
+ const existingAccount = resolveAccountConfig(config);
138
+ const sourceFile = resolveManagedCredentialsFile(existingAccount);
139
+
140
+ if (sourceFile && !existingAccount.privateKey) {
141
+ throw new Error(`BotCord credentialsFile is configured but could not be loaded: ${sourceFile}`);
142
+ }
143
+
144
+ if (!existingAccount.hubUrl || !existingAccount.agentId || !existingAccount.keyId || !existingAccount.privateKey) {
145
+ throw new Error("BotCord is not fully configured (need hubUrl, agentId, keyId, privateKey)");
146
+ }
147
+
148
+ let displayName: string | undefined;
149
+ if (sourceFile) {
150
+ try {
151
+ displayName = loadStoredCredentials(sourceFile).displayName;
152
+ } catch {
153
+ displayName = undefined;
154
+ }
155
+ }
156
+
157
+ const derivedPublicKey = derivePublicKey(existingAccount.privateKey);
158
+
159
+ return {
160
+ sourceFile,
161
+ credentials: {
162
+ version: 1,
163
+ hubUrl: existingAccount.hubUrl,
164
+ agentId: existingAccount.agentId,
165
+ keyId: existingAccount.keyId,
166
+ privateKey: existingAccount.privateKey,
167
+ publicKey: derivedPublicKey,
168
+ displayName,
169
+ savedAt: new Date().toISOString(),
170
+ },
171
+ };
172
+ }
173
+
174
+ export async function registerAgent(opts: {
175
+ name: string;
176
+ bio: string;
177
+ hub: string;
178
+ config: Record<string, any>;
179
+ newIdentity?: boolean;
180
+ }): Promise<RegisterResult> {
181
+ const {
182
+ name,
183
+ bio,
184
+ hub,
185
+ config,
186
+ newIdentity = false,
187
+ } = opts;
188
+ const singleAccountError = getSingleAccountModeError(config);
189
+ if (singleAccountError) {
190
+ throw new Error(singleAccountError);
191
+ }
192
+
193
+ const existingAccount = resolveAccountConfig(config);
194
+ const managedCredentialsFile = resolveManagedCredentialsFile(existingAccount);
195
+ if (!newIdentity && managedCredentialsFile && !existingAccount.privateKey) {
196
+ throw new Error(
197
+ `BotCord credentialsFile is configured but could not be loaded: ${managedCredentialsFile}`,
198
+ );
199
+ }
200
+
201
+ // 1. Reuse the existing keypair unless the caller explicitly requests a new identity.
202
+ const keys = buildRegistrationKeypair(config, newIdentity);
203
+ const normalizedBio = bio.trim() || `${name} on BotCord`;
204
+ const normalizedHub = normalizeAndValidateHubUrl(hub);
205
+
206
+ // 2. Register with Hub
207
+ const regResp = await fetch(`${normalizedHub}/registry/agents`, {
208
+ method: "POST",
209
+ headers: { "Content-Type": "application/json" },
210
+ body: JSON.stringify({
211
+ display_name: name,
212
+ pubkey: keys.pubkeyFormatted,
213
+ bio: normalizedBio,
214
+ }),
215
+ });
216
+
217
+ if (!regResp.ok) {
218
+ const body = await regResp.text();
219
+ throw new Error(`Registration failed (${regResp.status}): ${body}`);
220
+ }
221
+
222
+ const regData = (await regResp.json()) as {
223
+ agent_id: string;
224
+ key_id: string;
225
+ challenge: string;
226
+ };
227
+
228
+ // 3. Sign challenge
229
+ const sig = signChallenge(keys.privateKey, regData.challenge);
230
+
231
+ // 4. Verify (challenge-response)
232
+ const verifyResp = await fetch(
233
+ `${normalizedHub}/registry/agents/${regData.agent_id}/verify`,
234
+ {
235
+ method: "POST",
236
+ headers: { "Content-Type": "application/json" },
237
+ body: JSON.stringify({
238
+ key_id: regData.key_id,
239
+ challenge: regData.challenge,
240
+ sig,
241
+ }),
242
+ },
243
+ );
244
+
245
+ if (!verifyResp.ok) {
246
+ const body = await verifyResp.text();
247
+ throw new Error(`Verification failed (${verifyResp.status}): ${body}`);
248
+ }
249
+
250
+ const verifyData = (await verifyResp.json()) as { token: string };
251
+
252
+ // 5. Fetch claim URL (best-effort)
253
+ let claimUrl: string | undefined;
254
+ try {
255
+ const claimResp = await fetch(
256
+ `${normalizedHub}/registry/agents/${regData.agent_id}/claim-link`,
257
+ {
258
+ headers: { Authorization: `Bearer ${verifyData.token}` },
259
+ },
260
+ );
261
+ if (claimResp.ok) {
262
+ const claimData = (await claimResp.json()) as { claim_url: string };
263
+ claimUrl = claimData.claim_url;
264
+ }
265
+ } catch {
266
+ // Best-effort — claim URL fetch failure should not block registration.
267
+ }
268
+
269
+ // 6. Write credentials via OpenClaw's config API
270
+ const credentialsFile = await persistCredentials({
271
+ config,
272
+ credentials: {
273
+ version: 1,
274
+ hubUrl: normalizedHub,
275
+ agentId: regData.agent_id,
276
+ keyId: regData.key_id,
277
+ privateKey: keys.privateKey,
278
+ publicKey: keys.publicKey,
279
+ displayName: name,
280
+ savedAt: new Date().toISOString(),
281
+ },
282
+ });
283
+
284
+ return {
285
+ agentId: regData.agent_id,
286
+ keyId: regData.key_id,
287
+ displayName: name,
288
+ hub: normalizedHub,
289
+ credentialsFile,
290
+ claimUrl,
291
+ };
292
+ }
293
+
294
+ export async function importAgentCredentials(opts: {
295
+ file: string;
296
+ config: Record<string, any>;
297
+ destinationFile?: string;
298
+ }): Promise<ImportResult> {
299
+ const {
300
+ file,
301
+ config,
302
+ destinationFile,
303
+ } = opts;
304
+ const singleAccountError = getSingleAccountModeError(config);
305
+ if (singleAccountError) {
306
+ throw new Error(singleAccountError);
307
+ }
308
+
309
+ const sourceFile = resolveCredentialsFilePath(file);
310
+ const credentials = loadStoredCredentials(sourceFile);
311
+ const credentialsFile = await persistCredentials({
312
+ config,
313
+ credentials,
314
+ destinationFile,
315
+ });
316
+
317
+ return {
318
+ agentId: credentials.agentId,
319
+ keyId: credentials.keyId,
320
+ hub: credentials.hubUrl,
321
+ sourceFile,
322
+ credentialsFile,
323
+ };
324
+ }
325
+
326
+ export async function exportAgentCredentials(opts: {
327
+ config: Record<string, any>;
328
+ destinationFile: string;
329
+ force?: boolean;
330
+ }): Promise<ExportResult> {
331
+ const {
332
+ config,
333
+ destinationFile,
334
+ force = false,
335
+ } = opts;
336
+ const singleAccountError = getSingleAccountModeError(config);
337
+ if (singleAccountError) {
338
+ throw new Error(singleAccountError);
339
+ }
340
+
341
+ const resolvedDestinationFile = resolveCredentialsFilePath(destinationFile);
342
+ if (!force && existsSync(resolvedDestinationFile)) {
343
+ throw new Error(
344
+ `Destination credentials file already exists: ${resolvedDestinationFile} (pass --force to overwrite)`,
345
+ );
346
+ }
347
+
348
+ const { credentials, sourceFile } = buildExportableCredentials(config);
349
+ const credentialsFile = writeCredentialsFile(resolvedDestinationFile, credentials);
350
+
351
+ return {
352
+ agentId: credentials.agentId,
353
+ keyId: credentials.keyId,
354
+ hub: credentials.hubUrl,
355
+ sourceFile,
356
+ credentialsFile,
357
+ };
358
+ }
359
+
360
+ export function createRegisterCli() {
361
+ return {
362
+ setup: (ctx: any) => {
363
+ ctx.program
364
+ .command("botcord-register")
365
+ .description("Register a new BotCord agent and configure the plugin")
366
+ .requiredOption("--name <name>", "Agent display name")
367
+ .option("--bio <bio>", "Agent bio/description", "")
368
+ .option("--hub <url>", "Hub URL", DEFAULT_HUB)
369
+ .option("--new-identity", "Generate a fresh keypair instead of reusing existing BotCord credentials", false)
370
+ .action(async (options: { name: string; bio: string; hub: string; newIdentity?: boolean }) => {
371
+ try {
372
+ const result = await registerAgent({
373
+ ...options,
374
+ config: ctx.config,
375
+ });
376
+ ctx.logger.info(`Agent registered successfully!`);
377
+ ctx.logger.info(` Agent ID: ${result.agentId}`);
378
+ ctx.logger.info(` Key ID: ${result.keyId}`);
379
+ ctx.logger.info(` Display name: ${result.displayName}`);
380
+ ctx.logger.info(` Hub: ${result.hub}`);
381
+ ctx.logger.info(` Credentials: ${result.credentialsFile}`);
382
+ if (result.claimUrl) {
383
+ ctx.logger.info(` Claim URL: ${result.claimUrl}`);
384
+ }
385
+ ctx.logger.info(``);
386
+ ctx.logger.info(`Restart OpenClaw to activate: openclaw gateway restart`);
387
+ } catch (err: any) {
388
+ ctx.logger.error(`Registration failed: ${err.message}`);
389
+ throw err;
390
+ }
391
+ });
392
+ ctx.program
393
+ .command("botcord-import")
394
+ .alias("botcord_import")
395
+ .description("Import existing BotCord credentials from a file and configure the plugin")
396
+ .requiredOption("--file <path>", "Path to an existing BotCord credentials JSON file")
397
+ .option("--dest <path>", "Destination path for the managed credentials file")
398
+ .action(async (options: { file: string; dest?: string }) => {
399
+ try {
400
+ const result = await importAgentCredentials({
401
+ file: options.file,
402
+ destinationFile: options.dest,
403
+ config: ctx.config,
404
+ });
405
+ ctx.logger.info("BotCord credentials imported successfully!");
406
+ ctx.logger.info(` Agent ID: ${result.agentId}`);
407
+ ctx.logger.info(` Key ID: ${result.keyId}`);
408
+ ctx.logger.info(` Hub: ${result.hub}`);
409
+ ctx.logger.info(` Source: ${result.sourceFile}`);
410
+ ctx.logger.info(` Credentials: ${result.credentialsFile}`);
411
+ ctx.logger.info("");
412
+ ctx.logger.info("Restart OpenClaw to activate: openclaw gateway restart");
413
+ } catch (err: any) {
414
+ ctx.logger.error(`Import failed: ${err.message}`);
415
+ throw err;
416
+ }
417
+ });
418
+ ctx.program
419
+ .command("botcord-export")
420
+ .alias("botcord_export")
421
+ .description("Export the active BotCord credentials to a file")
422
+ .requiredOption("--dest <path>", "Destination path for the exported BotCord credentials JSON file")
423
+ .option("--force", "Overwrite the destination file if it already exists", false)
424
+ .action(async (options: { dest: string; force?: boolean }) => {
425
+ try {
426
+ const result = await exportAgentCredentials({
427
+ config: ctx.config,
428
+ destinationFile: options.dest,
429
+ force: options.force,
430
+ });
431
+ ctx.logger.info("BotCord credentials exported successfully!");
432
+ ctx.logger.info(` Agent ID: ${result.agentId}`);
433
+ ctx.logger.info(` Key ID: ${result.keyId}`);
434
+ ctx.logger.info(` Hub: ${result.hub}`);
435
+ if (result.sourceFile) {
436
+ ctx.logger.info(` Source: ${result.sourceFile}`);
437
+ } else {
438
+ ctx.logger.info(" Source: inline config");
439
+ }
440
+ ctx.logger.info(` Exported to: ${result.credentialsFile}`);
441
+ } catch (err: any) {
442
+ ctx.logger.error(`Export failed: ${err.message}`);
443
+ throw err;
444
+ }
445
+ });
446
+ },
447
+ commands: ["botcord-register", "botcord-import", "botcord-export"],
448
+ };
449
+ }
@@ -0,0 +1,42 @@
1
+ /**
2
+ * /botcord_token — Output the current JWT token for the configured account.
3
+ */
4
+ import {
5
+ getSingleAccountModeError,
6
+ resolveAccountConfig,
7
+ isAccountConfigured,
8
+ } from "../config.js";
9
+ import { BotCordClient } from "../client.js";
10
+ import { getConfig as getAppConfig } from "../runtime.js";
11
+
12
+ export function createTokenCommand() {
13
+ return {
14
+ name: "botcord_token",
15
+ description: "Fetch and display the current BotCord JWT token.",
16
+ acceptsArgs: false,
17
+ requireAuth: true,
18
+ handler: async () => {
19
+ const cfg = getAppConfig();
20
+ if (!cfg) {
21
+ return { text: "[FAIL] No OpenClaw configuration available" };
22
+ }
23
+ const singleAccountError = getSingleAccountModeError(cfg);
24
+ if (singleAccountError) {
25
+ return { text: `[FAIL] ${singleAccountError}` };
26
+ }
27
+
28
+ const acct = resolveAccountConfig(cfg);
29
+ if (!isAccountConfigured(acct)) {
30
+ return { text: "[FAIL] BotCord is not fully configured (need hubUrl, agentId, keyId, privateKey)" };
31
+ }
32
+
33
+ try {
34
+ const client = new BotCordClient(acct);
35
+ const token = await client.ensureToken();
36
+ return { text: token };
37
+ } catch (err: any) {
38
+ return { text: `[FAIL] Token refresh failed: ${err.message}` };
39
+ }
40
+ },
41
+ };
42
+ }