@botcord/botcord 0.3.4 → 0.3.6-beta.20260413082920

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.
@@ -1,14 +1,8 @@
1
1
  /**
2
2
  * botcord_rooms — Room lifecycle and membership management.
3
3
  */
4
- import {
5
- getSingleAccountModeError,
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
- const cfg = getAppConfig();
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 { error: "name is required" };
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 { error: "room_id is required" };
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 { error: "room_id is required" };
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,
@@ -160,10 +147,11 @@ export function createRoomsTool() {
160
147
  });
161
148
 
162
149
  case "discover":
163
- return await client.discoverRooms(args.name);
150
+ return await client.discoverPublicRooms(args.name);
164
151
 
165
152
  case "join":
166
- if (!args.room_id) return { error: "room_id is required" };
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 { error: "room_id is required" };
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 { error: "room_id is required" };
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 { error: "room_id is required" };
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 { error: "room_id and agent_id are required" };
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 { error: "room_id and agent_id are required" };
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 { error: "room_id and agent_id are required" };
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 { error: "room_id and agent_id are required" };
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 { error: "room_id and agent_id are required" };
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 { error: "room_id is required" };
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 { error: `Unknown action: ${args.action}` };
220
+ return validationError(`Unknown action: ${args.action}`);
225
221
  }
226
- } catch (err: any) {
227
- return { error: `Room action failed: ${err.message}` };
228
- }
222
+ });
229
223
  },
230
224
  };
231
225
  }
@@ -1,15 +1,9 @@
1
1
  /**
2
2
  * botcord_subscription — Create and manage coin-priced subscription products.
3
3
  */
4
- import {
5
- getSingleAccountModeError,
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";
12
- import { formatCoinAmount } from "./coin-format.js";
4
+ import { withClient } from "./with-client.js";
5
+ import { validationError, dryRunResult } from "./tool-result.js";
6
+ import { formatCoinAmount, parseCoinToMinor } from "./coin-format.js";
13
7
 
14
8
  function formatProduct(product: any): string {
15
9
  return [
@@ -98,14 +92,14 @@ export function createSubscriptionTool() {
98
92
  type: "string" as const,
99
93
  description: "Room rule/instructions — for create_subscription_room or bind_room_to_product",
100
94
  },
101
- amount_minor: {
95
+ amount: {
102
96
  type: "string" as const,
103
- description: "Price in minor coin units — for create_product",
97
+ description: "Price in COIN (supports up to 2 decimals, e.g. \"10\" or \"9.50\") — for create_product",
104
98
  },
105
99
  billing_interval: {
106
100
  type: "string" as const,
107
- enum: ["week", "month"],
108
- description: "Billing interval — for create_product",
101
+ enum: ["week", "month", "once"],
102
+ description: "Billing interval — for create_product. Use \"once\" for one-time payment (no recurring charges)",
109
103
  },
110
104
  asset_code: {
111
105
  type: "string" as const,
@@ -127,33 +121,27 @@ 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
- const cfg = getAppConfig();
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 { error: "name is required" };
151
- if (!args.amount_minor) return { error: "amount_minor is required" };
152
- if (!args.billing_interval) return { error: "billing_interval is required" };
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");
138
+ const amountMinor = parseCoinToMinor(args.amount);
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 });
153
141
  const product = await client.createSubscriptionProduct({
154
142
  name: args.name,
155
143
  description: args.description,
156
- amount_minor: args.amount_minor,
144
+ amount_minor: amountMinor,
157
145
  billing_interval: args.billing_interval,
158
146
  asset_code: args.asset_code,
159
147
  });
@@ -171,14 +159,16 @@ export function createSubscriptionTool() {
171
159
  }
172
160
 
173
161
  case "archive_product": {
174
- if (!args.product_id) return { error: "product_id is required" };
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`);
175
164
  const product = await client.archiveSubscriptionProduct(args.product_id);
176
165
  return { result: formatProduct(product), data: product };
177
166
  }
178
167
 
179
168
  case "create_subscription_room": {
180
- if (!args.product_id) return { error: "product_id is required" };
181
- if (!args.name) return { error: "name is required" };
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 });
182
172
  const room = await client.createRoom({
183
173
  name: args.name,
184
174
  description: args.description,
@@ -198,8 +188,9 @@ export function createSubscriptionTool() {
198
188
  }
199
189
 
200
190
  case "bind_room_to_product": {
201
- if (!args.room_id) return { error: "room_id is required" };
202
- if (!args.product_id) return { error: "product_id is required" };
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 });
203
194
  const room = await client.updateRoom(args.room_id, {
204
195
  name: args.name,
205
196
  description: args.description,
@@ -219,7 +210,8 @@ export function createSubscriptionTool() {
219
210
  }
220
211
 
221
212
  case "subscribe": {
222
- if (!args.product_id) return { error: "product_id is required" };
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`);
223
215
  const subscription = await client.subscribeToProduct(args.product_id, args.idempotency_key);
224
216
  return { result: formatSubscription(subscription), data: subscription };
225
217
  }
@@ -230,23 +222,22 @@ export function createSubscriptionTool() {
230
222
  }
231
223
 
232
224
  case "list_subscribers": {
233
- if (!args.product_id) return { error: "product_id is required" };
225
+ if (!args.product_id) return validationError("product_id is required");
234
226
  const subscriptions = await client.listProductSubscribers(args.product_id);
235
227
  return { result: formatSubscriptionList(subscriptions), data: subscriptions };
236
228
  }
237
229
 
238
230
  case "cancel": {
239
- if (!args.subscription_id) return { error: "subscription_id is required" };
231
+ if (!args.subscription_id) return validationError("subscription_id is required");
232
+ if (args.dry_run) return dryRunResult("POST", `/subscriptions/${args.subscription_id}/cancel`);
240
233
  const subscription = await client.cancelSubscription(args.subscription_id);
241
234
  return { result: formatSubscription(subscription), data: subscription };
242
235
  }
243
236
 
244
237
  default:
245
- return { error: `Unknown action: ${args.action}` };
238
+ return validationError(`Unknown action: ${args.action}`);
246
239
  }
247
- } catch (err: any) {
248
- return { error: `Subscription action failed: ${err.message}` };
249
- }
240
+ });
250
241
  },
251
242
  };
252
243
  }
@@ -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>): DryRunResult {
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: { method, path, ...(body !== undefined ? { body } : {}), ...(query ? { query } : {}) },
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
 
@@ -1,14 +1,8 @@
1
1
  /**
2
2
  * botcord_topics — Topic lifecycle management within rooms.
3
3
  */
4
- import {
5
- getSingleAccountModeError,
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 createTopicsTool() {
14
8
  return {
@@ -50,27 +44,19 @@ export function createTopicsTool() {
50
44
  enum: ["open", "completed", "failed", "expired"],
51
45
  description: "Topic status — for list (filter) or update (transition)",
52
46
  },
47
+ dry_run: {
48
+ type: "boolean" as const,
49
+ description: "Preview the request without executing. Returns the API call that would be made.",
50
+ },
53
51
  },
54
52
  required: ["action", "room_id"],
55
53
  },
56
54
  execute: async (toolCallId: any, args: any, signal?: any, onUpdate?: any) => {
57
- const cfg = getAppConfig();
58
- if (!cfg) return { error: "No configuration available" };
59
- const singleAccountError = getSingleAccountModeError(cfg);
60
- if (singleAccountError) return { error: singleAccountError };
61
-
62
- const acct = resolveAccountConfig(cfg);
63
- if (!isAccountConfigured(acct)) {
64
- return { error: "BotCord is not configured." };
65
- }
66
-
67
- const client = new BotCordClient(acct);
68
- attachTokenPersistence(client, acct);
69
-
70
- try {
55
+ return withClient(async (client) => {
71
56
  switch (args.action) {
72
57
  case "create":
73
- if (!args.title) return { error: "title is required" };
58
+ if (!args.title) return validationError("title is required");
59
+ if (args.dry_run) return dryRunResult("POST", `/hub/rooms/${args.room_id}/topics`, { title: args.title, description: args.description, goal: args.goal });
74
60
  return await client.createTopic(args.room_id, {
75
61
  title: args.title,
76
62
  description: args.description,
@@ -78,14 +64,15 @@ export function createTopicsTool() {
78
64
  });
79
65
 
80
66
  case "list":
81
- return await client.listTopics(args.room_id, args.status);
67
+ return { topics: await client.listTopics(args.room_id, args.status) };
82
68
 
83
69
  case "get":
84
- if (!args.topic_id) return { error: "topic_id is required" };
70
+ if (!args.topic_id) return validationError("topic_id is required");
85
71
  return await client.getTopic(args.room_id, args.topic_id);
86
72
 
87
73
  case "update":
88
- if (!args.topic_id) return { error: "topic_id is required" };
74
+ if (!args.topic_id) return validationError("topic_id is required");
75
+ if (args.dry_run) return dryRunResult("PATCH", `/hub/rooms/${args.room_id}/topics/${args.topic_id}`, { title: args.title, status: args.status, goal: args.goal });
89
76
  return await client.updateTopic(args.room_id, args.topic_id, {
90
77
  title: args.title,
91
78
  description: args.description,
@@ -94,16 +81,15 @@ export function createTopicsTool() {
94
81
  });
95
82
 
96
83
  case "delete":
97
- if (!args.topic_id) return { error: "topic_id is required" };
84
+ if (!args.topic_id) return validationError("topic_id is required");
85
+ if (args.dry_run) return dryRunResult("DELETE", `/hub/rooms/${args.room_id}/topics/${args.topic_id}`);
98
86
  await client.deleteTopic(args.room_id, args.topic_id);
99
87
  return { ok: true, deleted: args.topic_id, room: args.room_id };
100
88
 
101
89
  default:
102
- return { error: `Unknown action: ${args.action}` };
90
+ return validationError(`Unknown action: ${args.action}`);
103
91
  }
104
- } catch (err: any) {
105
- return { error: `Topic action failed: ${err.message}` };
106
- }
92
+ });
107
93
  },
108
94
  };
109
95
  }