@botcord/botcord 0.1.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,55 @@
1
+ /**
2
+ * botcord_notify — Agent tool for sending notifications to the owner's
3
+ * configured channel (e.g. Telegram). The agent decides when a message
4
+ * is important enough to warrant notifying the owner.
5
+ */
6
+ import { getBotCordRuntime } from "../runtime.js";
7
+ import { getConfig as getAppConfig } from "../runtime.js";
8
+ import { getSingleAccountModeError, resolveAccountConfig } from "../config.js";
9
+ import { deliverNotification } from "../inbound.js";
10
+
11
+ export function createNotifyTool() {
12
+ return {
13
+ name: "botcord_notify",
14
+ description:
15
+ "Send a notification to the owner's configured channel (e.g. Telegram, Discord). " +
16
+ "Use this when you receive an important BotCord message that the owner should know about — " +
17
+ "for example, a meaningful conversation update, an urgent request, or something requiring human attention. " +
18
+ "Do NOT use for routine or low-value messages.",
19
+ parameters: {
20
+ type: "object" as const,
21
+ properties: {
22
+ text: {
23
+ type: "string" as const,
24
+ description: "Notification text to send to the owner",
25
+ },
26
+ },
27
+ required: ["text"],
28
+ },
29
+ execute: async (toolCallId: any, args: any) => {
30
+ const cfg = getAppConfig();
31
+ if (!cfg) return { error: "No configuration available" };
32
+ const singleAccountError = getSingleAccountModeError(cfg);
33
+ if (singleAccountError) return { error: singleAccountError };
34
+
35
+ const acct = resolveAccountConfig(cfg);
36
+ const notifySession = acct.notifySession;
37
+ if (!notifySession) {
38
+ return { error: "notifySession is not configured in channels.botcord" };
39
+ }
40
+
41
+ const core = getBotCordRuntime();
42
+ const text = typeof args.text === "string" ? args.text.trim() : "";
43
+ if (!text) {
44
+ return { error: "text is required" };
45
+ }
46
+
47
+ try {
48
+ await deliverNotification(core, cfg, notifySession, text);
49
+ return { ok: true, notifySession };
50
+ } catch (err: any) {
51
+ return { error: `notify failed: ${err?.message ?? err}` };
52
+ }
53
+ },
54
+ };
55
+ }
@@ -0,0 +1,153 @@
1
+ import type { BotCordClient } from "../client.js";
2
+ import type { WalletTransaction } from "../types.js";
3
+ import { formatCoinAmount } from "./coin-format.js";
4
+
5
+ type FollowUpDeliveryResult = {
6
+ attempted: true;
7
+ sent: boolean;
8
+ hub_msg_id?: string;
9
+ error?: string;
10
+ };
11
+
12
+ export type ContactOnlyTransferResult = {
13
+ tx: WalletTransaction;
14
+ transfer_record_message: FollowUpDeliveryResult;
15
+ notifications: {
16
+ payer: FollowUpDeliveryResult;
17
+ payee: FollowUpDeliveryResult;
18
+ };
19
+ };
20
+
21
+ function extractTransferMetadata(tx: WalletTransaction): Record<string, unknown> | null {
22
+ if (!tx.metadata_json) return null;
23
+ try {
24
+ return typeof tx.metadata_json === "string"
25
+ ? JSON.parse(tx.metadata_json)
26
+ : tx.metadata_json;
27
+ } catch {
28
+ return null;
29
+ }
30
+ }
31
+
32
+ function formatOptionalLine(label: string, value: string | null | undefined): string | null {
33
+ return value ? `${label}: ${value}` : null;
34
+ }
35
+
36
+ export async function assertTransferPeerIsContact(client: BotCordClient, toAgentId: string): Promise<void> {
37
+ const contacts = await client.listContacts();
38
+ const isContact = contacts.some((contact) => contact.contact_agent_id === toAgentId);
39
+ if (!isContact) {
40
+ throw new Error("Transfer is only allowed between contacts. Please add this agent as a contact first.");
41
+ }
42
+ }
43
+
44
+ export function buildTransferRecordMessage(tx: WalletTransaction): string {
45
+ const metadata = extractTransferMetadata(tx);
46
+ return [
47
+ "[BotCord Transfer]",
48
+ `Status: ${tx.status}`,
49
+ `Transaction: ${tx.tx_id}`,
50
+ `Amount: ${formatCoinAmount(tx.amount_minor)}`,
51
+ `Asset: ${tx.asset_code}`,
52
+ formatOptionalLine("From", tx.from_agent_id),
53
+ formatOptionalLine("To", tx.to_agent_id),
54
+ formatOptionalLine("Memo", typeof metadata?.memo === "string" ? metadata.memo : undefined),
55
+ formatOptionalLine("Reference type", tx.reference_type),
56
+ formatOptionalLine("Reference id", tx.reference_id),
57
+ `Created: ${tx.created_at}`,
58
+ ].filter(Boolean).join("\n");
59
+ }
60
+
61
+ export function buildTransferNotificationMessage(
62
+ tx: WalletTransaction,
63
+ role: "payer" | "payee",
64
+ ): string {
65
+ if (role === "payer") {
66
+ return `[BotCord Notice] Transfer sent: ${formatCoinAmount(tx.amount_minor)} to ${tx.to_agent_id} (tx: ${tx.tx_id})`;
67
+ }
68
+ return `[BotCord Notice] Payment received: ${formatCoinAmount(tx.amount_minor)} from ${tx.from_agent_id} (tx: ${tx.tx_id})`;
69
+ }
70
+
71
+ export function formatFollowUpDeliverySummary(result: ContactOnlyTransferResult): string {
72
+ const lines = [
73
+ `Transfer record message: ${result.transfer_record_message.sent ? "sent" : "failed"}`,
74
+ `Payer notification: ${result.notifications.payer.sent ? "sent" : "failed"}`,
75
+ `Payee notification: ${result.notifications.payee.sent ? "sent" : "failed"}`,
76
+ ];
77
+ const failures = [
78
+ result.transfer_record_message.error,
79
+ result.notifications.payer.error,
80
+ result.notifications.payee.error,
81
+ ].filter(Boolean);
82
+ if (failures.length > 0) {
83
+ lines.push("Warning: some follow-up messages failed to send.");
84
+ }
85
+ return lines.join("\n");
86
+ }
87
+
88
+ async function sendRecordMessage(
89
+ client: BotCordClient,
90
+ tx: WalletTransaction,
91
+ ): Promise<FollowUpDeliveryResult> {
92
+ try {
93
+ const response = await client.sendMessage(tx.to_agent_id || "", buildTransferRecordMessage(tx));
94
+ return { attempted: true, sent: true, hub_msg_id: response.hub_msg_id };
95
+ } catch (err: any) {
96
+ return { attempted: true, sent: false, error: err?.message ?? String(err) };
97
+ }
98
+ }
99
+
100
+ async function sendNotification(
101
+ client: BotCordClient,
102
+ to: string,
103
+ tx: WalletTransaction,
104
+ role: "payer" | "payee",
105
+ ): Promise<FollowUpDeliveryResult> {
106
+ try {
107
+ const response = await client.sendSystemMessage(to, buildTransferNotificationMessage(tx, role), {
108
+ event: "wallet_transfer_notice",
109
+ role,
110
+ tx_id: tx.tx_id,
111
+ amount_minor: tx.amount_minor,
112
+ asset_code: tx.asset_code,
113
+ from_agent_id: tx.from_agent_id,
114
+ to_agent_id: tx.to_agent_id,
115
+ reference_type: tx.reference_type,
116
+ reference_id: tx.reference_id,
117
+ });
118
+ return { attempted: true, sent: true, hub_msg_id: response.hub_msg_id };
119
+ } catch (err: any) {
120
+ return { attempted: true, sent: false, error: err?.message ?? String(err) };
121
+ }
122
+ }
123
+
124
+ export async function executeContactOnlyTransfer(
125
+ client: BotCordClient,
126
+ params: {
127
+ to_agent_id: string;
128
+ amount_minor: string;
129
+ memo?: string;
130
+ reference_type?: string;
131
+ reference_id?: string;
132
+ metadata?: Record<string, unknown>;
133
+ idempotency_key?: string;
134
+ },
135
+ ): Promise<ContactOnlyTransferResult> {
136
+ await assertTransferPeerIsContact(client, params.to_agent_id);
137
+
138
+ const tx = await client.createTransfer(params);
139
+ const [recordMessage, payerNotification, payeeNotification] = await Promise.all([
140
+ sendRecordMessage(client, tx),
141
+ sendNotification(client, client.getAgentId(), tx, "payer"),
142
+ sendNotification(client, params.to_agent_id, tx, "payee"),
143
+ ]);
144
+
145
+ return {
146
+ tx,
147
+ transfer_record_message: recordMessage,
148
+ notifications: {
149
+ payer: payerNotification,
150
+ payee: payeeNotification,
151
+ },
152
+ };
153
+ }
@@ -0,0 +1,384 @@
1
+ /**
2
+ * botcord_payment — Unified payment and transaction tool for BotCord coin flows.
3
+ */
4
+ import {
5
+ getSingleAccountModeError,
6
+ resolveAccountConfig,
7
+ isAccountConfigured,
8
+ } from "../config.js";
9
+ import { BotCordClient } from "../client.js";
10
+ import { getConfig as getAppConfig } from "../runtime.js";
11
+ import { formatCoinAmount } from "./coin-format.js";
12
+ import { executeContactOnlyTransfer, formatFollowUpDeliverySummary } from "./payment-transfer.js";
13
+
14
+ function sanitizeBalance(summary: any): any {
15
+ return {
16
+ agent_id: summary.agent_id,
17
+ asset_code: summary.asset_code,
18
+ available_balance: formatCoinAmount(summary.available_balance_minor),
19
+ locked_balance: formatCoinAmount(summary.locked_balance_minor),
20
+ total_balance: formatCoinAmount(summary.total_balance_minor),
21
+ updated_at: summary.updated_at,
22
+ };
23
+ }
24
+
25
+ function formatBalance(summary: any): string {
26
+ const available = summary.available_balance_minor ?? "0";
27
+ const locked = summary.locked_balance_minor ?? "0";
28
+ const total = summary.total_balance_minor ?? "0";
29
+ return [
30
+ `Asset: ${summary.asset_code}`,
31
+ `Available: ${formatCoinAmount(available)}`,
32
+ `Locked: ${formatCoinAmount(locked)}`,
33
+ `Total: ${formatCoinAmount(total)}`,
34
+ `Updated: ${summary.updated_at}`,
35
+ ].join("\n");
36
+ }
37
+
38
+ function formatRecipient(agent: any): string {
39
+ return [
40
+ `Agent: ${agent.agent_id}`,
41
+ `Name: ${agent.display_name || "(none)"}`,
42
+ `Policy: ${agent.message_policy || "(unknown)"}`,
43
+ `Endpoints: ${Array.isArray(agent.endpoints) ? agent.endpoints.length : 0}`,
44
+ ].join("\n");
45
+ }
46
+
47
+ function extractMetadata(tx: any): Record<string, unknown> | null {
48
+ if (!tx?.metadata_json) return null;
49
+ try {
50
+ return typeof tx.metadata_json === "string"
51
+ ? JSON.parse(tx.metadata_json)
52
+ : tx.metadata_json;
53
+ } catch {
54
+ return null;
55
+ }
56
+ }
57
+
58
+ function sanitizeTransaction(tx: any): any {
59
+ const metadata = extractMetadata(tx);
60
+ return {
61
+ tx_id: tx.tx_id,
62
+ type: tx.type,
63
+ status: tx.status,
64
+ asset_code: tx.asset_code,
65
+ amount: formatCoinAmount(tx.amount_minor),
66
+ fee: formatCoinAmount(tx.fee_minor),
67
+ from_agent_id: tx.from_agent_id,
68
+ to_agent_id: tx.to_agent_id,
69
+ reference_type: tx.reference_type,
70
+ reference_id: tx.reference_id,
71
+ idempotency_key: tx.idempotency_key,
72
+ metadata: metadata ?? undefined,
73
+ created_at: tx.created_at,
74
+ updated_at: tx.updated_at,
75
+ completed_at: tx.completed_at,
76
+ };
77
+ }
78
+
79
+ function sanitizeTransferResult(transfer: any): any {
80
+ return {
81
+ tx: sanitizeTransaction(transfer.tx),
82
+ transfer_record_message: transfer.transfer_record_message,
83
+ notifications: transfer.notifications,
84
+ };
85
+ }
86
+
87
+ function formatTransaction(tx: any): string {
88
+ const lines = [
89
+ `Transaction: ${tx.tx_id}`,
90
+ `Type: ${tx.type}`,
91
+ `Status: ${tx.status}`,
92
+ `Amount: ${formatCoinAmount(tx.amount_minor)}`,
93
+ `Fee: ${formatCoinAmount(tx.fee_minor)}`,
94
+ ];
95
+ if (tx.from_agent_id) lines.push(`From: ${tx.from_agent_id}`);
96
+ if (tx.to_agent_id) lines.push(`To: ${tx.to_agent_id}`);
97
+ if (tx.reference_type) lines.push(`Reference type: ${tx.reference_type}`);
98
+ if (tx.reference_id) lines.push(`Reference id: ${tx.reference_id}`);
99
+ const metadata = extractMetadata(tx);
100
+ if (metadata?.memo) lines.push(`Memo: ${String(metadata.memo)}`);
101
+ if (tx.idempotency_key) lines.push(`Idempotency: ${tx.idempotency_key}`);
102
+ lines.push(`Created: ${tx.created_at}`);
103
+ if (tx.completed_at) lines.push(`Completed: ${tx.completed_at}`);
104
+ return lines.join("\n");
105
+ }
106
+
107
+ function formatTopup(topup: any): string {
108
+ return [
109
+ `Topup: ${topup.topup_id}`,
110
+ `Status: ${topup.status}`,
111
+ `Amount: ${formatCoinAmount(topup.amount_minor)}`,
112
+ `Channel: ${topup.channel}`,
113
+ `Created: ${topup.created_at}`,
114
+ topup.completed_at ? `Completed: ${topup.completed_at}` : null,
115
+ ].filter(Boolean).join("\n");
116
+ }
117
+
118
+ function sanitizeTopup(topup: any): any {
119
+ return {
120
+ topup_id: topup.topup_id,
121
+ status: topup.status,
122
+ asset_code: topup.asset_code,
123
+ amount: formatCoinAmount(topup.amount_minor),
124
+ channel: topup.channel,
125
+ idempotency_key: topup.idempotency_key,
126
+ created_at: topup.created_at,
127
+ updated_at: topup.updated_at,
128
+ completed_at: topup.completed_at,
129
+ };
130
+ }
131
+
132
+ function formatWithdrawal(withdrawal: any): string {
133
+ return [
134
+ `Withdrawal: ${withdrawal.withdrawal_id}`,
135
+ `Status: ${withdrawal.status}`,
136
+ `Amount: ${formatCoinAmount(withdrawal.amount_minor)}`,
137
+ `Fee: ${formatCoinAmount(withdrawal.fee_minor)}`,
138
+ withdrawal.destination_type ? `Destination type: ${withdrawal.destination_type}` : null,
139
+ `Created: ${withdrawal.created_at}`,
140
+ withdrawal.reviewed_at ? `Reviewed: ${withdrawal.reviewed_at}` : null,
141
+ withdrawal.completed_at ? `Completed: ${withdrawal.completed_at}` : null,
142
+ ].filter(Boolean).join("\n");
143
+ }
144
+
145
+ function sanitizeWithdrawal(withdrawal: any): any {
146
+ return {
147
+ withdrawal_id: withdrawal.withdrawal_id,
148
+ status: withdrawal.status,
149
+ asset_code: withdrawal.asset_code,
150
+ amount: formatCoinAmount(withdrawal.amount_minor),
151
+ fee: formatCoinAmount(withdrawal.fee_minor),
152
+ destination_type: withdrawal.destination_type,
153
+ destination: withdrawal.destination,
154
+ idempotency_key: withdrawal.idempotency_key,
155
+ created_at: withdrawal.created_at,
156
+ updated_at: withdrawal.updated_at,
157
+ reviewed_at: withdrawal.reviewed_at,
158
+ completed_at: withdrawal.completed_at,
159
+ };
160
+ }
161
+
162
+ function formatLedger(data: any): string {
163
+ const entries = data.entries ?? [];
164
+ if (entries.length === 0) return "No payment ledger entries found.";
165
+
166
+ const lines = entries.map((e: any) => {
167
+ const dir = e.direction === "credit" ? "+" : "-";
168
+ return `${e.created_at} | ${dir}${formatCoinAmount(e.amount_minor)} | bal=${formatCoinAmount(e.balance_after_minor)} | tx=${e.tx_id}`;
169
+ });
170
+
171
+ if (data.has_more) {
172
+ lines.push(`\n(More entries available — use cursor: "${data.next_cursor}")`);
173
+ }
174
+ return lines.join("\n");
175
+ }
176
+
177
+ function sanitizeLedger(data: any): any {
178
+ const entries = (data.entries ?? []).map((entry: any) => ({
179
+ entry_id: entry.entry_id,
180
+ tx_id: entry.tx_id,
181
+ direction: entry.direction,
182
+ amount: formatCoinAmount(entry.amount_minor),
183
+ balance_after: formatCoinAmount(entry.balance_after_minor),
184
+ created_at: entry.created_at,
185
+ }));
186
+
187
+ return {
188
+ entries,
189
+ next_cursor: data.next_cursor,
190
+ has_more: data.has_more,
191
+ };
192
+ }
193
+
194
+ export function createPaymentTool(opts?: { name?: string; description?: string }) {
195
+ return {
196
+ name: opts?.name || "botcord_payment",
197
+ description:
198
+ opts?.description ||
199
+ "Manage BotCord coin payments and transactions: verify recipients, check balance, view ledger, transfer coins, create topups and withdrawals, cancel withdrawals, and query transaction status.",
200
+ parameters: {
201
+ type: "object" as const,
202
+ properties: {
203
+ action: {
204
+ type: "string" as const,
205
+ enum: [
206
+ "recipient_verify",
207
+ "balance",
208
+ "ledger",
209
+ "transfer",
210
+ "topup",
211
+ "withdraw",
212
+ "cancel_withdrawal",
213
+ "tx_status",
214
+ ],
215
+ description: "Payment action to perform",
216
+ },
217
+ agent_id: {
218
+ type: "string" as const,
219
+ description: "Agent ID (ag_...) — for recipient_verify",
220
+ },
221
+ to_agent_id: {
222
+ type: "string" as const,
223
+ description: "Recipient agent ID (ag_...) — for transfer",
224
+ },
225
+ amount_minor: {
226
+ type: "string" as const,
227
+ description: "Amount in minor units (string) — for transfer, topup, withdraw",
228
+ },
229
+ memo: {
230
+ type: "string" as const,
231
+ description: "Optional payment memo — for transfer",
232
+ },
233
+ reference_type: {
234
+ type: "string" as const,
235
+ description: "Optional business reference type — for transfer",
236
+ },
237
+ reference_id: {
238
+ type: "string" as const,
239
+ description: "Optional business reference ID — for transfer",
240
+ },
241
+ metadata: {
242
+ type: "object" as const,
243
+ description: "Optional metadata object — for transfer or topup",
244
+ },
245
+ idempotency_key: {
246
+ type: "string" as const,
247
+ description: "Optional idempotency key — for transfer, topup, withdraw",
248
+ },
249
+ channel: {
250
+ type: "string" as const,
251
+ description: "Topup channel (e.g. 'mock') — for topup",
252
+ },
253
+ destination_type: {
254
+ type: "string" as const,
255
+ description: "Withdrawal destination type — for withdraw",
256
+ },
257
+ destination: {
258
+ type: "object" as const,
259
+ description: "Withdrawal destination details — for withdraw",
260
+ },
261
+ fee_minor: {
262
+ type: "string" as const,
263
+ description: "Optional withdrawal fee in minor units — for withdraw",
264
+ },
265
+ withdrawal_id: {
266
+ type: "string" as const,
267
+ description: "Withdrawal ID — for cancel_withdrawal",
268
+ },
269
+ tx_id: {
270
+ type: "string" as const,
271
+ description: "Transaction ID — for tx_status",
272
+ },
273
+ cursor: {
274
+ type: "string" as const,
275
+ description: "Pagination cursor — for ledger",
276
+ },
277
+ limit: {
278
+ type: "number" as const,
279
+ description: "Max entries to return — for ledger",
280
+ },
281
+ type: {
282
+ type: "string" as const,
283
+ description: "Filter by transaction type — for ledger",
284
+ },
285
+ },
286
+ required: ["action"],
287
+ },
288
+ execute: async (_toolCallId: any, args: any) => {
289
+ const cfg = getAppConfig();
290
+ if (!cfg) return { error: "No configuration available" };
291
+ const singleAccountError = getSingleAccountModeError(cfg);
292
+ if (singleAccountError) return { error: singleAccountError };
293
+
294
+ const acct = resolveAccountConfig(cfg);
295
+ if (!isAccountConfigured(acct)) {
296
+ return { error: "BotCord is not configured." };
297
+ }
298
+
299
+ const client = new BotCordClient(acct);
300
+
301
+ try {
302
+ switch (args.action) {
303
+ case "recipient_verify": {
304
+ if (!args.agent_id) return { error: "agent_id is required" };
305
+ const agent = await client.resolve(args.agent_id);
306
+ return { result: formatRecipient(agent), data: agent };
307
+ }
308
+
309
+ case "balance": {
310
+ const summary = await client.getWallet();
311
+ return { result: formatBalance(summary), data: sanitizeBalance(summary) };
312
+ }
313
+
314
+ case "ledger": {
315
+ const opts: { cursor?: string; limit?: number; type?: string } = {};
316
+ if (args.cursor) opts.cursor = args.cursor;
317
+ if (args.limit) opts.limit = args.limit;
318
+ if (args.type) opts.type = args.type;
319
+ const ledger = await client.getWalletLedger(opts);
320
+ return { result: formatLedger(ledger), data: sanitizeLedger(ledger) };
321
+ }
322
+
323
+ case "transfer": {
324
+ if (!args.to_agent_id) return { error: "to_agent_id is required" };
325
+ if (!args.amount_minor) return { error: "amount_minor is required" };
326
+ const transfer = await executeContactOnlyTransfer(client, {
327
+ to_agent_id: args.to_agent_id,
328
+ amount_minor: args.amount_minor,
329
+ memo: args.memo,
330
+ reference_type: args.reference_type,
331
+ reference_id: args.reference_id,
332
+ metadata: args.metadata,
333
+ idempotency_key: args.idempotency_key,
334
+ });
335
+ return {
336
+ result: `${formatTransaction(transfer.tx)}\n${formatFollowUpDeliverySummary(transfer)}`,
337
+ data: sanitizeTransferResult(transfer),
338
+ };
339
+ }
340
+
341
+ case "topup": {
342
+ if (!args.amount_minor) return { error: "amount_minor is required" };
343
+ const topup = await client.createTopup({
344
+ amount_minor: args.amount_minor,
345
+ channel: args.channel,
346
+ metadata: args.metadata,
347
+ idempotency_key: args.idempotency_key,
348
+ });
349
+ return { result: formatTopup(topup), data: sanitizeTopup(topup) };
350
+ }
351
+
352
+ case "withdraw": {
353
+ if (!args.amount_minor) return { error: "amount_minor is required" };
354
+ const withdrawal = await client.createWithdrawal({
355
+ amount_minor: args.amount_minor,
356
+ fee_minor: args.fee_minor,
357
+ destination_type: args.destination_type,
358
+ destination: args.destination,
359
+ idempotency_key: args.idempotency_key,
360
+ });
361
+ return { result: formatWithdrawal(withdrawal), data: sanitizeWithdrawal(withdrawal) };
362
+ }
363
+
364
+ case "cancel_withdrawal": {
365
+ if (!args.withdrawal_id) return { error: "withdrawal_id is required" };
366
+ const withdrawal = await client.cancelWithdrawal(args.withdrawal_id);
367
+ return { result: formatWithdrawal(withdrawal), data: sanitizeWithdrawal(withdrawal) };
368
+ }
369
+
370
+ case "tx_status": {
371
+ if (!args.tx_id) return { error: "tx_id is required" };
372
+ const tx = await client.getWalletTransaction(args.tx_id);
373
+ return { result: formatTransaction(tx), data: sanitizeTransaction(tx) };
374
+ }
375
+
376
+ default:
377
+ return { error: `Unknown action: ${args.action}` };
378
+ }
379
+ } catch (err: any) {
380
+ return { error: `Payment action failed: ${err.message}` };
381
+ }
382
+ },
383
+ };
384
+ }