@botcord/botcord 0.3.6 → 0.3.7
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/index.ts +31 -10
- package/openclaw.plugin.json +1 -1
- package/package.json +2 -1
- package/skills/botcord/SKILL.md +83 -381
- package/skills/botcord/SKILL_PROACTIVE.md +116 -0
- package/skills/botcord/SKILL_SCENARIOS.md +263 -0
- package/skills/botcord/onboarding_instruction.md +45 -0
- package/skills/botcord-account/SKILL.md +195 -0
- package/skills/botcord-messaging/SKILL.md +188 -0
- package/skills/botcord-payment/SKILL.md +90 -0
- package/skills/botcord-social/SKILL.md +106 -0
- package/src/client.ts +100 -3
- package/src/commands/bind.ts +4 -3
- package/src/commands/healthcheck.ts +2 -11
- package/src/commands/uninstall.ts +129 -0
- package/src/credentials.ts +26 -168
- package/src/crypto.ts +1 -155
- package/src/dynamic-context.ts +33 -16
- package/src/hub-url.ts +1 -41
- package/src/memory.ts +50 -0
- package/src/session-key.ts +1 -59
- package/src/tools/account.ts +16 -32
- package/src/tools/api.ts +112 -0
- package/src/tools/bind.ts +10 -30
- package/src/tools/contacts.ts +26 -37
- package/src/tools/directory.ts +8 -29
- package/src/tools/messaging.ts +25 -40
- package/src/tools/payment.ts +27 -37
- package/src/tools/register.ts +5 -4
- package/src/tools/reset-credential.ts +6 -5
- package/src/tools/room-context.ts +10 -31
- package/src/tools/rooms.ts +35 -41
- package/src/tools/subscription.ts +27 -38
- package/src/tools/tool-result.ts +10 -3
- package/src/tools/topics.ts +17 -31
- package/src/types.ts +3 -283
- package/src/onboarding-hook.ts +0 -139
package/src/tools/payment.ts
CHANGED
|
@@ -1,14 +1,8 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* botcord_payment — Unified payment and transaction tool for BotCord coin flows.
|
|
3
3
|
*/
|
|
4
|
-
import {
|
|
5
|
-
|
|
6
|
-
resolveAccountConfig,
|
|
7
|
-
isAccountConfigured,
|
|
8
|
-
} from "../config.js";
|
|
9
|
-
import { BotCordClient } from "../client.js";
|
|
10
|
-
import { attachTokenPersistence } from "../credentials.js";
|
|
11
|
-
import { getConfig as getAppConfig } from "../runtime.js";
|
|
4
|
+
import { withClient } from "./with-client.js";
|
|
5
|
+
import { validationError, dryRunResult } from "./tool-result.js";
|
|
12
6
|
import { formatCoinAmount, parseCoinToMinor } from "./coin-format.js";
|
|
13
7
|
import { executeTransfer, isPeerContact, formatFollowUpDeliverySummary } from "./payment-transfer.js";
|
|
14
8
|
|
|
@@ -287,27 +281,18 @@ export function createPaymentTool(opts?: { name?: string; description?: string }
|
|
|
287
281
|
type: "string" as const,
|
|
288
282
|
description: "Filter by transaction type — for ledger",
|
|
289
283
|
},
|
|
284
|
+
dry_run: {
|
|
285
|
+
type: "boolean" as const,
|
|
286
|
+
description: "Preview the request without executing. Returns the API call that would be made.",
|
|
287
|
+
},
|
|
290
288
|
},
|
|
291
289
|
required: ["action"],
|
|
292
290
|
},
|
|
293
291
|
execute: async (_toolCallId: any, args: any) => {
|
|
294
|
-
|
|
295
|
-
if (!cfg) return { error: "No configuration available" };
|
|
296
|
-
const singleAccountError = getSingleAccountModeError(cfg);
|
|
297
|
-
if (singleAccountError) return { error: singleAccountError };
|
|
298
|
-
|
|
299
|
-
const acct = resolveAccountConfig(cfg);
|
|
300
|
-
if (!isAccountConfigured(acct)) {
|
|
301
|
-
return { error: "BotCord is not configured." };
|
|
302
|
-
}
|
|
303
|
-
|
|
304
|
-
const client = new BotCordClient(acct);
|
|
305
|
-
attachTokenPersistence(client, acct);
|
|
306
|
-
|
|
307
|
-
try {
|
|
292
|
+
return withClient(async (client) => {
|
|
308
293
|
switch (args.action) {
|
|
309
294
|
case "recipient_verify": {
|
|
310
|
-
if (!args.agent_id) return
|
|
295
|
+
if (!args.agent_id) return validationError("agent_id is required");
|
|
311
296
|
const agent = await client.resolve(args.agent_id);
|
|
312
297
|
return { result: formatRecipient(agent), data: agent };
|
|
313
298
|
}
|
|
@@ -327,10 +312,11 @@ export function createPaymentTool(opts?: { name?: string; description?: string }
|
|
|
327
312
|
}
|
|
328
313
|
|
|
329
314
|
case "transfer": {
|
|
330
|
-
if (!args.to_agent_id) return
|
|
331
|
-
if (!args.amount) return
|
|
315
|
+
if (!args.to_agent_id) return validationError("to_agent_id is required");
|
|
316
|
+
if (!args.amount) return validationError("amount is required");
|
|
332
317
|
const transferMinor = parseCoinToMinor(args.amount);
|
|
333
|
-
if (transferMinor === null) return
|
|
318
|
+
if (transferMinor === null) return validationError("amount must be a valid number (e.g. \"10\" or \"9.50\")");
|
|
319
|
+
if (args.dry_run) return dryRunResult("POST", "/wallet/transfers", { to_agent_id: args.to_agent_id, amount_minor: transferMinor, memo: args.memo });
|
|
334
320
|
|
|
335
321
|
const isContact = await isPeerContact(client, args.to_agent_id);
|
|
336
322
|
if (!isContact && args.confirmed !== true) {
|
|
@@ -355,9 +341,11 @@ export function createPaymentTool(opts?: { name?: string; description?: string }
|
|
|
355
341
|
}
|
|
356
342
|
|
|
357
343
|
case "topup": {
|
|
358
|
-
if (!args.amount) return
|
|
344
|
+
if (!args.amount) return validationError("amount is required");
|
|
359
345
|
const topupMinor = parseCoinToMinor(args.amount);
|
|
360
|
-
if (topupMinor === null) return
|
|
346
|
+
if (topupMinor === null) return validationError("amount must be a valid number (e.g. \"10\" or \"9.50\")");
|
|
347
|
+
if (args.dry_run) return dryRunResult("POST", "/wallet/topups", { amount_minor: topupMinor, channel: args.channel });
|
|
348
|
+
|
|
361
349
|
const topup = await client.createTopup({
|
|
362
350
|
amount_minor: topupMinor,
|
|
363
351
|
channel: args.channel,
|
|
@@ -368,14 +356,16 @@ export function createPaymentTool(opts?: { name?: string; description?: string }
|
|
|
368
356
|
}
|
|
369
357
|
|
|
370
358
|
case "withdraw": {
|
|
371
|
-
if (!args.amount) return
|
|
359
|
+
if (!args.amount) return validationError("amount is required");
|
|
372
360
|
const withdrawMinor = parseCoinToMinor(args.amount);
|
|
373
|
-
if (withdrawMinor === null) return
|
|
361
|
+
if (withdrawMinor === null) return validationError("amount must be a valid number (e.g. \"10\" or \"9.50\")");
|
|
374
362
|
let feeMinor: string | undefined;
|
|
375
363
|
if (args.fee) {
|
|
376
364
|
feeMinor = parseCoinToMinor(args.fee) ?? undefined;
|
|
377
|
-
if (feeMinor === undefined) return
|
|
365
|
+
if (feeMinor === undefined) return validationError("fee must be a valid number (e.g. \"1\" or \"0.50\")");
|
|
378
366
|
}
|
|
367
|
+
if (args.dry_run) return dryRunResult("POST", "/wallet/withdrawals", { amount_minor: withdrawMinor, destination_type: args.destination_type });
|
|
368
|
+
|
|
379
369
|
const withdrawal = await client.createWithdrawal({
|
|
380
370
|
amount_minor: withdrawMinor,
|
|
381
371
|
fee_minor: feeMinor,
|
|
@@ -387,23 +377,23 @@ export function createPaymentTool(opts?: { name?: string; description?: string }
|
|
|
387
377
|
}
|
|
388
378
|
|
|
389
379
|
case "cancel_withdrawal": {
|
|
390
|
-
if (!args.withdrawal_id) return
|
|
380
|
+
if (!args.withdrawal_id) return validationError("withdrawal_id is required");
|
|
381
|
+
if (args.dry_run) return dryRunResult("POST", `/wallet/withdrawals/${args.withdrawal_id}/cancel`);
|
|
382
|
+
|
|
391
383
|
const withdrawal = await client.cancelWithdrawal(args.withdrawal_id);
|
|
392
384
|
return { result: formatWithdrawal(withdrawal), data: sanitizeWithdrawal(withdrawal) };
|
|
393
385
|
}
|
|
394
386
|
|
|
395
387
|
case "tx_status": {
|
|
396
|
-
if (!args.tx_id) return
|
|
388
|
+
if (!args.tx_id) return validationError("tx_id is required");
|
|
397
389
|
const tx = await client.getWalletTransaction(args.tx_id);
|
|
398
390
|
return { result: formatTransaction(tx), data: sanitizeTransaction(tx) };
|
|
399
391
|
}
|
|
400
392
|
|
|
401
393
|
default:
|
|
402
|
-
return
|
|
394
|
+
return validationError(`Unknown action: ${args.action}`);
|
|
403
395
|
}
|
|
404
|
-
}
|
|
405
|
-
return { error: `Payment action failed: ${err.message}` };
|
|
406
|
-
}
|
|
396
|
+
});
|
|
407
397
|
},
|
|
408
398
|
};
|
|
409
399
|
}
|
package/src/tools/register.ts
CHANGED
|
@@ -4,6 +4,7 @@
|
|
|
4
4
|
import { registerAgent } from "../commands/register.js";
|
|
5
5
|
import { getConfig as getAppConfig } from "../runtime.js";
|
|
6
6
|
import { DEFAULT_HUB } from "../constants.js";
|
|
7
|
+
import { validationError, configError, classifyError } from "./tool-result.js";
|
|
7
8
|
|
|
8
9
|
export function createRegisterTool() {
|
|
9
10
|
return {
|
|
@@ -35,11 +36,11 @@ export function createRegisterTool() {
|
|
|
35
36
|
},
|
|
36
37
|
execute: async (toolCallId: any, args: any, signal?: any, onUpdate?: any) => {
|
|
37
38
|
if (!args.name) {
|
|
38
|
-
return
|
|
39
|
+
return validationError("name is required");
|
|
39
40
|
}
|
|
40
41
|
|
|
41
42
|
const cfg = getAppConfig();
|
|
42
|
-
if (!cfg) return
|
|
43
|
+
if (!cfg) return configError("No configuration available");
|
|
43
44
|
|
|
44
45
|
try {
|
|
45
46
|
const result = await registerAgent({
|
|
@@ -59,8 +60,8 @@ export function createRegisterTool() {
|
|
|
59
60
|
claim_url: result.claimUrl,
|
|
60
61
|
note: "Restart OpenClaw to activate: openclaw gateway restart",
|
|
61
62
|
};
|
|
62
|
-
} catch (err:
|
|
63
|
-
return
|
|
63
|
+
} catch (err: unknown) {
|
|
64
|
+
return classifyError(err);
|
|
64
65
|
}
|
|
65
66
|
},
|
|
66
67
|
};
|
|
@@ -3,6 +3,7 @@
|
|
|
3
3
|
*/
|
|
4
4
|
import { getConfig as getAppConfig } from "../runtime.js";
|
|
5
5
|
import { resetCredential } from "../reset-credential.js";
|
|
6
|
+
import { validationError, configError, classifyError } from "./tool-result.js";
|
|
6
7
|
|
|
7
8
|
export function createResetCredentialTool() {
|
|
8
9
|
return {
|
|
@@ -30,9 +31,9 @@ export function createResetCredentialTool() {
|
|
|
30
31
|
},
|
|
31
32
|
execute: async (_toolCallId: any, args: any) => {
|
|
32
33
|
const cfg = getAppConfig();
|
|
33
|
-
if (!cfg) return
|
|
34
|
-
if (!args.agent_id) return
|
|
35
|
-
if (!args.reset_code) return
|
|
34
|
+
if (!cfg) return configError("No configuration available");
|
|
35
|
+
if (!args.agent_id) return validationError("agent_id is required");
|
|
36
|
+
if (!args.reset_code) return validationError("reset_code is required");
|
|
36
37
|
|
|
37
38
|
try {
|
|
38
39
|
const result = await resetCredential({
|
|
@@ -50,8 +51,8 @@ export function createResetCredentialTool() {
|
|
|
50
51
|
credentials_file: result.credentialsFile,
|
|
51
52
|
note: "Restart OpenClaw to activate: openclaw gateway restart",
|
|
52
53
|
};
|
|
53
|
-
} catch (err:
|
|
54
|
-
return
|
|
54
|
+
} catch (err: unknown) {
|
|
55
|
+
return classifyError(err);
|
|
55
56
|
}
|
|
56
57
|
},
|
|
57
58
|
};
|
|
@@ -2,14 +2,8 @@
|
|
|
2
2
|
* botcord_room_context — Inspect room context, recent messages, and search
|
|
3
3
|
* message history within one room or across all joined rooms.
|
|
4
4
|
*/
|
|
5
|
-
import {
|
|
6
|
-
|
|
7
|
-
resolveAccountConfig,
|
|
8
|
-
isAccountConfigured,
|
|
9
|
-
} from "../config.js";
|
|
10
|
-
import { BotCordClient } from "../client.js";
|
|
11
|
-
import { attachTokenPersistence } from "../credentials.js";
|
|
12
|
-
import { getConfig as getAppConfig } from "../runtime.js";
|
|
5
|
+
import { withClient } from "./with-client.js";
|
|
6
|
+
import { validationError } from "./tool-result.js";
|
|
13
7
|
|
|
14
8
|
/** Normalize query input: accept a string or string[] from the LLM. */
|
|
15
9
|
function _normalizeQuery(raw: unknown): string | string[] | null {
|
|
@@ -89,28 +83,15 @@ export function createRoomContextTool() {
|
|
|
89
83
|
required: ["action"],
|
|
90
84
|
},
|
|
91
85
|
execute: async (toolCallId: any, args: any, signal?: any, onUpdate?: any) => {
|
|
92
|
-
|
|
93
|
-
if (!cfg) return { error: "No configuration available" };
|
|
94
|
-
const singleAccountError = getSingleAccountModeError(cfg);
|
|
95
|
-
if (singleAccountError) return { error: singleAccountError };
|
|
96
|
-
|
|
97
|
-
const acct = resolveAccountConfig(cfg);
|
|
98
|
-
if (!isAccountConfigured(acct)) {
|
|
99
|
-
return { error: "BotCord is not configured." };
|
|
100
|
-
}
|
|
101
|
-
|
|
102
|
-
const client = new BotCordClient(acct);
|
|
103
|
-
attachTokenPersistence(client, acct);
|
|
104
|
-
|
|
105
|
-
try {
|
|
86
|
+
return withClient(async (client) => {
|
|
106
87
|
switch (args.action) {
|
|
107
88
|
case "room_summary": {
|
|
108
|
-
if (!args.room_id) return
|
|
89
|
+
if (!args.room_id) return validationError("room_id is required for room_summary");
|
|
109
90
|
return await client.roomSummary(args.room_id, args.limit);
|
|
110
91
|
}
|
|
111
92
|
|
|
112
93
|
case "room_messages": {
|
|
113
|
-
if (!args.room_id) return
|
|
94
|
+
if (!args.room_id) return validationError("room_id is required for room_messages");
|
|
114
95
|
return await client.roomMessages(args.room_id, {
|
|
115
96
|
limit: args.limit,
|
|
116
97
|
before: args.before,
|
|
@@ -121,9 +102,9 @@ export function createRoomContextTool() {
|
|
|
121
102
|
}
|
|
122
103
|
|
|
123
104
|
case "room_search": {
|
|
124
|
-
if (!args.room_id) return
|
|
105
|
+
if (!args.room_id) return validationError("room_id is required for room_search");
|
|
125
106
|
const rsQuery = _normalizeQuery(args.query);
|
|
126
|
-
if (!rsQuery) return
|
|
107
|
+
if (!rsQuery) return validationError("query is required for room_search");
|
|
127
108
|
return await client.roomSearch(args.room_id, rsQuery, {
|
|
128
109
|
limit: args.limit,
|
|
129
110
|
before: args.before,
|
|
@@ -138,7 +119,7 @@ export function createRoomContextTool() {
|
|
|
138
119
|
|
|
139
120
|
case "global_search": {
|
|
140
121
|
const gsQuery = _normalizeQuery(args.query);
|
|
141
|
-
if (!gsQuery) return
|
|
122
|
+
if (!gsQuery) return validationError("query is required for global_search");
|
|
142
123
|
return await client.globalSearch(gsQuery, {
|
|
143
124
|
limit: args.limit,
|
|
144
125
|
roomId: args.room_id,
|
|
@@ -149,11 +130,9 @@ export function createRoomContextTool() {
|
|
|
149
130
|
}
|
|
150
131
|
|
|
151
132
|
default:
|
|
152
|
-
return
|
|
133
|
+
return validationError(`Unknown action: ${args.action}`);
|
|
153
134
|
}
|
|
154
|
-
}
|
|
155
|
-
return { error: `Room context action failed: ${err.message}` };
|
|
156
|
-
}
|
|
135
|
+
});
|
|
157
136
|
},
|
|
158
137
|
};
|
|
159
138
|
}
|
package/src/tools/rooms.ts
CHANGED
|
@@ -1,14 +1,8 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* botcord_rooms — Room lifecycle and membership management.
|
|
3
3
|
*/
|
|
4
|
-
import {
|
|
5
|
-
|
|
6
|
-
resolveAccountConfig,
|
|
7
|
-
isAccountConfigured,
|
|
8
|
-
} from "../config.js";
|
|
9
|
-
import { BotCordClient } from "../client.js";
|
|
10
|
-
import { attachTokenPersistence } from "../credentials.js";
|
|
11
|
-
import { getConfig as getAppConfig } from "../runtime.js";
|
|
4
|
+
import { withClient } from "./with-client.js";
|
|
5
|
+
import { validationError, dryRunResult } from "./tool-result.js";
|
|
12
6
|
|
|
13
7
|
export function createRoomsTool() {
|
|
14
8
|
return {
|
|
@@ -102,27 +96,19 @@ export function createRoomsTool() {
|
|
|
102
96
|
type: "boolean" as const,
|
|
103
97
|
description: "Mute or unmute the current member in a room — for mute",
|
|
104
98
|
},
|
|
99
|
+
dry_run: {
|
|
100
|
+
type: "boolean" as const,
|
|
101
|
+
description: "Preview the request without executing. Returns the API call that would be made.",
|
|
102
|
+
},
|
|
105
103
|
},
|
|
106
104
|
required: ["action"],
|
|
107
105
|
},
|
|
108
106
|
execute: async (toolCallId: any, args: any, signal?: any, onUpdate?: any) => {
|
|
109
|
-
|
|
110
|
-
if (!cfg) return { error: "No configuration available" };
|
|
111
|
-
const singleAccountError = getSingleAccountModeError(cfg);
|
|
112
|
-
if (singleAccountError) return { error: singleAccountError };
|
|
113
|
-
|
|
114
|
-
const acct = resolveAccountConfig(cfg);
|
|
115
|
-
if (!isAccountConfigured(acct)) {
|
|
116
|
-
return { error: "BotCord is not configured." };
|
|
117
|
-
}
|
|
118
|
-
|
|
119
|
-
const client = new BotCordClient(acct);
|
|
120
|
-
attachTokenPersistence(client, acct);
|
|
121
|
-
|
|
122
|
-
try {
|
|
107
|
+
return withClient(async (client) => {
|
|
123
108
|
switch (args.action) {
|
|
124
109
|
case "create":
|
|
125
|
-
if (!args.name) return
|
|
110
|
+
if (!args.name) return validationError("name is required");
|
|
111
|
+
if (args.dry_run) return dryRunResult("POST", "/hub/rooms", { name: args.name, visibility: args.visibility || "private", join_policy: args.join_policy, member_ids: args.member_ids });
|
|
126
112
|
return await client.createRoom({
|
|
127
113
|
name: args.name,
|
|
128
114
|
description: args.description,
|
|
@@ -138,14 +124,15 @@ export function createRoomsTool() {
|
|
|
138
124
|
});
|
|
139
125
|
|
|
140
126
|
case "list":
|
|
141
|
-
return await client.listMyRooms();
|
|
127
|
+
return { rooms: await client.listMyRooms() };
|
|
142
128
|
|
|
143
129
|
case "info":
|
|
144
|
-
if (!args.room_id) return
|
|
130
|
+
if (!args.room_id) return validationError("room_id is required");
|
|
145
131
|
return await client.getRoomInfo(args.room_id);
|
|
146
132
|
|
|
147
133
|
case "update":
|
|
148
|
-
if (!args.room_id) return
|
|
134
|
+
if (!args.room_id) return validationError("room_id is required");
|
|
135
|
+
if (args.dry_run) return dryRunResult("PATCH", `/hub/rooms/${args.room_id}`, { name: args.name, description: args.description, rule: args.rule, visibility: args.visibility, join_policy: args.join_policy, required_subscription_product_id: args.required_subscription_product_id, max_members: args.max_members, default_send: args.default_send, default_invite: args.default_invite, slow_mode_seconds: args.slow_mode_seconds });
|
|
149
136
|
return await client.updateRoom(args.room_id, {
|
|
150
137
|
name: args.name,
|
|
151
138
|
description: args.description,
|
|
@@ -163,7 +150,8 @@ export function createRoomsTool() {
|
|
|
163
150
|
return await client.discoverPublicRooms(args.name);
|
|
164
151
|
|
|
165
152
|
case "join":
|
|
166
|
-
if (!args.room_id) return
|
|
153
|
+
if (!args.room_id) return validationError("room_id is required");
|
|
154
|
+
if (args.dry_run) return dryRunResult("POST", `/hub/rooms/${args.room_id}/members`, { agent_id: "{self}" });
|
|
167
155
|
await client.joinRoom(args.room_id, {
|
|
168
156
|
can_send: args.can_send,
|
|
169
157
|
can_invite: args.can_invite,
|
|
@@ -171,21 +159,24 @@ export function createRoomsTool() {
|
|
|
171
159
|
return { ok: true, joined: args.room_id };
|
|
172
160
|
|
|
173
161
|
case "leave":
|
|
174
|
-
if (!args.room_id) return
|
|
162
|
+
if (!args.room_id) return validationError("room_id is required");
|
|
163
|
+
if (args.dry_run) return dryRunResult("POST", `/hub/rooms/${args.room_id}/leave`);
|
|
175
164
|
await client.leaveRoom(args.room_id);
|
|
176
165
|
return { ok: true, left: args.room_id };
|
|
177
166
|
|
|
178
167
|
case "dissolve":
|
|
179
|
-
if (!args.room_id) return
|
|
168
|
+
if (!args.room_id) return validationError("room_id is required");
|
|
169
|
+
if (args.dry_run) return dryRunResult("DELETE", `/hub/rooms/${args.room_id}`);
|
|
180
170
|
await client.dissolveRoom(args.room_id);
|
|
181
171
|
return { ok: true, dissolved: args.room_id };
|
|
182
172
|
|
|
183
173
|
case "members":
|
|
184
|
-
if (!args.room_id) return
|
|
185
|
-
return await client.getRoomMembers(args.room_id);
|
|
174
|
+
if (!args.room_id) return validationError("room_id is required");
|
|
175
|
+
return { members: await client.getRoomMembers(args.room_id) };
|
|
186
176
|
|
|
187
177
|
case "invite":
|
|
188
|
-
if (!args.room_id || !args.agent_id) return
|
|
178
|
+
if (!args.room_id || !args.agent_id) return validationError("room_id and agent_id are required");
|
|
179
|
+
if (args.dry_run) return dryRunResult("POST", `/hub/rooms/${args.room_id}/members`, { agent_id: args.agent_id, can_send: args.can_send, can_invite: args.can_invite });
|
|
189
180
|
await client.inviteToRoom(args.room_id, args.agent_id, {
|
|
190
181
|
can_send: args.can_send,
|
|
191
182
|
can_invite: args.can_invite,
|
|
@@ -193,22 +184,26 @@ export function createRoomsTool() {
|
|
|
193
184
|
return { ok: true, invited: args.agent_id, room: args.room_id };
|
|
194
185
|
|
|
195
186
|
case "remove_member":
|
|
196
|
-
if (!args.room_id || !args.agent_id) return
|
|
187
|
+
if (!args.room_id || !args.agent_id) return validationError("room_id and agent_id are required");
|
|
188
|
+
if (args.dry_run) return dryRunResult("DELETE", `/hub/rooms/${args.room_id}/members/${args.agent_id}`);
|
|
197
189
|
await client.removeMember(args.room_id, args.agent_id);
|
|
198
190
|
return { ok: true, removed: args.agent_id, room: args.room_id };
|
|
199
191
|
|
|
200
192
|
case "promote":
|
|
201
|
-
if (!args.room_id || !args.agent_id) return
|
|
193
|
+
if (!args.room_id || !args.agent_id) return validationError("room_id and agent_id are required");
|
|
194
|
+
if (args.dry_run) return dryRunResult("POST", `/hub/rooms/${args.room_id}/promote`, { agent_id: args.agent_id, role: args.role || "admin" });
|
|
202
195
|
await client.promoteMember(args.room_id, args.agent_id, args.role || "admin");
|
|
203
196
|
return { ok: true, promoted: args.agent_id, role: args.role || "admin", room: args.room_id };
|
|
204
197
|
|
|
205
198
|
case "transfer":
|
|
206
|
-
if (!args.room_id || !args.agent_id) return
|
|
199
|
+
if (!args.room_id || !args.agent_id) return validationError("room_id and agent_id are required");
|
|
200
|
+
if (args.dry_run) return dryRunResult("POST", `/hub/rooms/${args.room_id}/transfer`, { new_owner_id: args.agent_id });
|
|
207
201
|
await client.transferOwnership(args.room_id, args.agent_id);
|
|
208
202
|
return { ok: true, new_owner: args.agent_id, room: args.room_id };
|
|
209
203
|
|
|
210
204
|
case "permissions":
|
|
211
|
-
if (!args.room_id || !args.agent_id) return
|
|
205
|
+
if (!args.room_id || !args.agent_id) return validationError("room_id and agent_id are required");
|
|
206
|
+
if (args.dry_run) return dryRunResult("POST", `/hub/rooms/${args.room_id}/permissions`, { agent_id: args.agent_id, can_send: args.can_send, can_invite: args.can_invite });
|
|
212
207
|
await client.setMemberPermissions(args.room_id, args.agent_id, {
|
|
213
208
|
can_send: args.can_send,
|
|
214
209
|
can_invite: args.can_invite,
|
|
@@ -216,16 +211,15 @@ export function createRoomsTool() {
|
|
|
216
211
|
return { ok: true, agent: args.agent_id, room: args.room_id };
|
|
217
212
|
|
|
218
213
|
case "mute":
|
|
219
|
-
if (!args.room_id) return
|
|
214
|
+
if (!args.room_id) return validationError("room_id is required");
|
|
215
|
+
if (args.dry_run) return dryRunResult("POST", `/hub/rooms/${args.room_id}/mute`, { muted: args.muted ?? true });
|
|
220
216
|
await client.muteRoom(args.room_id, args.muted ?? true);
|
|
221
217
|
return { ok: true, room: args.room_id, muted: args.muted ?? true };
|
|
222
218
|
|
|
223
219
|
default:
|
|
224
|
-
return
|
|
220
|
+
return validationError(`Unknown action: ${args.action}`);
|
|
225
221
|
}
|
|
226
|
-
}
|
|
227
|
-
return { error: `Room action failed: ${err.message}` };
|
|
228
|
-
}
|
|
222
|
+
});
|
|
229
223
|
},
|
|
230
224
|
};
|
|
231
225
|
}
|
|
@@ -1,14 +1,8 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* botcord_subscription — Create and manage coin-priced subscription products.
|
|
3
3
|
*/
|
|
4
|
-
import {
|
|
5
|
-
|
|
6
|
-
resolveAccountConfig,
|
|
7
|
-
isAccountConfigured,
|
|
8
|
-
} from "../config.js";
|
|
9
|
-
import { BotCordClient } from "../client.js";
|
|
10
|
-
import { attachTokenPersistence } from "../credentials.js";
|
|
11
|
-
import { getConfig as getAppConfig } from "../runtime.js";
|
|
4
|
+
import { withClient } from "./with-client.js";
|
|
5
|
+
import { validationError, dryRunResult } from "./tool-result.js";
|
|
12
6
|
import { formatCoinAmount, parseCoinToMinor } from "./coin-format.js";
|
|
13
7
|
|
|
14
8
|
function formatProduct(product: any): string {
|
|
@@ -127,31 +121,23 @@ export function createSubscriptionTool() {
|
|
|
127
121
|
type: "number" as const,
|
|
128
122
|
description: "Slow mode interval in seconds — for create_subscription_room or bind_room_to_product",
|
|
129
123
|
},
|
|
124
|
+
dry_run: {
|
|
125
|
+
type: "boolean" as const,
|
|
126
|
+
description: "Preview the request without executing. Returns the API call that would be made.",
|
|
127
|
+
},
|
|
130
128
|
},
|
|
131
129
|
required: ["action"],
|
|
132
130
|
},
|
|
133
131
|
execute: async (toolCallId: any, args: any, signal?: any, onUpdate?: any) => {
|
|
134
|
-
|
|
135
|
-
if (!cfg) return { error: "No configuration available" };
|
|
136
|
-
const singleAccountError = getSingleAccountModeError(cfg);
|
|
137
|
-
if (singleAccountError) return { error: singleAccountError };
|
|
138
|
-
|
|
139
|
-
const acct = resolveAccountConfig(cfg);
|
|
140
|
-
if (!isAccountConfigured(acct)) {
|
|
141
|
-
return { error: "BotCord is not configured." };
|
|
142
|
-
}
|
|
143
|
-
|
|
144
|
-
const client = new BotCordClient(acct);
|
|
145
|
-
attachTokenPersistence(client, acct);
|
|
146
|
-
|
|
147
|
-
try {
|
|
132
|
+
return withClient(async (client) => {
|
|
148
133
|
switch (args.action) {
|
|
149
134
|
case "create_product": {
|
|
150
|
-
if (!args.name) return
|
|
151
|
-
if (!args.amount) return
|
|
152
|
-
if (!args.billing_interval) return
|
|
135
|
+
if (!args.name) return validationError("name is required");
|
|
136
|
+
if (!args.amount) return validationError("amount is required");
|
|
137
|
+
if (!args.billing_interval) return validationError("billing_interval is required");
|
|
153
138
|
const amountMinor = parseCoinToMinor(args.amount);
|
|
154
|
-
if (amountMinor === null) return
|
|
139
|
+
if (amountMinor === null) return validationError("amount must be a valid number (e.g. \"10\" or \"9.50\")");
|
|
140
|
+
if (args.dry_run) return dryRunResult("POST", "/subscriptions/products", { name: args.name, amount_minor: amountMinor, billing_interval: args.billing_interval });
|
|
155
141
|
const product = await client.createSubscriptionProduct({
|
|
156
142
|
name: args.name,
|
|
157
143
|
description: args.description,
|
|
@@ -173,14 +159,16 @@ export function createSubscriptionTool() {
|
|
|
173
159
|
}
|
|
174
160
|
|
|
175
161
|
case "archive_product": {
|
|
176
|
-
if (!args.product_id) return
|
|
162
|
+
if (!args.product_id) return validationError("product_id is required");
|
|
163
|
+
if (args.dry_run) return dryRunResult("POST", `/subscriptions/products/${args.product_id}/archive`);
|
|
177
164
|
const product = await client.archiveSubscriptionProduct(args.product_id);
|
|
178
165
|
return { result: formatProduct(product), data: product };
|
|
179
166
|
}
|
|
180
167
|
|
|
181
168
|
case "create_subscription_room": {
|
|
182
|
-
if (!args.product_id) return
|
|
183
|
-
if (!args.name) return
|
|
169
|
+
if (!args.product_id) return validationError("product_id is required");
|
|
170
|
+
if (!args.name) return validationError("name is required");
|
|
171
|
+
if (args.dry_run) return dryRunResult("POST", "/hub/rooms", { name: args.name, description: args.description, visibility: "public", join_policy: "open", required_subscription_product_id: args.product_id });
|
|
184
172
|
const room = await client.createRoom({
|
|
185
173
|
name: args.name,
|
|
186
174
|
description: args.description,
|
|
@@ -200,8 +188,9 @@ export function createSubscriptionTool() {
|
|
|
200
188
|
}
|
|
201
189
|
|
|
202
190
|
case "bind_room_to_product": {
|
|
203
|
-
if (!args.room_id) return
|
|
204
|
-
if (!args.product_id) return
|
|
191
|
+
if (!args.room_id) return validationError("room_id is required");
|
|
192
|
+
if (!args.product_id) return validationError("product_id is required");
|
|
193
|
+
if (args.dry_run) return dryRunResult("PATCH", `/hub/rooms/${args.room_id}`, { visibility: "public", join_policy: "open", required_subscription_product_id: args.product_id });
|
|
205
194
|
const room = await client.updateRoom(args.room_id, {
|
|
206
195
|
name: args.name,
|
|
207
196
|
description: args.description,
|
|
@@ -221,7 +210,8 @@ export function createSubscriptionTool() {
|
|
|
221
210
|
}
|
|
222
211
|
|
|
223
212
|
case "subscribe": {
|
|
224
|
-
if (!args.product_id) return
|
|
213
|
+
if (!args.product_id) return validationError("product_id is required");
|
|
214
|
+
if (args.dry_run) return dryRunResult("POST", `/subscriptions/products/${args.product_id}/subscribe`);
|
|
225
215
|
const subscription = await client.subscribeToProduct(args.product_id, args.idempotency_key);
|
|
226
216
|
return { result: formatSubscription(subscription), data: subscription };
|
|
227
217
|
}
|
|
@@ -232,23 +222,22 @@ export function createSubscriptionTool() {
|
|
|
232
222
|
}
|
|
233
223
|
|
|
234
224
|
case "list_subscribers": {
|
|
235
|
-
if (!args.product_id) return
|
|
225
|
+
if (!args.product_id) return validationError("product_id is required");
|
|
236
226
|
const subscriptions = await client.listProductSubscribers(args.product_id);
|
|
237
227
|
return { result: formatSubscriptionList(subscriptions), data: subscriptions };
|
|
238
228
|
}
|
|
239
229
|
|
|
240
230
|
case "cancel": {
|
|
241
|
-
if (!args.subscription_id) return
|
|
231
|
+
if (!args.subscription_id) return validationError("subscription_id is required");
|
|
232
|
+
if (args.dry_run) return dryRunResult("POST", `/subscriptions/${args.subscription_id}/cancel`);
|
|
242
233
|
const subscription = await client.cancelSubscription(args.subscription_id);
|
|
243
234
|
return { result: formatSubscription(subscription), data: subscription };
|
|
244
235
|
}
|
|
245
236
|
|
|
246
237
|
default:
|
|
247
|
-
return
|
|
238
|
+
return validationError(`Unknown action: ${args.action}`);
|
|
248
239
|
}
|
|
249
|
-
}
|
|
250
|
-
return { error: `Subscription action failed: ${err.message}` };
|
|
251
|
-
}
|
|
240
|
+
});
|
|
252
241
|
},
|
|
253
242
|
};
|
|
254
243
|
}
|
package/src/tools/tool-result.ts
CHANGED
|
@@ -28,7 +28,8 @@ export interface DryRunRequest {
|
|
|
28
28
|
method: string;
|
|
29
29
|
path: string;
|
|
30
30
|
body?: unknown;
|
|
31
|
-
query?: Record<string, string>;
|
|
31
|
+
query?: Record<string, string | string[]>;
|
|
32
|
+
note?: string;
|
|
32
33
|
}
|
|
33
34
|
|
|
34
35
|
export type DryRunResult = { ok: true; dry_run: true; request: DryRunRequest };
|
|
@@ -60,11 +61,17 @@ export function apiError(code: string, message: string, hint?: string): ToolFail
|
|
|
60
61
|
return fail("api", code, message, hint);
|
|
61
62
|
}
|
|
62
63
|
|
|
63
|
-
export function dryRunResult(method: string, path: string, body?: unknown, query?: Record<string, string
|
|
64
|
+
export function dryRunResult(method: string, path: string, body?: unknown, options?: { query?: Record<string, string | string[]>; note?: string }): DryRunResult {
|
|
64
65
|
return {
|
|
65
66
|
ok: true,
|
|
66
67
|
dry_run: true,
|
|
67
|
-
request: {
|
|
68
|
+
request: {
|
|
69
|
+
method,
|
|
70
|
+
path,
|
|
71
|
+
...(body !== undefined ? { body } : {}),
|
|
72
|
+
...(options?.query ? { query: options.query } : {}),
|
|
73
|
+
...(options?.note ? { note: options.note } : {}),
|
|
74
|
+
},
|
|
68
75
|
};
|
|
69
76
|
}
|
|
70
77
|
|