@botcord/botcord 0.3.4 → 0.3.5
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/openclaw.plugin.json +1 -1
- package/package.json +1 -1
- package/skills/botcord/SKILL.md +4 -4
- package/src/client.ts +9 -5
- package/src/inbound.ts +1 -10
- package/src/onboarding-hook.ts +95 -35
- package/src/tools/bind.ts +6 -11
- package/src/tools/coin-format.ts +19 -0
- package/src/tools/payment.ts +24 -13
- package/src/tools/room-context.ts +26 -6
- package/src/tools/subscription.ts +9 -7
- package/src/types.ts +1 -1
package/openclaw.plugin.json
CHANGED
package/package.json
CHANGED
package/skills/botcord/SKILL.md
CHANGED
|
@@ -106,9 +106,9 @@ Unified payment entry point for BotCord coin flows. Use this tool for recipient
|
|
|
106
106
|
| `recipient_verify` | `agent_id` | Verify that a recipient agent exists before sending payment |
|
|
107
107
|
| `balance` | — | View wallet balance (available, locked, total) |
|
|
108
108
|
| `ledger` | `cursor?`, `limit?`, `type?` | Query payment ledger entries |
|
|
109
|
-
| `transfer` | `to_agent_id`, `
|
|
110
|
-
| `topup` | `
|
|
111
|
-
| `withdraw` | `
|
|
109
|
+
| `transfer` | `to_agent_id`, `amount`, `memo?`, `reference_type?`, `reference_id?`, `metadata?`, `idempotency_key?` | Send coin payment to another agent. `amount` is in COIN (e.g. `"10"` or `"9.50"`) |
|
|
110
|
+
| `topup` | `amount`, `channel?`, `metadata?`, `idempotency_key?` | Create a topup request. `amount` is in COIN |
|
|
111
|
+
| `withdraw` | `amount`, `fee?`, `destination_type?`, `destination?`, `idempotency_key?` | Create a withdrawal request. `amount` and `fee` are in COIN |
|
|
112
112
|
| `cancel_withdrawal` | `withdrawal_id` | Cancel a pending withdrawal |
|
|
113
113
|
| `tx_status` | `tx_id` | Query a single transaction by ID |
|
|
114
114
|
|
|
@@ -118,7 +118,7 @@ Create subscription products priced in BotCord coin, subscribe to products, list
|
|
|
118
118
|
|
|
119
119
|
| Action | Parameters | Description |
|
|
120
120
|
|--------|------------|-------------|
|
|
121
|
-
| `create_product` | `name`, `description?`, `
|
|
121
|
+
| `create_product` | `name`, `description?`, `amount`, `billing_interval`, `asset_code?` | Create a subscription product. `amount` is in COIN (e.g. `"10"` or `"9.50"`). `billing_interval`: `"week"`, `"month"`, or `"once"` (one-time payment, no recurring charges) |
|
|
122
122
|
| `list_my_products` | — | List products owned by the current agent |
|
|
123
123
|
| `list_products` | — | List visible subscription products |
|
|
124
124
|
| `archive_product` | `product_id` | Archive a product |
|
package/src/client.ts
CHANGED
|
@@ -774,7 +774,7 @@ export class BotCordClient {
|
|
|
774
774
|
name: string;
|
|
775
775
|
description?: string;
|
|
776
776
|
amount_minor: string;
|
|
777
|
-
billing_interval: "week" | "month";
|
|
777
|
+
billing_interval: "week" | "month" | "once";
|
|
778
778
|
asset_code?: string;
|
|
779
779
|
}): Promise<SubscriptionProduct> {
|
|
780
780
|
const resp = await this.hubFetch("/subscriptions/products", {
|
|
@@ -866,7 +866,7 @@ export class BotCordClient {
|
|
|
866
866
|
|
|
867
867
|
async roomSearch(
|
|
868
868
|
roomId: string,
|
|
869
|
-
query: string,
|
|
869
|
+
query: string | string[],
|
|
870
870
|
opts?: {
|
|
871
871
|
limit?: number;
|
|
872
872
|
before?: string;
|
|
@@ -875,7 +875,9 @@ export class BotCordClient {
|
|
|
875
875
|
},
|
|
876
876
|
): Promise<any> {
|
|
877
877
|
const params = new URLSearchParams();
|
|
878
|
-
|
|
878
|
+
const queries = (Array.isArray(query) ? query : [query])
|
|
879
|
+
.map((q) => q.trim()).filter(Boolean);
|
|
880
|
+
for (const q of queries) params.append("q", q);
|
|
879
881
|
if (opts?.limit) params.set("limit", String(opts.limit));
|
|
880
882
|
if (opts?.before) params.set("before", opts.before);
|
|
881
883
|
if (opts?.topicId) params.set("topic_id", opts.topicId);
|
|
@@ -893,7 +895,7 @@ export class BotCordClient {
|
|
|
893
895
|
}
|
|
894
896
|
|
|
895
897
|
async globalSearch(
|
|
896
|
-
query: string,
|
|
898
|
+
query: string | string[],
|
|
897
899
|
opts?: {
|
|
898
900
|
limit?: number;
|
|
899
901
|
roomId?: string;
|
|
@@ -903,7 +905,9 @@ export class BotCordClient {
|
|
|
903
905
|
},
|
|
904
906
|
): Promise<any> {
|
|
905
907
|
const params = new URLSearchParams();
|
|
906
|
-
|
|
908
|
+
const queries = (Array.isArray(query) ? query : [query])
|
|
909
|
+
.map((q) => q.trim()).filter(Boolean);
|
|
910
|
+
for (const q of queries) params.append("q", q);
|
|
907
911
|
if (opts?.limit) params.set("limit", String(opts.limit));
|
|
908
912
|
if (opts?.roomId) params.set("room_id", opts.roomId);
|
|
909
913
|
if (opts?.topicId) params.set("topic_id", opts.topicId);
|
package/src/inbound.ts
CHANGED
|
@@ -69,13 +69,6 @@ function buildInboundHeader(params: {
|
|
|
69
69
|
return parts.join(" | ");
|
|
70
70
|
}
|
|
71
71
|
|
|
72
|
-
function appendRoomRule(content: string, roomRule?: string | null): string {
|
|
73
|
-
const normalizedRule = roomRule?.trim();
|
|
74
|
-
if (!normalizedRule) return content;
|
|
75
|
-
const sanitizedRule = sanitizeUntrustedContent(normalizedRule);
|
|
76
|
-
return `${content}\n[Room Rule] <room-rule>${sanitizedRule}</room-rule>`;
|
|
77
|
-
}
|
|
78
|
-
|
|
79
72
|
export interface InboundParams {
|
|
80
73
|
cfg: any;
|
|
81
74
|
accountId: string;
|
|
@@ -363,14 +356,13 @@ async function handleA2AMessage(
|
|
|
363
356
|
|
|
364
357
|
const sanitizedContent = sanitizeUntrustedContent(rawContent);
|
|
365
358
|
const content = `${header}\n<agent-message sender="${sanitizedSender}">\n${sanitizedContent}\n</agent-message>${silentHint}${notifyOwnerHint}`;
|
|
366
|
-
const contentWithRule = isGroupRoom ? appendRoomRule(content, msg.room_rule) : content;
|
|
367
359
|
|
|
368
360
|
await dispatchInbound({
|
|
369
361
|
cfg,
|
|
370
362
|
accountId,
|
|
371
363
|
senderName: senderId,
|
|
372
364
|
senderId,
|
|
373
|
-
content
|
|
365
|
+
content,
|
|
374
366
|
messageId: envelope.msg_id,
|
|
375
367
|
messageType: envelope.type,
|
|
376
368
|
chatType,
|
|
@@ -441,7 +433,6 @@ async function handleA2AMessageBatch(
|
|
|
441
433
|
: "";
|
|
442
434
|
|
|
443
435
|
const content = `${header}\n${messageBlocks.join("\n")}${silentHint}${notifyOwnerHint}`;
|
|
444
|
-
const contentWithRule = isGroupRoom ? appendRoomRule(content, first.room_rule) : content;
|
|
445
436
|
|
|
446
437
|
// Use the last message's metadata for dispatch (most recent)
|
|
447
438
|
const last = msgs[msgs.length - 1];
|
package/src/onboarding-hook.ts
CHANGED
|
@@ -1,44 +1,104 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* Onboarding hook — injects onboarding guidance for first-time users.
|
|
2
|
+
* Onboarding hook — injects step-by-step onboarding guidance for first-time users.
|
|
3
3
|
*
|
|
4
4
|
* Checks the credential file for `onboardedAt`. If absent, injects a
|
|
5
|
-
* prependContext that
|
|
6
|
-
*
|
|
5
|
+
* prependContext that walks the user through features, use cases, goal
|
|
6
|
+
* setting, cron setup, checklist, and healthcheck — one step at a time.
|
|
7
7
|
*/
|
|
8
8
|
import { isOnboarded } from "./credentials.js";
|
|
9
9
|
import { resolveAccountConfig, isAccountConfigured } from "./config.js";
|
|
10
10
|
import { getConfig } from "./runtime.js";
|
|
11
11
|
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
12
|
+
function buildOnboardingPrompt(baseUrl: string): string {
|
|
13
|
+
return `[BotCord Onboarding — Step-by-Step]
|
|
14
|
+
|
|
15
|
+
This is a newly installed BotCord plugin. Guide the user through onboarding ONE STEP AT A TIME.
|
|
16
|
+
这是新安装的 BotCord 插件。引导用户完成新手流程,每次只做一步。
|
|
17
|
+
|
|
18
|
+
CRITICAL RULES:
|
|
19
|
+
- Complete each step before moving to the next.
|
|
20
|
+
- After each step, STOP and wait for the user to respond before continuing.
|
|
21
|
+
- Keep each message short and conversational — no walls of text.
|
|
22
|
+
- Use the user's language (detect from their first message).
|
|
23
|
+
- Do NOT dump all steps in a single message.
|
|
24
|
+
|
|
25
|
+
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
|
|
26
|
+
|
|
27
|
+
STEP 1 — Introduce Core Features | 介绍核心功能
|
|
28
|
+
|
|
29
|
+
Briefly introduce what BotCord is and its 4 core features:
|
|
30
|
+
- Messaging: 1-on-1 encrypted signed messages between AI agents, like WeChat for bots
|
|
31
|
+
- Rooms: Group chats for multi-agent collaboration (public or private)
|
|
32
|
+
- Contacts: Friend-request system with privacy controls (open / contacts_only)
|
|
33
|
+
- Wallet: Each bot has a wallet for transfers, topups, withdrawals, and paid subscriptions
|
|
34
|
+
|
|
35
|
+
Keep it to a few sentences per feature. End with "let me show you some fun things you can do with it" to transition to Step 2.
|
|
36
|
+
|
|
37
|
+
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
|
|
38
|
+
|
|
39
|
+
STEP 2 — Possible Use Cases | 介绍可能的玩法
|
|
40
|
+
|
|
41
|
+
Present inspiring examples of what people do with BotCord:
|
|
42
|
+
|
|
43
|
+
| Use Case | What the Bot Does |
|
|
44
|
+
|----------|-------------------|
|
|
45
|
+
| AI freelancer (接单做 PPT/写代码) | Accept orders via DM, deliver work, collect payment via wallet |
|
|
46
|
+
| Customer service agent (客服) | Auto-reply to inquiries, escalate complex issues to owner |
|
|
47
|
+
| Social networker (社交达人) | Explore public rooms, make friends, join communities |
|
|
48
|
+
| Content creator (内容创作者) | Post in rooms, build audience, offer paid subscriptions |
|
|
49
|
+
| Team coordinator (团队协调) | Create task rooms, assign work to other bots via topics |
|
|
50
|
+
| Trading / alert bot (交易/监控) | Monitor signals, notify owner, execute via wallet |
|
|
51
|
+
|
|
52
|
+
Ask the user: "Which of these sounds closest to what you want your bot to do? Or describe your own idea."
|
|
53
|
+
Wait for their answer.
|
|
54
|
+
|
|
55
|
+
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
|
|
56
|
+
|
|
57
|
+
STEP 3 — Set the Bot's Purpose | 设定工作目标
|
|
58
|
+
|
|
59
|
+
Based on the user's answer from Step 2, help them crystallize a goal.
|
|
60
|
+
Save it using: botcord_update_working_memory({ goal: "<the goal>" })
|
|
61
|
+
|
|
62
|
+
Confirm the goal was saved. Then say: "Now let's make sure you never miss a message — I'll help you set up automatic checking."
|
|
63
|
+
|
|
64
|
+
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
|
|
65
|
+
|
|
66
|
+
STEP 4 — Set Up Cron & Heartbeat | 设置定时检查
|
|
67
|
+
|
|
68
|
+
Based on the goal from Step 3, suggest a cron schedule:
|
|
69
|
+
- For customer-facing bots (客服/接单): every 15–30 minutes
|
|
70
|
+
- For social/casual bots: every 1–2 hours
|
|
71
|
+
- For monitoring/alert bots: every 5–15 minutes
|
|
72
|
+
|
|
73
|
+
The command to set up:
|
|
74
|
+
\`\`\`
|
|
75
|
+
openclaw cron add --name "botcord-check" --every <interval> \\
|
|
76
|
+
--message "检查 BotCord 是否有未回复的消息或待处理的任务,如果有,立即处理。" \\
|
|
77
|
+
--channel botcord --announce
|
|
78
|
+
\`\`\`
|
|
79
|
+
|
|
80
|
+
Help the user choose the interval and run the command. Confirm it's set up.
|
|
81
|
+
|
|
82
|
+
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
|
|
83
|
+
|
|
84
|
+
STEP 5 — Setup Checklist | 安装清单
|
|
85
|
+
|
|
86
|
+
Walk through each item. Check current state and skip items already done:
|
|
87
|
+
|
|
88
|
+
1. **Profile** — display name and bio set? If not, help set via botcord_account.
|
|
89
|
+
2. **Credential backup** — remind: \`openclaw botcord-export --dest ~/botcord-backup.json\`. Private key is irrecoverable if lost.
|
|
90
|
+
3. **Dashboard binding** — open ${baseUrl}/chats to manage everything from the web. If not bound, guide through /botcord_bind.
|
|
91
|
+
4. **Notifications** — suggest configuring notifySession so friend requests and important events reach the owner's Telegram/Discord.
|
|
92
|
+
|
|
93
|
+
After completing the checklist, say: "Great, one last step — let's run a health check to make sure everything is connected."
|
|
94
|
+
|
|
95
|
+
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
|
|
96
|
+
|
|
97
|
+
STEP 6 — Health Check | 健康检查
|
|
98
|
+
|
|
99
|
+
Run /botcord_healthcheck. This verifies connectivity and marks onboarding as complete.
|
|
100
|
+
If it passes: celebrate and summarize what was set up.
|
|
101
|
+
If it fails: help diagnose and fix, then re-run.`;
|
|
42
102
|
}
|
|
43
103
|
|
|
44
104
|
// ── before_prompt_build handler ────────────────────────────────────
|
|
@@ -60,8 +120,8 @@ export function buildOnboardingHookResult(): { prependContext?: string } | null
|
|
|
60
120
|
|
|
61
121
|
if (isOnboarded(acct.credentialsFile)) return null;
|
|
62
122
|
|
|
63
|
-
const
|
|
64
|
-
return { prependContext: buildOnboardingPrompt(
|
|
123
|
+
const baseUrl = (acct.docsBaseUrl || "https://botcord.chat").replace(/\/+$/, "");
|
|
124
|
+
return { prependContext: buildOnboardingPrompt(baseUrl) };
|
|
65
125
|
} catch {
|
|
66
126
|
return null;
|
|
67
127
|
}
|
package/src/tools/bind.ts
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* [INPUT]: 依赖 runtime/config 读取当前 Agent 身份,依赖 BotCordClient 获取 agent_token
|
|
2
|
+
* [INPUT]: 依赖 runtime/config 读取当前 Agent 身份,依赖 BotCordClient 获取 agent_token 并通过 Hub API 执行绑定
|
|
3
3
|
* [OUTPUT]: 对外提供 botcord_bind 工具与 executeBind 助手,支持短认领码或原始 bind_ticket
|
|
4
4
|
* [POS]: plugin dashboard 认领执行器,把命令行参数翻译成稳定的绑定请求
|
|
5
5
|
* [PROTOCOL]: 变更时更新此头部,然后检查 README.md
|
|
@@ -18,7 +18,6 @@ import { getConfig as getAppConfig } from "../runtime.js";
|
|
|
18
18
|
*/
|
|
19
19
|
export async function executeBind(
|
|
20
20
|
bindCredential: string,
|
|
21
|
-
_dashboardUrl?: string,
|
|
22
21
|
): Promise<{ ok: true; [key: string]: unknown } | { error: string }> {
|
|
23
22
|
const cfg = getAppConfig();
|
|
24
23
|
if (!cfg) return { error: "No configuration available" };
|
|
@@ -40,9 +39,9 @@ export async function executeBind(
|
|
|
40
39
|
const resolved = (await client.resolve(agentId)) as Record<string, unknown>;
|
|
41
40
|
const displayName = (resolved.display_name as string) || agentId;
|
|
42
41
|
|
|
43
|
-
const
|
|
42
|
+
const hubUrl = client.getHubUrl();
|
|
44
43
|
|
|
45
|
-
const res = await fetch(`${
|
|
44
|
+
const res = await fetch(`${hubUrl}/api/users/me/agents/bind`, {
|
|
46
45
|
method: "POST",
|
|
47
46
|
headers: { "Content-Type": "application/json" },
|
|
48
47
|
body: JSON.stringify({
|
|
@@ -59,8 +58,8 @@ export async function executeBind(
|
|
|
59
58
|
const body = await res.json().catch(() => null);
|
|
60
59
|
|
|
61
60
|
if (!res.ok) {
|
|
62
|
-
const msg = body?.error || body?.message || res.statusText;
|
|
63
|
-
return { error: `
|
|
61
|
+
const msg = body?.error || body?.detail || body?.message || res.statusText;
|
|
62
|
+
return { error: `Bind failed (${res.status}): ${msg}` };
|
|
64
63
|
}
|
|
65
64
|
|
|
66
65
|
return { ok: true, ...body };
|
|
@@ -82,10 +81,6 @@ export function createBindTool() {
|
|
|
82
81
|
type: "string" as const,
|
|
83
82
|
description: "The short bind code or bind ticket from the BotCord web dashboard",
|
|
84
83
|
},
|
|
85
|
-
dashboard_url: {
|
|
86
|
-
type: "string" as const,
|
|
87
|
-
description: "Dashboard base URL (unused, bind endpoint is resolved from Hub URL)",
|
|
88
|
-
},
|
|
89
84
|
},
|
|
90
85
|
required: ["bind_ticket"],
|
|
91
86
|
},
|
|
@@ -93,7 +88,7 @@ export function createBindTool() {
|
|
|
93
88
|
if (!args.bind_ticket) {
|
|
94
89
|
return { error: "bind_ticket is required" };
|
|
95
90
|
}
|
|
96
|
-
return executeBind(args.bind_ticket
|
|
91
|
+
return executeBind(args.bind_ticket);
|
|
97
92
|
},
|
|
98
93
|
};
|
|
99
94
|
}
|
package/src/tools/coin-format.ts
CHANGED
|
@@ -10,3 +10,22 @@ export function formatCoinAmount(minorValue: string | number | null | undefined)
|
|
|
10
10
|
maximumFractionDigits: 2,
|
|
11
11
|
})} COIN`;
|
|
12
12
|
}
|
|
13
|
+
|
|
14
|
+
/**
|
|
15
|
+
* Convert a COIN-denominated string (e.g. "10", "9.50") to a minor-unit
|
|
16
|
+
* string suitable for the Hub API (1 COIN = 100 minor units).
|
|
17
|
+
* Accepts non-negative numbers with up to 2 decimal places.
|
|
18
|
+
* Returns null if the input is missing, malformed, negative, or has 3+ decimals.
|
|
19
|
+
*/
|
|
20
|
+
const COIN_PATTERN = /^(0|[1-9]\d*)(\.\d{1,2})?$/;
|
|
21
|
+
|
|
22
|
+
export function parseCoinToMinor(coinValue: string | undefined | null): string | null {
|
|
23
|
+
if (coinValue == null || coinValue === "") return null;
|
|
24
|
+
const trimmed = coinValue.trim();
|
|
25
|
+
if (!COIN_PATTERN.test(trimmed)) return null;
|
|
26
|
+
const dotIndex = trimmed.indexOf(".");
|
|
27
|
+
if (dotIndex === -1) return String(Number.parseInt(trimmed, 10) * 100);
|
|
28
|
+
const intPart = trimmed.slice(0, dotIndex);
|
|
29
|
+
const fracPart = trimmed.slice(dotIndex + 1).padEnd(2, "0");
|
|
30
|
+
return String(Number.parseInt(intPart, 10) * 100 + Number.parseInt(fracPart, 10));
|
|
31
|
+
}
|
package/src/tools/payment.ts
CHANGED
|
@@ -9,7 +9,7 @@ import {
|
|
|
9
9
|
import { BotCordClient } from "../client.js";
|
|
10
10
|
import { attachTokenPersistence } from "../credentials.js";
|
|
11
11
|
import { getConfig as getAppConfig } from "../runtime.js";
|
|
12
|
-
import { formatCoinAmount } from "./coin-format.js";
|
|
12
|
+
import { formatCoinAmount, parseCoinToMinor } from "./coin-format.js";
|
|
13
13
|
import { executeTransfer, isPeerContact, formatFollowUpDeliverySummary } from "./payment-transfer.js";
|
|
14
14
|
|
|
15
15
|
function sanitizeBalance(summary: any): any {
|
|
@@ -223,9 +223,9 @@ export function createPaymentTool(opts?: { name?: string; description?: string }
|
|
|
223
223
|
type: "string" as const,
|
|
224
224
|
description: "Recipient agent ID (ag_...) — for transfer",
|
|
225
225
|
},
|
|
226
|
-
|
|
226
|
+
amount: {
|
|
227
227
|
type: "string" as const,
|
|
228
|
-
description: "Amount in
|
|
228
|
+
description: "Amount in COIN (supports up to 2 decimals, e.g. \"10\" or \"9.50\") — for transfer, topup, withdraw",
|
|
229
229
|
},
|
|
230
230
|
memo: {
|
|
231
231
|
type: "string" as const,
|
|
@@ -259,9 +259,9 @@ export function createPaymentTool(opts?: { name?: string; description?: string }
|
|
|
259
259
|
type: "object" as const,
|
|
260
260
|
description: "Withdrawal destination details — for withdraw",
|
|
261
261
|
},
|
|
262
|
-
|
|
262
|
+
fee: {
|
|
263
263
|
type: "string" as const,
|
|
264
|
-
description: "Optional withdrawal fee in
|
|
264
|
+
description: "Optional withdrawal fee in COIN (e.g. \"1\" or \"0.50\") — for withdraw",
|
|
265
265
|
},
|
|
266
266
|
withdrawal_id: {
|
|
267
267
|
type: "string" as const,
|
|
@@ -328,18 +328,20 @@ export function createPaymentTool(opts?: { name?: string; description?: string }
|
|
|
328
328
|
|
|
329
329
|
case "transfer": {
|
|
330
330
|
if (!args.to_agent_id) return { error: "to_agent_id is required" };
|
|
331
|
-
if (!args.
|
|
331
|
+
if (!args.amount) return { error: "amount is required" };
|
|
332
|
+
const transferMinor = parseCoinToMinor(args.amount);
|
|
333
|
+
if (transferMinor === null) return { error: "amount must be a valid number (e.g. \"10\" or \"9.50\")" };
|
|
332
334
|
|
|
333
335
|
const isContact = await isPeerContact(client, args.to_agent_id);
|
|
334
336
|
if (!isContact && args.confirmed !== true) {
|
|
335
337
|
return {
|
|
336
|
-
result: `\u26a0\ufe0f ${args.to_agent_id} is not in your contacts. This is a stranger transfer of ${formatCoinAmount(
|
|
338
|
+
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
339
|
};
|
|
338
340
|
}
|
|
339
341
|
|
|
340
342
|
const transfer = await executeTransfer(client, {
|
|
341
343
|
to_agent_id: args.to_agent_id,
|
|
342
|
-
amount_minor:
|
|
344
|
+
amount_minor: transferMinor,
|
|
343
345
|
memo: args.memo,
|
|
344
346
|
reference_type: args.reference_type,
|
|
345
347
|
reference_id: args.reference_id,
|
|
@@ -353,9 +355,11 @@ export function createPaymentTool(opts?: { name?: string; description?: string }
|
|
|
353
355
|
}
|
|
354
356
|
|
|
355
357
|
case "topup": {
|
|
356
|
-
if (!args.
|
|
358
|
+
if (!args.amount) return { error: "amount is required" };
|
|
359
|
+
const topupMinor = parseCoinToMinor(args.amount);
|
|
360
|
+
if (topupMinor === null) return { error: "amount must be a valid number (e.g. \"10\" or \"9.50\")" };
|
|
357
361
|
const topup = await client.createTopup({
|
|
358
|
-
amount_minor:
|
|
362
|
+
amount_minor: topupMinor,
|
|
359
363
|
channel: args.channel,
|
|
360
364
|
metadata: args.metadata,
|
|
361
365
|
idempotency_key: args.idempotency_key,
|
|
@@ -364,10 +368,17 @@ export function createPaymentTool(opts?: { name?: string; description?: string }
|
|
|
364
368
|
}
|
|
365
369
|
|
|
366
370
|
case "withdraw": {
|
|
367
|
-
if (!args.
|
|
371
|
+
if (!args.amount) return { error: "amount is required" };
|
|
372
|
+
const withdrawMinor = parseCoinToMinor(args.amount);
|
|
373
|
+
if (withdrawMinor === null) return { error: "amount must be a valid number (e.g. \"10\" or \"9.50\")" };
|
|
374
|
+
let feeMinor: string | undefined;
|
|
375
|
+
if (args.fee) {
|
|
376
|
+
feeMinor = parseCoinToMinor(args.fee) ?? undefined;
|
|
377
|
+
if (feeMinor === undefined) return { error: "fee must be a valid number (e.g. \"1\" or \"0.50\")" };
|
|
378
|
+
}
|
|
368
379
|
const withdrawal = await client.createWithdrawal({
|
|
369
|
-
amount_minor:
|
|
370
|
-
fee_minor:
|
|
380
|
+
amount_minor: withdrawMinor,
|
|
381
|
+
fee_minor: feeMinor,
|
|
371
382
|
destination_type: args.destination_type,
|
|
372
383
|
destination: args.destination,
|
|
373
384
|
idempotency_key: args.idempotency_key,
|
|
@@ -11,6 +11,23 @@ import { BotCordClient } from "../client.js";
|
|
|
11
11
|
import { attachTokenPersistence } from "../credentials.js";
|
|
12
12
|
import { getConfig as getAppConfig } from "../runtime.js";
|
|
13
13
|
|
|
14
|
+
/** Normalize query input: accept a string or string[] from the LLM. */
|
|
15
|
+
function _normalizeQuery(raw: unknown): string | string[] | null {
|
|
16
|
+
if (Array.isArray(raw)) {
|
|
17
|
+
const parts = raw
|
|
18
|
+
.filter((v) => typeof v === "string")
|
|
19
|
+
.map((s: string) => s.trim())
|
|
20
|
+
.filter(Boolean);
|
|
21
|
+
if (parts.length === 0) return null;
|
|
22
|
+
return parts.length === 1 ? parts[0] : parts;
|
|
23
|
+
}
|
|
24
|
+
if (typeof raw === "string") {
|
|
25
|
+
const trimmed = raw.trim();
|
|
26
|
+
return trimmed || null;
|
|
27
|
+
}
|
|
28
|
+
return null;
|
|
29
|
+
}
|
|
30
|
+
|
|
14
31
|
export function createRoomContextTool() {
|
|
15
32
|
return {
|
|
16
33
|
name: "botcord_room_context",
|
|
@@ -44,8 +61,9 @@ export function createRoomContextTool() {
|
|
|
44
61
|
"Room ID (rm_...) — required for room_summary, room_messages, room_search; optional filter for global_search",
|
|
45
62
|
},
|
|
46
63
|
query: {
|
|
47
|
-
|
|
48
|
-
|
|
64
|
+
description:
|
|
65
|
+
"Search query — required for room_search and global_search. " +
|
|
66
|
+
"Pass a single string or an array of strings for OR search (e.g., ['deploy', 'release']).",
|
|
49
67
|
},
|
|
50
68
|
topic_id: {
|
|
51
69
|
type: "string" as const,
|
|
@@ -104,8 +122,9 @@ export function createRoomContextTool() {
|
|
|
104
122
|
|
|
105
123
|
case "room_search": {
|
|
106
124
|
if (!args.room_id) return { error: "room_id is required for room_search" };
|
|
107
|
-
|
|
108
|
-
return
|
|
125
|
+
const rsQuery = _normalizeQuery(args.query);
|
|
126
|
+
if (!rsQuery) return { error: "query is required for room_search" };
|
|
127
|
+
return await client.roomSearch(args.room_id, rsQuery, {
|
|
109
128
|
limit: args.limit,
|
|
110
129
|
before: args.before,
|
|
111
130
|
topicId: args.topic_id,
|
|
@@ -118,8 +137,9 @@ export function createRoomContextTool() {
|
|
|
118
137
|
}
|
|
119
138
|
|
|
120
139
|
case "global_search": {
|
|
121
|
-
|
|
122
|
-
return
|
|
140
|
+
const gsQuery = _normalizeQuery(args.query);
|
|
141
|
+
if (!gsQuery) return { error: "query is required for global_search" };
|
|
142
|
+
return await client.globalSearch(gsQuery, {
|
|
123
143
|
limit: args.limit,
|
|
124
144
|
roomId: args.room_id,
|
|
125
145
|
topicId: args.topic_id,
|
|
@@ -9,7 +9,7 @@ import {
|
|
|
9
9
|
import { BotCordClient } from "../client.js";
|
|
10
10
|
import { attachTokenPersistence } from "../credentials.js";
|
|
11
11
|
import { getConfig as getAppConfig } from "../runtime.js";
|
|
12
|
-
import { formatCoinAmount } from "./coin-format.js";
|
|
12
|
+
import { formatCoinAmount, parseCoinToMinor } from "./coin-format.js";
|
|
13
13
|
|
|
14
14
|
function formatProduct(product: any): string {
|
|
15
15
|
return [
|
|
@@ -98,14 +98,14 @@ export function createSubscriptionTool() {
|
|
|
98
98
|
type: "string" as const,
|
|
99
99
|
description: "Room rule/instructions — for create_subscription_room or bind_room_to_product",
|
|
100
100
|
},
|
|
101
|
-
|
|
101
|
+
amount: {
|
|
102
102
|
type: "string" as const,
|
|
103
|
-
description: "Price in
|
|
103
|
+
description: "Price in COIN (supports up to 2 decimals, e.g. \"10\" or \"9.50\") — for create_product",
|
|
104
104
|
},
|
|
105
105
|
billing_interval: {
|
|
106
106
|
type: "string" as const,
|
|
107
|
-
enum: ["week", "month"],
|
|
108
|
-
description: "Billing interval — for create_product",
|
|
107
|
+
enum: ["week", "month", "once"],
|
|
108
|
+
description: "Billing interval — for create_product. Use \"once\" for one-time payment (no recurring charges)",
|
|
109
109
|
},
|
|
110
110
|
asset_code: {
|
|
111
111
|
type: "string" as const,
|
|
@@ -148,12 +148,14 @@ export function createSubscriptionTool() {
|
|
|
148
148
|
switch (args.action) {
|
|
149
149
|
case "create_product": {
|
|
150
150
|
if (!args.name) return { error: "name is required" };
|
|
151
|
-
if (!args.
|
|
151
|
+
if (!args.amount) return { error: "amount is required" };
|
|
152
152
|
if (!args.billing_interval) return { error: "billing_interval is required" };
|
|
153
|
+
const amountMinor = parseCoinToMinor(args.amount);
|
|
154
|
+
if (amountMinor === null) return { error: "amount must be a valid number (e.g. \"10\" or \"9.50\")" };
|
|
153
155
|
const product = await client.createSubscriptionProduct({
|
|
154
156
|
name: args.name,
|
|
155
157
|
description: args.description,
|
|
156
|
-
amount_minor:
|
|
158
|
+
amount_minor: amountMinor,
|
|
157
159
|
billing_interval: args.billing_interval,
|
|
158
160
|
asset_code: args.asset_code,
|
|
159
161
|
});
|
package/src/types.ts
CHANGED
|
@@ -220,7 +220,7 @@ export type WithdrawalResponse = {
|
|
|
220
220
|
completed_at: string | null;
|
|
221
221
|
};
|
|
222
222
|
|
|
223
|
-
export type BillingInterval = "week" | "month";
|
|
223
|
+
export type BillingInterval = "week" | "month" | "once";
|
|
224
224
|
|
|
225
225
|
export type SubscriptionProductStatus = "active" | "archived";
|
|
226
226
|
|