@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,94 @@
1
+ /**
2
+ * botcord_account — Manage the agent's own identity, profile, and settings.
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
+
12
+ export function createAccountTool() {
13
+ return {
14
+ name: "botcord_account",
15
+ description:
16
+ "Manage your own BotCord agent: view identity, update profile, get/set message policy, check message delivery status.",
17
+ parameters: {
18
+ type: "object" as const,
19
+ properties: {
20
+ action: {
21
+ type: "string" as const,
22
+ enum: ["whoami", "update_profile", "get_policy", "set_policy", "message_status"],
23
+ description: "Account action to perform",
24
+ },
25
+ display_name: {
26
+ type: "string" as const,
27
+ description: "New display name — for update_profile",
28
+ },
29
+ bio: {
30
+ type: "string" as const,
31
+ description: "New bio — for update_profile",
32
+ },
33
+ policy: {
34
+ type: "string" as const,
35
+ enum: ["open", "contacts_only"],
36
+ description: "Message policy — for set_policy",
37
+ },
38
+ msg_id: {
39
+ type: "string" as const,
40
+ description: "Message ID — for message_status",
41
+ },
42
+ },
43
+ required: ["action"],
44
+ },
45
+ execute: async (toolCallId: any, args: any, signal?: any, onUpdate?: any) => {
46
+ const cfg = getAppConfig();
47
+ if (!cfg) return { error: "No configuration available" };
48
+ const singleAccountError = getSingleAccountModeError(cfg);
49
+ if (singleAccountError) return { error: singleAccountError };
50
+
51
+ const acct = resolveAccountConfig(cfg);
52
+ if (!isAccountConfigured(acct)) {
53
+ return { error: "BotCord is not configured." };
54
+ }
55
+
56
+ const client = new BotCordClient(acct);
57
+
58
+ try {
59
+ switch (args.action) {
60
+ case "whoami":
61
+ return await client.resolve(client.getAgentId());
62
+
63
+ case "update_profile": {
64
+ if (!args.display_name && !args.bio) {
65
+ return { error: "At least one of display_name or bio is required" };
66
+ }
67
+ const params: { display_name?: string; bio?: string } = {};
68
+ if (args.display_name) params.display_name = args.display_name;
69
+ if (args.bio) params.bio = args.bio;
70
+ await client.updateProfile(params);
71
+ return { ok: true, updated: params };
72
+ }
73
+
74
+ case "get_policy":
75
+ return await client.getPolicy();
76
+
77
+ case "set_policy":
78
+ if (!args.policy) return { error: "policy is required (open or contacts_only)" };
79
+ await client.setPolicy(args.policy);
80
+ return { ok: true, policy: args.policy };
81
+
82
+ case "message_status":
83
+ if (!args.msg_id) return { error: "msg_id is required" };
84
+ return await client.getMessageStatus(args.msg_id);
85
+
86
+ default:
87
+ return { error: `Unknown action: ${args.action}` };
88
+ }
89
+ } catch (err: any) {
90
+ return { error: `Account action failed: ${err.message}` };
91
+ }
92
+ },
93
+ };
94
+ }
@@ -0,0 +1,96 @@
1
+ /**
2
+ * botcord_bind — Bind this BotCord agent to a user's web dashboard account.
3
+ *
4
+ * Also exports `executeBind()` shared helper used by both the tool and the
5
+ * `/botcord_bind` command.
6
+ */
7
+ import {
8
+ getSingleAccountModeError,
9
+ resolveAccountConfig,
10
+ isAccountConfigured,
11
+ } from "../config.js";
12
+ import { BotCordClient } from "../client.js";
13
+ import { getConfig as getAppConfig } from "../runtime.js";
14
+
15
+ const DEFAULT_DASHBOARD_URL = "https://www.botcord.chat";
16
+
17
+ /**
18
+ * Shared bind logic used by both the tool and the command.
19
+ */
20
+ export async function executeBind(
21
+ bindTicket: string,
22
+ dashboardUrl?: string,
23
+ ): Promise<{ ok: true; [key: string]: unknown } | { error: string }> {
24
+ const cfg = getAppConfig();
25
+ if (!cfg) return { error: "No configuration available" };
26
+ const singleAccountError = getSingleAccountModeError(cfg);
27
+ if (singleAccountError) return { error: singleAccountError };
28
+
29
+ const acct = resolveAccountConfig(cfg);
30
+ if (!isAccountConfigured(acct)) {
31
+ return { error: "BotCord is not configured." };
32
+ }
33
+
34
+ const client = new BotCordClient(acct);
35
+
36
+ try {
37
+ const agentToken = await client.ensureToken();
38
+ const agentId = client.getAgentId();
39
+
40
+ const resolved = (await client.resolve(agentId)) as Record<string, unknown>;
41
+ const displayName = (resolved.display_name as string) || agentId;
42
+
43
+ const baseUrl = (dashboardUrl || DEFAULT_DASHBOARD_URL).replace(/\/+$/, "");
44
+
45
+ const res = await fetch(`${baseUrl}/api/users/me/agents/bind`, {
46
+ method: "POST",
47
+ headers: { "Content-Type": "application/json" },
48
+ body: JSON.stringify({
49
+ agent_id: agentId,
50
+ display_name: displayName,
51
+ agent_token: agentToken,
52
+ bind_ticket: bindTicket,
53
+ }),
54
+ signal: AbortSignal.timeout(15000),
55
+ });
56
+
57
+ const body = await res.json().catch(() => null);
58
+
59
+ if (!res.ok) {
60
+ const msg = body?.error || body?.message || res.statusText;
61
+ return { error: `Dashboard bind failed (${res.status}): ${msg}` };
62
+ }
63
+
64
+ return { ok: true, ...body };
65
+ } catch (err: any) {
66
+ return { error: `Bind failed: ${err.message}` };
67
+ }
68
+ }
69
+
70
+ export function createBindTool() {
71
+ return {
72
+ name: "botcord_bind",
73
+ description:
74
+ "Bind this BotCord agent to a user's web dashboard account using a bind ticket.",
75
+ parameters: {
76
+ type: "object" as const,
77
+ properties: {
78
+ bind_ticket: {
79
+ type: "string" as const,
80
+ description: "The bind ticket from the BotCord web dashboard",
81
+ },
82
+ dashboard_url: {
83
+ type: "string" as const,
84
+ description: `Dashboard base URL (defaults to ${DEFAULT_DASHBOARD_URL})`,
85
+ },
86
+ },
87
+ required: ["bind_ticket"],
88
+ },
89
+ execute: async (toolCallId: any, args: any, signal?: any, onUpdate?: any) => {
90
+ if (!args.bind_ticket) {
91
+ return { error: "bind_ticket is required" };
92
+ }
93
+ return executeBind(args.bind_ticket, args.dashboard_url);
94
+ },
95
+ };
96
+ }
@@ -0,0 +1,12 @@
1
+ export function formatCoinAmount(minorValue: string | number | null | undefined): string {
2
+ const minor = typeof minorValue === "number"
3
+ ? minorValue
4
+ : Number.parseInt(minorValue ?? "0", 10);
5
+
6
+ if (!Number.isFinite(minor)) return "0.00 COIN";
7
+
8
+ return `${(minor / 100).toLocaleString("en-US", {
9
+ minimumFractionDigits: 2,
10
+ maximumFractionDigits: 2,
11
+ })} COIN`;
12
+ }
@@ -0,0 +1,120 @@
1
+ /**
2
+ * botcord_contacts — Manage social relationships: contacts, requests, blocks.
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
+
12
+ export function createContactsTool() {
13
+ return {
14
+ name: "botcord_contacts",
15
+ description: "Manage BotCord contacts: list/remove contacts, send/accept/reject requests, block/unblock agents.",
16
+ parameters: {
17
+ type: "object" as const,
18
+ properties: {
19
+ action: {
20
+ type: "string" as const,
21
+ enum: [
22
+ "list",
23
+ "remove",
24
+ "send_request",
25
+ "received_requests",
26
+ "sent_requests",
27
+ "accept_request",
28
+ "reject_request",
29
+ "block",
30
+ "unblock",
31
+ "list_blocks",
32
+ ],
33
+ description: "Contact action to perform",
34
+ },
35
+ agent_id: {
36
+ type: "string" as const,
37
+ description: "Agent ID (ag_...) — for remove, send_request, block, unblock",
38
+ },
39
+ message: {
40
+ type: "string" as const,
41
+ description: "Message to include with contact request — for send_request",
42
+ },
43
+ request_id: {
44
+ type: "string" as const,
45
+ description: "Request ID — for accept_request, reject_request",
46
+ },
47
+ state: {
48
+ type: "string" as const,
49
+ enum: ["pending", "accepted", "rejected"],
50
+ description: "Filter by state — for received_requests, sent_requests",
51
+ },
52
+ },
53
+ required: ["action"],
54
+ },
55
+ execute: async (toolCallId: any, args: any, signal?: any, onUpdate?: any) => {
56
+ const cfg = getAppConfig();
57
+ if (!cfg) return { error: "No configuration available" };
58
+ const singleAccountError = getSingleAccountModeError(cfg);
59
+ if (singleAccountError) return { error: singleAccountError };
60
+
61
+ const acct = resolveAccountConfig(cfg);
62
+ if (!isAccountConfigured(acct)) {
63
+ return { error: "BotCord is not configured." };
64
+ }
65
+
66
+ const client = new BotCordClient(acct);
67
+
68
+ try {
69
+ switch (args.action) {
70
+ case "list":
71
+ return await client.listContacts();
72
+
73
+ case "remove":
74
+ if (!args.agent_id) return { error: "agent_id is required" };
75
+ await client.removeContact(args.agent_id);
76
+ return { ok: true, removed: args.agent_id };
77
+
78
+ case "send_request":
79
+ if (!args.agent_id) return { error: "agent_id is required" };
80
+ await client.sendContactRequest(args.agent_id, args.message);
81
+ return { ok: true, sent_to: args.agent_id };
82
+
83
+ case "received_requests":
84
+ return await client.listReceivedRequests(args.state);
85
+
86
+ case "sent_requests":
87
+ return await client.listSentRequests(args.state);
88
+
89
+ case "accept_request":
90
+ if (!args.request_id) return { error: "request_id is required" };
91
+ await client.acceptRequest(args.request_id);
92
+ return { ok: true, accepted: args.request_id };
93
+
94
+ case "reject_request":
95
+ if (!args.request_id) return { error: "request_id is required" };
96
+ await client.rejectRequest(args.request_id);
97
+ return { ok: true, rejected: args.request_id };
98
+
99
+ case "block":
100
+ if (!args.agent_id) return { error: "agent_id is required" };
101
+ await client.blockAgent(args.agent_id);
102
+ return { ok: true, blocked: args.agent_id };
103
+
104
+ case "unblock":
105
+ if (!args.agent_id) return { error: "agent_id is required" };
106
+ await client.unblockAgent(args.agent_id);
107
+ return { ok: true, unblocked: args.agent_id };
108
+
109
+ case "list_blocks":
110
+ return await client.listBlocks();
111
+
112
+ default:
113
+ return { error: `Unknown action: ${args.action}` };
114
+ }
115
+ } catch (err: any) {
116
+ return { error: `Contact action failed: ${err.message}` };
117
+ }
118
+ },
119
+ };
120
+ }
@@ -0,0 +1,104 @@
1
+ /**
2
+ * botcord_directory — Read-only queries: resolve agents, discover rooms, message history.
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
+
12
+ export function createDirectoryTool() {
13
+ return {
14
+ name: "botcord_directory",
15
+ description: "Look up agents, discover public rooms, and query message history on BotCord.",
16
+ parameters: {
17
+ type: "object" as const,
18
+ properties: {
19
+ action: {
20
+ type: "string" as const,
21
+ enum: ["resolve", "discover_rooms", "history"],
22
+ description: "Query action to perform",
23
+ },
24
+ agent_id: {
25
+ type: "string" as const,
26
+ description: "Agent ID to resolve (ag_...)",
27
+ },
28
+ room_name: {
29
+ type: "string" as const,
30
+ description: "Room name to search — for discover_rooms",
31
+ },
32
+ peer: {
33
+ type: "string" as const,
34
+ description: "Peer agent ID — for history",
35
+ },
36
+ room_id: {
37
+ type: "string" as const,
38
+ description: "Room ID — for history",
39
+ },
40
+ topic: {
41
+ type: "string" as const,
42
+ description: "Topic name — for history",
43
+ },
44
+ topic_id: {
45
+ type: "string" as const,
46
+ description: "Topic ID — for history",
47
+ },
48
+ before: {
49
+ type: "string" as const,
50
+ description: "Return messages before this hub message ID — for history",
51
+ },
52
+ after: {
53
+ type: "string" as const,
54
+ description: "Return messages after this hub message ID — for history",
55
+ },
56
+ limit: {
57
+ type: "number" as const,
58
+ description: "Max results to return",
59
+ },
60
+ },
61
+ required: ["action"],
62
+ },
63
+ execute: async (toolCallId: any, args: any, signal?: any, onUpdate?: any) => {
64
+ const cfg = getAppConfig();
65
+ if (!cfg) return { error: "No configuration available" };
66
+ const singleAccountError = getSingleAccountModeError(cfg);
67
+ if (singleAccountError) return { error: singleAccountError };
68
+
69
+ const acct = resolveAccountConfig(cfg);
70
+ if (!isAccountConfigured(acct)) {
71
+ return { error: "BotCord is not configured." };
72
+ }
73
+
74
+ const client = new BotCordClient(acct);
75
+
76
+ try {
77
+ switch (args.action) {
78
+ case "resolve":
79
+ if (!args.agent_id) return { error: "agent_id is required" };
80
+ return await client.resolve(args.agent_id);
81
+
82
+ case "discover_rooms":
83
+ return await client.discoverRooms(args.room_name);
84
+
85
+ case "history":
86
+ return await client.getHistory({
87
+ peer: args.peer,
88
+ roomId: args.room_id,
89
+ topic: args.topic,
90
+ topicId: args.topic_id,
91
+ before: args.before,
92
+ after: args.after,
93
+ limit: args.limit || 20,
94
+ });
95
+
96
+ default:
97
+ return { error: `Unknown action: ${args.action}` };
98
+ }
99
+ } catch (err: any) {
100
+ return { error: `Directory action failed: ${err.message}` };
101
+ }
102
+ },
103
+ };
104
+ }
@@ -0,0 +1,234 @@
1
+ /**
2
+ * botcord_send — Agent tool for sending messages via BotCord.
3
+ */
4
+ import { readFile } from "node:fs/promises";
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 { getConfig as getAppConfig } from "../runtime.js";
14
+ import type { MessageAttachment } from "../types.js";
15
+
16
+ /** Extract clean filename from a URL, stripping query string and hash. */
17
+ function extractFilename(url: string): string {
18
+ try {
19
+ return new URL(url).pathname.split("/").pop() || "attachment";
20
+ } catch {
21
+ return url.split("/").pop()?.split("?")[0]?.split("#")[0] || "attachment";
22
+ }
23
+ }
24
+
25
+ /** Guess MIME type from file extension. */
26
+ function guessMimeType(filename: string): string {
27
+ const ext = filename.split(".").pop()?.toLowerCase();
28
+ const map: Record<string, string> = {
29
+ txt: "text/plain",
30
+ html: "text/html",
31
+ css: "text/css",
32
+ js: "text/javascript",
33
+ json: "application/json",
34
+ xml: "application/xml",
35
+ pdf: "application/pdf",
36
+ zip: "application/zip",
37
+ gz: "application/gzip",
38
+ png: "image/png",
39
+ jpg: "image/jpeg",
40
+ jpeg: "image/jpeg",
41
+ gif: "image/gif",
42
+ webp: "image/webp",
43
+ svg: "image/svg+xml",
44
+ mp3: "audio/mpeg",
45
+ wav: "audio/wav",
46
+ mp4: "video/mp4",
47
+ webm: "video/webm",
48
+ csv: "text/csv",
49
+ md: "text/markdown",
50
+ };
51
+ return map[ext || ""] || "application/octet-stream";
52
+ }
53
+
54
+ /**
55
+ * Upload local files to Hub and return attachments.
56
+ */
57
+ async function uploadLocalFiles(
58
+ client: BotCordClient,
59
+ filePaths: string[],
60
+ ): Promise<MessageAttachment[]> {
61
+ const results: MessageAttachment[] = [];
62
+ for (const filePath of filePaths) {
63
+ const data = await readFile(filePath);
64
+ const filename = basename(filePath);
65
+ const contentType = guessMimeType(filename);
66
+ const uploaded = await client.uploadFile(data, filename, contentType);
67
+ results.push({
68
+ filename: uploaded.original_filename,
69
+ url: uploaded.url,
70
+ content_type: uploaded.content_type,
71
+ size_bytes: uploaded.size_bytes,
72
+ });
73
+ }
74
+ return results;
75
+ }
76
+
77
+ export function createMessagingTool() {
78
+ return {
79
+ name: "botcord_send",
80
+ description:
81
+ "Send a message to another agent or room via BotCord. " +
82
+ "Use ag_* for direct messages, rm_* for rooms. " +
83
+ "Set type to 'result' or 'error' to terminate a topic. " +
84
+ "Attach files via file_paths (local files, auto-uploaded) or file_urls (existing URLs).",
85
+ parameters: {
86
+ type: "object" as const,
87
+ properties: {
88
+ to: {
89
+ type: "string" as const,
90
+ description: "Target agent ID (ag_...) or room ID (rm_...)",
91
+ },
92
+ text: {
93
+ type: "string" as const,
94
+ description: "Message text to send",
95
+ },
96
+ topic: {
97
+ type: "string" as const,
98
+ description: "Topic name for the conversation",
99
+ },
100
+ goal: {
101
+ type: "string" as const,
102
+ description: "Goal of the conversation — declares why the topic exists",
103
+ },
104
+ type: {
105
+ type: "string" as const,
106
+ enum: ["message", "result", "error"],
107
+ description: "Message type: 'message' (default), 'result' (task done), 'error' (task failed)",
108
+ },
109
+ reply_to: {
110
+ type: "string" as const,
111
+ description: "Message ID to reply to",
112
+ },
113
+ mentions: {
114
+ type: "array" as const,
115
+ items: { type: "string" as const },
116
+ description: 'Agent IDs to mention (e.g. ["ag_xxx"]). Use ["@all"] to mention everyone.',
117
+ },
118
+ file_paths: {
119
+ type: "array" as const,
120
+ items: { type: "string" as const },
121
+ description: "Local file paths to upload and attach to the message",
122
+ },
123
+ file_urls: {
124
+ type: "array" as const,
125
+ items: { type: "string" as const },
126
+ description: "URLs of already-hosted files to attach to the message",
127
+ },
128
+ },
129
+ required: ["to", "text"],
130
+ },
131
+ execute: async (toolCallId: any, args: any, signal?: any, onUpdate?: any) => {
132
+ const cfg = getAppConfig();
133
+ if (!cfg) return { error: "No configuration available" };
134
+ const singleAccountError = getSingleAccountModeError(cfg);
135
+ if (singleAccountError) return { error: singleAccountError };
136
+
137
+ const acct = resolveAccountConfig(cfg);
138
+ if (!isAccountConfigured(acct)) {
139
+ return { error: "BotCord is not configured. Set hubUrl, agentId, keyId, and privateKey." };
140
+ }
141
+
142
+ try {
143
+ const client = new BotCordClient(acct);
144
+ const msgType = args.type || "message";
145
+
146
+ // Collect attachments from both file_paths (upload first) and file_urls
147
+ const attachments: MessageAttachment[] = [];
148
+
149
+ // Upload local files
150
+ if (args.file_paths && args.file_paths.length > 0) {
151
+ const uploaded = await uploadLocalFiles(client, args.file_paths);
152
+ attachments.push(...uploaded);
153
+ }
154
+
155
+ // Add pre-existing URL attachments
156
+ if (args.file_urls && args.file_urls.length > 0) {
157
+ for (const url of args.file_urls) {
158
+ attachments.push({ filename: extractFilename(url), url });
159
+ }
160
+ }
161
+
162
+ const finalAttachments = attachments.length > 0 ? attachments : undefined;
163
+
164
+ if (msgType === "message") {
165
+ const result = await client.sendMessage(args.to, args.text, {
166
+ replyTo: args.reply_to,
167
+ topic: args.topic,
168
+ goal: args.goal,
169
+ mentions: args.mentions,
170
+ attachments: finalAttachments,
171
+ });
172
+ return { ok: true, hub_msg_id: result.hub_msg_id, to: args.to, attachments: finalAttachments };
173
+ }
174
+
175
+ // result/error types — use sendTypedMessage for topic termination
176
+ const result = await client.sendTypedMessage(args.to, msgType, args.text, {
177
+ replyTo: args.reply_to,
178
+ topic: args.topic,
179
+ attachments: finalAttachments,
180
+ });
181
+ return { ok: true, hub_msg_id: result.hub_msg_id, to: args.to, type: msgType, attachments: finalAttachments };
182
+ } catch (err: any) {
183
+ return { error: `Failed to send: ${err.message}` };
184
+ }
185
+ },
186
+ };
187
+ }
188
+
189
+ /**
190
+ * Standalone file upload tool — uploads files to Hub without sending a message.
191
+ */
192
+ export function createUploadTool() {
193
+ return {
194
+ name: "botcord_upload",
195
+ description:
196
+ "Upload one or more local files to BotCord Hub. " +
197
+ "Returns file URLs that can be used later in botcord_send's file_urls parameter. " +
198
+ "Files expire after the Hub's configured TTL (default 1 hour).",
199
+ parameters: {
200
+ type: "object" as const,
201
+ properties: {
202
+ file_paths: {
203
+ type: "array" as const,
204
+ items: { type: "string" as const },
205
+ description: "Local file paths to upload",
206
+ },
207
+ },
208
+ required: ["file_paths"],
209
+ },
210
+ execute: async (toolCallId: any, args: any, signal?: any, onUpdate?: any) => {
211
+ const cfg = getAppConfig();
212
+ if (!cfg) return { error: "No configuration available" };
213
+ const singleAccountError = getSingleAccountModeError(cfg);
214
+ if (singleAccountError) return { error: singleAccountError };
215
+
216
+ const acct = resolveAccountConfig(cfg);
217
+ if (!isAccountConfigured(acct)) {
218
+ return { error: "BotCord is not configured. Set hubUrl, agentId, keyId, and privateKey." };
219
+ }
220
+
221
+ if (!args.file_paths || args.file_paths.length === 0) {
222
+ return { error: "file_paths is required and must not be empty" };
223
+ }
224
+
225
+ try {
226
+ const client = new BotCordClient(acct);
227
+ const uploaded = await uploadLocalFiles(client, args.file_paths);
228
+ return { ok: true, files: uploaded };
229
+ } catch (err: any) {
230
+ return { error: `Upload failed: ${err.message}` };
231
+ }
232
+ },
233
+ };
234
+ }