@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.
@@ -1,14 +1,8 @@
1
1
  /**
2
2
  * botcord_payment — Unified payment and transaction tool for BotCord coin flows.
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
  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
- const cfg = getAppConfig();
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 { error: "agent_id is required" };
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 { error: "to_agent_id is required" };
331
- if (!args.amount) return { error: "amount is required" };
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 { error: "amount must be a valid number (e.g. \"10\" or \"9.50\")" };
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 { error: "amount is required" };
344
+ if (!args.amount) return validationError("amount is required");
359
345
  const topupMinor = parseCoinToMinor(args.amount);
360
- if (topupMinor === null) return { error: "amount must be a valid number (e.g. \"10\" or \"9.50\")" };
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 { error: "amount is required" };
359
+ if (!args.amount) return validationError("amount is required");
372
360
  const withdrawMinor = parseCoinToMinor(args.amount);
373
- if (withdrawMinor === null) return { error: "amount must be a valid number (e.g. \"10\" or \"9.50\")" };
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 { error: "fee must be a valid number (e.g. \"1\" or \"0.50\")" };
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 { error: "withdrawal_id is required" };
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 { error: "tx_id is required" };
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 { error: `Unknown action: ${args.action}` };
394
+ return validationError(`Unknown action: ${args.action}`);
403
395
  }
404
- } catch (err: any) {
405
- return { error: `Payment action failed: ${err.message}` };
406
- }
396
+ });
407
397
  },
408
398
  };
409
399
  }
@@ -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 { error: "name is required" };
39
+ return validationError("name is required");
39
40
  }
40
41
 
41
42
  const cfg = getAppConfig();
42
- if (!cfg) return { error: "No configuration available" };
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: any) {
63
- return { error: `Registration failed: ${err.message}` };
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 { error: "No configuration available" };
34
- if (!args.agent_id) return { error: "agent_id is required" };
35
- if (!args.reset_code) return { error: "reset_code is required" };
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: any) {
54
- return { error: err.message };
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
- getSingleAccountModeError,
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
- const cfg = getAppConfig();
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 { error: "room_id is required for room_summary" };
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 { error: "room_id is required for room_messages" };
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 { error: "room_id is required for room_search" };
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 { error: "query is required for room_search" };
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 { error: "query is required for global_search" };
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 { error: `Unknown action: ${args.action}` };
133
+ return validationError(`Unknown action: ${args.action}`);
153
134
  }
154
- } catch (err: any) {
155
- return { error: `Room context action failed: ${err.message}` };
156
- }
135
+ });
157
136
  },
158
137
  };
159
138
  }
@@ -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,
@@ -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 { 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,14 +1,8 @@
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";
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
- 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) return { error: "amount 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");
153
138
  const amountMinor = parseCoinToMinor(args.amount);
154
- if (amountMinor === null) return { error: "amount must be a valid number (e.g. \"10\" or \"9.50\")" };
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 { 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`);
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 { error: "product_id is required" };
183
- 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 });
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 { error: "room_id is required" };
204
- 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 });
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 { 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`);
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 { error: "product_id is required" };
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 { 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`);
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 { error: `Unknown action: ${args.action}` };
238
+ return validationError(`Unknown action: ${args.action}`);
248
239
  }
249
- } catch (err: any) {
250
- return { error: `Subscription action failed: ${err.message}` };
251
- }
240
+ });
252
241
  },
253
242
  };
254
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