@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_contacts — Manage social relationships: contacts, requests, blocks.
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 createContactsTool() {
14
8
  return {
@@ -56,69 +50,66 @@ export function createContactsTool() {
56
50
  enum: ["pending", "accepted", "rejected"],
57
51
  description: "Filter by state — for received_requests, sent_requests",
58
52
  },
53
+ dry_run: {
54
+ type: "boolean" as const,
55
+ description: "Preview the request without executing. Returns the API call that would be made.",
56
+ },
59
57
  },
60
58
  required: ["action"],
61
59
  },
62
60
  execute: async (toolCallId: any, args: any, signal?: any, onUpdate?: any) => {
63
- const cfg = getAppConfig();
64
- if (!cfg) return { error: "No configuration available" };
65
- const singleAccountError = getSingleAccountModeError(cfg);
66
- if (singleAccountError) return { error: singleAccountError };
67
-
68
- const acct = resolveAccountConfig(cfg);
69
- if (!isAccountConfigured(acct)) {
70
- return { error: "BotCord is not configured." };
71
- }
72
-
73
- const client = new BotCordClient(acct);
74
- attachTokenPersistence(client, acct);
75
-
76
- try {
61
+ return withClient(async (client) => {
77
62
  switch (args.action) {
78
63
  case "list":
79
- return await client.listContacts();
64
+ return { contacts: await client.listContacts() };
80
65
 
81
66
  case "remove":
82
- if (!args.agent_id) return { error: "agent_id is required" };
67
+ if (!args.agent_id) return validationError("agent_id is required");
68
+ if (args.dry_run) return dryRunResult("DELETE", `/registry/agents/{self}/contacts/${args.agent_id}`);
83
69
  await client.removeContact(args.agent_id);
84
70
  return { ok: true, removed: args.agent_id };
85
71
 
86
72
  case "send_request":
87
- if (!args.agent_id) return { error: "agent_id is required" };
73
+ if (!args.agent_id) return validationError("agent_id is required");
74
+ if (args.dry_run) return dryRunResult("POST", "/hub/send", { to: args.agent_id, type: "contact_request", payload: args.message ? { text: args.message } : {} }, { note: "The actual body is a signed envelope (JCS + Ed25519), not the raw fields shown here." });
88
75
  await client.sendContactRequest(args.agent_id, args.message);
89
76
  return { ok: true, sent_to: args.agent_id };
90
77
 
91
78
  case "received_requests":
92
- return await client.listReceivedRequests(args.state);
79
+ return { requests: await client.listReceivedRequests(args.state) };
93
80
 
94
81
  case "sent_requests":
95
- return await client.listSentRequests(args.state);
82
+ return { requests: await client.listSentRequests(args.state) };
96
83
 
97
84
  case "accept_request":
98
- if (!args.request_id) return { error: "request_id is required" };
85
+ if (!args.request_id) return validationError("request_id is required");
86
+ if (args.dry_run) return dryRunResult("POST", `/registry/agents/{self}/contact-requests/${args.request_id}/accept`);
99
87
  await client.acceptRequest(args.request_id);
100
88
  return { ok: true, accepted: args.request_id };
101
89
 
102
90
  case "reject_request":
103
- if (!args.request_id) return { error: "request_id is required" };
91
+ if (!args.request_id) return validationError("request_id is required");
92
+ if (args.dry_run) return dryRunResult("POST", `/registry/agents/{self}/contact-requests/${args.request_id}/reject`);
104
93
  await client.rejectRequest(args.request_id);
105
94
  return { ok: true, rejected: args.request_id };
106
95
 
107
96
  case "block":
108
- if (!args.agent_id) return { error: "agent_id is required" };
97
+ if (!args.agent_id) return validationError("agent_id is required");
98
+ if (args.dry_run) return dryRunResult("POST", `/registry/agents/{self}/blocks`, { blocked_agent_id: args.agent_id });
109
99
  await client.blockAgent(args.agent_id);
110
100
  return { ok: true, blocked: args.agent_id };
111
101
 
112
102
  case "unblock":
113
- if (!args.agent_id) return { error: "agent_id is required" };
103
+ if (!args.agent_id) return validationError("agent_id is required");
104
+ if (args.dry_run) return dryRunResult("DELETE", `/registry/agents/{self}/blocks/${args.agent_id}`);
114
105
  await client.unblockAgent(args.agent_id);
115
106
  return { ok: true, unblocked: args.agent_id };
116
107
 
117
108
  case "list_blocks":
118
- return await client.listBlocks();
109
+ return { blocks: await client.listBlocks() };
119
110
 
120
111
  case "redeem_invite": {
121
- if (!args.invite_code) return { error: "invite_code is required" };
112
+ if (!args.invite_code) return validationError("invite_code is required");
122
113
  // Extract code from full URL if needed (e.g. .../invites/iv_xxx/redeem or /i/iv_xxx)
123
114
  const raw = args.invite_code as string;
124
115
  const match = raw.match(/\b(iv_[a-zA-Z0-9]+)/);
@@ -127,11 +118,9 @@ export function createContactsTool() {
127
118
  }
128
119
 
129
120
  default:
130
- return { error: `Unknown action: ${args.action}` };
121
+ return validationError(`Unknown action: ${args.action}`);
131
122
  }
132
- } catch (err: any) {
133
- return { error: `Contact action failed: ${err.message}` };
134
- }
123
+ });
135
124
  },
136
125
  };
137
126
  }
@@ -1,14 +1,8 @@
1
1
  /**
2
2
  * botcord_directory — Read-only queries: resolve agents, discover rooms, message history.
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 } from "./tool-result.js";
12
6
 
13
7
  export function createDirectoryTool() {
14
8
  return {
@@ -63,30 +57,17 @@ export function createDirectoryTool() {
63
57
  required: ["action"],
64
58
  },
65
59
  execute: async (toolCallId: any, args: any, signal?: any, onUpdate?: any) => {
66
- const cfg = getAppConfig();
67
- if (!cfg) return { error: "No configuration available" };
68
- const singleAccountError = getSingleAccountModeError(cfg);
69
- if (singleAccountError) return { error: singleAccountError };
70
-
71
- const acct = resolveAccountConfig(cfg);
72
- if (!isAccountConfigured(acct)) {
73
- return { error: "BotCord is not configured." };
74
- }
75
-
76
- const client = new BotCordClient(acct);
77
- attachTokenPersistence(client, acct);
78
-
79
- try {
60
+ return withClient(async (client) => {
80
61
  switch (args.action) {
81
62
  case "resolve":
82
- if (!args.agent_id) return { error: "agent_id is required" };
63
+ if (!args.agent_id) return validationError("agent_id is required");
83
64
  return await client.resolve(args.agent_id);
84
65
 
85
66
  case "discover_rooms":
86
- return await client.discoverRooms(args.room_name);
67
+ return await client.discoverPublicRooms(args.room_name);
87
68
 
88
69
  case "history":
89
- return await client.getHistory({
70
+ return { history: await client.getHistory({
90
71
  peer: args.peer,
91
72
  roomId: args.room_id,
92
73
  topic: args.topic,
@@ -94,14 +75,12 @@ export function createDirectoryTool() {
94
75
  before: args.before,
95
76
  after: args.after,
96
77
  limit: args.limit || 20,
97
- });
78
+ }) };
98
79
 
99
80
  default:
100
- return { error: `Unknown action: ${args.action}` };
81
+ return validationError(`Unknown action: ${args.action}`);
101
82
  }
102
- } catch (err: any) {
103
- return { error: `Directory action failed: ${err.message}` };
104
- }
83
+ });
105
84
  },
106
85
  };
107
86
  }
@@ -3,15 +3,9 @@
3
3
  */
4
4
  import { readFile } from "node:fs/promises";
5
5
  import { basename } from "node:path";
6
- import { lookup } from "node:dns";
7
- import {
8
- getSingleAccountModeError,
9
- resolveAccountConfig,
10
- isAccountConfigured,
11
- } from "../config.js";
12
- import { BotCordClient } from "../client.js";
13
- import { attachTokenPersistence } from "../credentials.js";
14
- import { getConfig as getAppConfig } from "../runtime.js";
6
+ import { withClient } from "./with-client.js";
7
+ import { validationError, dryRunResult } from "./tool-result.js";
8
+ import type { BotCordClient } from "../client.js";
15
9
  import type { MessageAttachment } from "../types.js";
16
10
 
17
11
  /** Extract clean filename from a URL, stripping query string and hash. */
@@ -127,23 +121,30 @@ export function createMessagingTool() {
127
121
  items: { type: "string" as const },
128
122
  description: "URLs of already-hosted files to attach to the message",
129
123
  },
124
+ dry_run: {
125
+ type: "boolean" as const,
126
+ description: "Preview the request without sending. Returns the API call that would be made.",
127
+ },
130
128
  },
131
129
  required: ["to", "text"],
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. Set hubUrl, agentId, keyId, and privateKey." };
132
+ if (args.dry_run) {
133
+ const msgType = args.type || "message";
134
+ const body: Record<string, unknown> = { to: args.to, text: args.text, type: msgType };
135
+ if (args.topic) body.topic = args.topic;
136
+ if (args.goal) body.goal = args.goal;
137
+ if (args.reply_to) body.reply_to = args.reply_to;
138
+ if (args.mentions) body.mentions = args.mentions;
139
+ if (args.file_paths) body.file_paths = args.file_paths;
140
+ if (args.file_urls) body.file_urls = args.file_urls;
141
+ const notes: string[] = [];
142
+ if (args.file_paths?.length) notes.push("Local files will be uploaded via POST /hub/upload before sending.");
143
+ notes.push("The actual body is a signed envelope (JCS + Ed25519), not the raw fields shown here.");
144
+ return dryRunResult("POST", "/hub/send", body, { note: notes.join(" ") });
142
145
  }
143
146
 
144
- try {
145
- const client = new BotCordClient(acct);
146
- attachTokenPersistence(client, acct);
147
+ return withClient(async (client) => {
147
148
  const msgType = args.type || "message";
148
149
 
149
150
  // Collect attachments from both file_paths (upload first) and file_urls
@@ -182,9 +183,7 @@ export function createMessagingTool() {
182
183
  attachments: finalAttachments,
183
184
  });
184
185
  return { ok: true, hub_msg_id: result.hub_msg_id, to: args.to, type: msgType, attachments: finalAttachments };
185
- } catch (err: any) {
186
- return { error: `Failed to send: ${err.message}` };
187
- }
186
+ });
188
187
  },
189
188
  };
190
189
  }
@@ -212,28 +211,14 @@ export function createUploadTool() {
212
211
  required: ["file_paths"],
213
212
  },
214
213
  execute: async (toolCallId: any, args: any, signal?: any, onUpdate?: any) => {
215
- const cfg = getAppConfig();
216
- if (!cfg) return { error: "No configuration available" };
217
- const singleAccountError = getSingleAccountModeError(cfg);
218
- if (singleAccountError) return { error: singleAccountError };
219
-
220
- const acct = resolveAccountConfig(cfg);
221
- if (!isAccountConfigured(acct)) {
222
- return { error: "BotCord is not configured. Set hubUrl, agentId, keyId, and privateKey." };
223
- }
224
-
225
214
  if (!args.file_paths || args.file_paths.length === 0) {
226
- return { error: "file_paths is required and must not be empty" };
215
+ return validationError("file_paths is required and must not be empty");
227
216
  }
228
217
 
229
- try {
230
- const client = new BotCordClient(acct);
231
- attachTokenPersistence(client, acct);
218
+ return withClient(async (client) => {
232
219
  const uploaded = await uploadLocalFiles(client, args.file_paths);
233
220
  return { ok: true, files: uploaded };
234
- } catch (err: any) {
235
- return { error: `Upload failed: ${err.message}` };
236
- }
221
+ });
237
222
  },
238
223
  };
239
224
  }
@@ -1,15 +1,9 @@
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";
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
  import { executeTransfer, isPeerContact, formatFollowUpDeliverySummary } from "./payment-transfer.js";
14
8
 
15
9
  function sanitizeBalance(summary: any): any {
@@ -223,9 +217,9 @@ export function createPaymentTool(opts?: { name?: string; description?: string }
223
217
  type: "string" as const,
224
218
  description: "Recipient agent ID (ag_...) — for transfer",
225
219
  },
226
- amount_minor: {
220
+ amount: {
227
221
  type: "string" as const,
228
- description: "Amount in minor units (1 COIN = 100 minor units). To transfer N coins, pass N × 100. Example: 10 COIN \"1000\" — for transfer, topup, withdraw",
222
+ description: "Amount in COIN (supports up to 2 decimals, e.g. \"10\" or \"9.50\") — for transfer, topup, withdraw",
229
223
  },
230
224
  memo: {
231
225
  type: "string" as const,
@@ -259,9 +253,9 @@ export function createPaymentTool(opts?: { name?: string; description?: string }
259
253
  type: "object" as const,
260
254
  description: "Withdrawal destination details — for withdraw",
261
255
  },
262
- fee_minor: {
256
+ fee: {
263
257
  type: "string" as const,
264
- description: "Optional withdrawal fee in minor units (1 COIN = 100 minor units) — for withdraw",
258
+ description: "Optional withdrawal fee in COIN (e.g. \"1\" or \"0.50\") — for withdraw",
265
259
  },
266
260
  withdrawal_id: {
267
261
  type: "string" as const,
@@ -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,19 +312,22 @@ 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_minor) return { error: "amount_minor is required" };
315
+ if (!args.to_agent_id) return validationError("to_agent_id is required");
316
+ if (!args.amount) return validationError("amount is required");
317
+ const transferMinor = parseCoinToMinor(args.amount);
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 });
332
320
 
333
321
  const isContact = await isPeerContact(client, args.to_agent_id);
334
322
  if (!isContact && args.confirmed !== true) {
335
323
  return {
336
- result: `\u26a0\ufe0f ${args.to_agent_id} is not in your contacts. This is a stranger transfer of ${formatCoinAmount(args.amount_minor)}. To proceed, call this tool again with confirmed: true. The transfer will create a chat room between you and the recipient.`,
324
+ result: `\u26a0\ufe0f ${args.to_agent_id} is not in your contacts. This is a stranger transfer of ${formatCoinAmount(transferMinor)}. To proceed, call this tool again with confirmed: true. The transfer will create a chat room between you and the recipient.`,
337
325
  };
338
326
  }
339
327
 
340
328
  const transfer = await executeTransfer(client, {
341
329
  to_agent_id: args.to_agent_id,
342
- amount_minor: args.amount_minor,
330
+ amount_minor: transferMinor,
343
331
  memo: args.memo,
344
332
  reference_type: args.reference_type,
345
333
  reference_id: args.reference_id,
@@ -353,9 +341,13 @@ export function createPaymentTool(opts?: { name?: string; description?: string }
353
341
  }
354
342
 
355
343
  case "topup": {
356
- if (!args.amount_minor) return { error: "amount_minor is required" };
344
+ if (!args.amount) return validationError("amount is required");
345
+ const topupMinor = parseCoinToMinor(args.amount);
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
+
357
349
  const topup = await client.createTopup({
358
- amount_minor: args.amount_minor,
350
+ amount_minor: topupMinor,
359
351
  channel: args.channel,
360
352
  metadata: args.metadata,
361
353
  idempotency_key: args.idempotency_key,
@@ -364,10 +356,19 @@ export function createPaymentTool(opts?: { name?: string; description?: string }
364
356
  }
365
357
 
366
358
  case "withdraw": {
367
- if (!args.amount_minor) return { error: "amount_minor is required" };
359
+ if (!args.amount) return validationError("amount is required");
360
+ const withdrawMinor = parseCoinToMinor(args.amount);
361
+ if (withdrawMinor === null) return validationError("amount must be a valid number (e.g. \"10\" or \"9.50\")");
362
+ let feeMinor: string | undefined;
363
+ if (args.fee) {
364
+ feeMinor = parseCoinToMinor(args.fee) ?? undefined;
365
+ if (feeMinor === undefined) return validationError("fee must be a valid number (e.g. \"1\" or \"0.50\")");
366
+ }
367
+ if (args.dry_run) return dryRunResult("POST", "/wallet/withdrawals", { amount_minor: withdrawMinor, destination_type: args.destination_type });
368
+
368
369
  const withdrawal = await client.createWithdrawal({
369
- amount_minor: args.amount_minor,
370
- fee_minor: args.fee_minor,
370
+ amount_minor: withdrawMinor,
371
+ fee_minor: feeMinor,
371
372
  destination_type: args.destination_type,
372
373
  destination: args.destination,
373
374
  idempotency_key: args.idempotency_key,
@@ -376,23 +377,23 @@ export function createPaymentTool(opts?: { name?: string; description?: string }
376
377
  }
377
378
 
378
379
  case "cancel_withdrawal": {
379
- 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
+
380
383
  const withdrawal = await client.cancelWithdrawal(args.withdrawal_id);
381
384
  return { result: formatWithdrawal(withdrawal), data: sanitizeWithdrawal(withdrawal) };
382
385
  }
383
386
 
384
387
  case "tx_status": {
385
- if (!args.tx_id) return { error: "tx_id is required" };
388
+ if (!args.tx_id) return validationError("tx_id is required");
386
389
  const tx = await client.getWalletTransaction(args.tx_id);
387
390
  return { result: formatTransaction(tx), data: sanitizeTransaction(tx) };
388
391
  }
389
392
 
390
393
  default:
391
- return { error: `Unknown action: ${args.action}` };
394
+ return validationError(`Unknown action: ${args.action}`);
392
395
  }
393
- } catch (err: any) {
394
- return { error: `Payment action failed: ${err.message}` };
395
- }
396
+ });
396
397
  },
397
398
  };
398
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,25 @@
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";
7
+
8
+ /** Normalize query input: accept a string or string[] from the LLM. */
9
+ function _normalizeQuery(raw: unknown): string | string[] | null {
10
+ if (Array.isArray(raw)) {
11
+ const parts = raw
12
+ .filter((v) => typeof v === "string")
13
+ .map((s: string) => s.trim())
14
+ .filter(Boolean);
15
+ if (parts.length === 0) return null;
16
+ return parts.length === 1 ? parts[0] : parts;
17
+ }
18
+ if (typeof raw === "string") {
19
+ const trimmed = raw.trim();
20
+ return trimmed || null;
21
+ }
22
+ return null;
23
+ }
13
24
 
14
25
  export function createRoomContextTool() {
15
26
  return {
@@ -44,8 +55,9 @@ export function createRoomContextTool() {
44
55
  "Room ID (rm_...) — required for room_summary, room_messages, room_search; optional filter for global_search",
45
56
  },
46
57
  query: {
47
- type: "string" as const,
48
- description: "Search query text — required for room_search and global_search",
58
+ description:
59
+ "Search query — required for room_search and global_search. " +
60
+ "Pass a single string or an array of strings for OR search (e.g., ['deploy', 'release']).",
49
61
  },
50
62
  topic_id: {
51
63
  type: "string" as const,
@@ -71,28 +83,15 @@ export function createRoomContextTool() {
71
83
  required: ["action"],
72
84
  },
73
85
  execute: async (toolCallId: any, args: any, signal?: any, onUpdate?: any) => {
74
- const cfg = getAppConfig();
75
- if (!cfg) return { error: "No configuration available" };
76
- const singleAccountError = getSingleAccountModeError(cfg);
77
- if (singleAccountError) return { error: singleAccountError };
78
-
79
- const acct = resolveAccountConfig(cfg);
80
- if (!isAccountConfigured(acct)) {
81
- return { error: "BotCord is not configured." };
82
- }
83
-
84
- const client = new BotCordClient(acct);
85
- attachTokenPersistence(client, acct);
86
-
87
- try {
86
+ return withClient(async (client) => {
88
87
  switch (args.action) {
89
88
  case "room_summary": {
90
- 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");
91
90
  return await client.roomSummary(args.room_id, args.limit);
92
91
  }
93
92
 
94
93
  case "room_messages": {
95
- 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");
96
95
  return await client.roomMessages(args.room_id, {
97
96
  limit: args.limit,
98
97
  before: args.before,
@@ -103,9 +102,10 @@ export function createRoomContextTool() {
103
102
  }
104
103
 
105
104
  case "room_search": {
106
- if (!args.room_id) return { error: "room_id is required for room_search" };
107
- if (!args.query) return { error: "query is required for room_search" };
108
- return await client.roomSearch(args.room_id, args.query, {
105
+ if (!args.room_id) return validationError("room_id is required for room_search");
106
+ const rsQuery = _normalizeQuery(args.query);
107
+ if (!rsQuery) return validationError("query is required for room_search");
108
+ return await client.roomSearch(args.room_id, rsQuery, {
109
109
  limit: args.limit,
110
110
  before: args.before,
111
111
  topicId: args.topic_id,
@@ -118,8 +118,9 @@ export function createRoomContextTool() {
118
118
  }
119
119
 
120
120
  case "global_search": {
121
- if (!args.query) return { error: "query is required for global_search" };
122
- return await client.globalSearch(args.query, {
121
+ const gsQuery = _normalizeQuery(args.query);
122
+ if (!gsQuery) return validationError("query is required for global_search");
123
+ return await client.globalSearch(gsQuery, {
123
124
  limit: args.limit,
124
125
  roomId: args.room_id,
125
126
  topicId: args.topic_id,
@@ -129,11 +130,9 @@ export function createRoomContextTool() {
129
130
  }
130
131
 
131
132
  default:
132
- return { error: `Unknown action: ${args.action}` };
133
+ return validationError(`Unknown action: ${args.action}`);
133
134
  }
134
- } catch (err: any) {
135
- return { error: `Room context action failed: ${err.message}` };
136
- }
135
+ });
137
136
  },
138
137
  };
139
138
  }