@botcord/botcord 0.3.6 → 0.3.8-beta.20260415044559
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/index.ts +31 -10
- package/openclaw.plugin.json +1 -1
- package/package.json +2 -1
- package/skills/botcord/SKILL.md +82 -381
- package/skills/botcord/SKILL_PROACTIVE.md +116 -0
- package/skills/botcord/SKILL_SCENARIOS.md +263 -0
- package/skills/botcord/SKILL_SETUP.md +172 -0
- package/skills/botcord-account/SKILL.md +195 -0
- package/skills/botcord-messaging/SKILL.md +188 -0
- package/skills/botcord-payment/SKILL.md +90 -0
- package/skills/botcord-social/SKILL.md +106 -0
- package/src/client.ts +100 -3
- package/src/commands/bind.ts +4 -3
- package/src/commands/healthcheck.ts +2 -11
- package/src/commands/uninstall.ts +129 -0
- package/src/constants.ts +1 -1
- package/src/credentials.ts +26 -168
- package/src/crypto.ts +1 -155
- package/src/dynamic-context.ts +9 -4
- package/src/hub-url.ts +1 -41
- package/src/memory.ts +50 -0
- package/src/session-key.ts +1 -59
- package/src/tools/account.ts +16 -32
- package/src/tools/api.ts +112 -0
- package/src/tools/bind.ts +10 -30
- package/src/tools/contacts.ts +26 -37
- package/src/tools/directory.ts +8 -29
- package/src/tools/messaging.ts +25 -40
- package/src/tools/payment.ts +27 -37
- package/src/tools/register.ts +5 -4
- package/src/tools/reset-credential.ts +6 -5
- package/src/tools/room-context.ts +10 -31
- package/src/tools/rooms.ts +35 -41
- package/src/tools/subscription.ts +27 -38
- package/src/tools/tool-result.ts +10 -3
- package/src/tools/topics.ts +17 -31
- package/src/types.ts +3 -283
- package/src/onboarding-hook.ts +0 -139
package/src/dynamic-context.ts
CHANGED
|
@@ -16,12 +16,13 @@
|
|
|
16
16
|
* out of the box with zero config.
|
|
17
17
|
*/
|
|
18
18
|
import { buildCrossRoomDigest, getSessionRoom } from "./room-context.js";
|
|
19
|
-
import { readWorkingMemory } from "./memory.js";
|
|
19
|
+
import { readWorkingMemory, readOrSeedWorkingMemory } from "./memory.js";
|
|
20
20
|
import { buildWorkingMemoryPrompt } from "./memory-protocol.js";
|
|
21
21
|
import {
|
|
22
22
|
buildBotCordLoopRiskPrompt,
|
|
23
23
|
shouldRunBotCordLoopRiskCheck,
|
|
24
24
|
} from "./loop-risk.js";
|
|
25
|
+
import type { BotCordClient as BotCordClientType } from "./client.js";
|
|
25
26
|
|
|
26
27
|
/**
|
|
27
28
|
* Build the dynamic context for a BotCord session.
|
|
@@ -35,8 +36,10 @@ export async function buildDynamicContext(params: {
|
|
|
35
36
|
prompt?: string;
|
|
36
37
|
messages?: unknown[];
|
|
37
38
|
trigger?: string;
|
|
39
|
+
client?: BotCordClientType;
|
|
40
|
+
credentialsFile?: string;
|
|
38
41
|
}): Promise<string | null> {
|
|
39
|
-
const { sessionKey, channelId, prompt, messages, trigger } = params;
|
|
42
|
+
const { sessionKey, channelId, prompt, messages, trigger, client, credentialsFile } = params;
|
|
40
43
|
|
|
41
44
|
const isOwnerChat = sessionKey === "botcord:owner:main";
|
|
42
45
|
const isBotCordSession = isOwnerChat || !!getSessionRoom(sessionKey);
|
|
@@ -49,9 +52,11 @@ export async function buildDynamicContext(params: {
|
|
|
49
52
|
const digest = await buildCrossRoomDigest(sessionKey);
|
|
50
53
|
if (digest) parts.push(digest);
|
|
51
54
|
|
|
52
|
-
// 2. Working memory
|
|
55
|
+
// 2. Working memory (with lazy seed from API on first read)
|
|
53
56
|
try {
|
|
54
|
-
const wm =
|
|
57
|
+
const wm = client
|
|
58
|
+
? await readOrSeedWorkingMemory({ client, credentialsFile })
|
|
59
|
+
: readWorkingMemory();
|
|
55
60
|
const memoryPrompt = buildWorkingMemoryPrompt({ workingMemory: wm });
|
|
56
61
|
parts.push(memoryPrompt);
|
|
57
62
|
} catch (err: unknown) {
|
package/src/hub-url.ts
CHANGED
|
@@ -1,41 +1 @@
|
|
|
1
|
-
|
|
2
|
-
|
|
3
|
-
function isLoopbackHost(hostname: string): boolean {
|
|
4
|
-
const normalized = hostname.toLowerCase().replace(/^\[(.*)\]$/, "$1");
|
|
5
|
-
return LOOPBACK_HOSTS.has(normalized) || normalized.endsWith(".localhost");
|
|
6
|
-
}
|
|
7
|
-
|
|
8
|
-
export function normalizeAndValidateHubUrl(hubUrl: string): string {
|
|
9
|
-
const trimmed = hubUrl.trim();
|
|
10
|
-
if (!trimmed) {
|
|
11
|
-
throw new Error("BotCord hubUrl is required");
|
|
12
|
-
}
|
|
13
|
-
|
|
14
|
-
let parsed: URL;
|
|
15
|
-
try {
|
|
16
|
-
parsed = new URL(trimmed);
|
|
17
|
-
} catch {
|
|
18
|
-
throw new Error(`BotCord hubUrl must be a valid absolute URL: ${hubUrl}`);
|
|
19
|
-
}
|
|
20
|
-
|
|
21
|
-
if (parsed.protocol !== "https:" && parsed.protocol !== "http:") {
|
|
22
|
-
throw new Error("BotCord hubUrl must use http:// or https://");
|
|
23
|
-
}
|
|
24
|
-
|
|
25
|
-
if (parsed.protocol === "http:" && !isLoopbackHost(parsed.hostname)) {
|
|
26
|
-
throw new Error(
|
|
27
|
-
"BotCord hubUrl must use https:// unless it targets localhost, 127.0.0.1, or ::1 for local development",
|
|
28
|
-
);
|
|
29
|
-
}
|
|
30
|
-
|
|
31
|
-
return trimmed.replace(/\/$/, "");
|
|
32
|
-
}
|
|
33
|
-
|
|
34
|
-
export function buildHubWebSocketUrl(hubUrl: string): string {
|
|
35
|
-
const parsed = new URL(normalizeAndValidateHubUrl(hubUrl));
|
|
36
|
-
parsed.protocol = parsed.protocol === "https:" ? "wss:" : "ws:";
|
|
37
|
-
parsed.search = "";
|
|
38
|
-
parsed.hash = "";
|
|
39
|
-
parsed.pathname = `${parsed.pathname.replace(/\/$/, "")}/hub/ws`;
|
|
40
|
-
return parsed.toString();
|
|
41
|
-
}
|
|
1
|
+
export * from "@botcord/protocol-core";
|
package/src/memory.ts
CHANGED
|
@@ -12,6 +12,8 @@ import path from "node:path";
|
|
|
12
12
|
import os from "node:os";
|
|
13
13
|
import { getBotCordRuntime, getConfig } from "./runtime.js";
|
|
14
14
|
import { resolveAccountConfig } from "./config.js";
|
|
15
|
+
import { isLegacyOnboarded } from "./credentials.js";
|
|
16
|
+
import type { BotCordClient as BotCordClientType } from "./client.js";
|
|
15
17
|
|
|
16
18
|
// ── Types ──────────────────────────────────────────────────────────
|
|
17
19
|
|
|
@@ -197,6 +199,54 @@ export function writeWorkingMemory(
|
|
|
197
199
|
writeJsonFileAtomic(workingMemoryPath(memDir), data);
|
|
198
200
|
}
|
|
199
201
|
|
|
202
|
+
// ── Seed (lazy init) ──────────────────────────────────────────────
|
|
203
|
+
|
|
204
|
+
/**
|
|
205
|
+
* Read working memory with lazy seed from Hub API.
|
|
206
|
+
*
|
|
207
|
+
* If local memory file does not exist:
|
|
208
|
+
* 1. Check the legacy onboardedAt flag → skip seed if set (migration bridge).
|
|
209
|
+
* 2. Fetch default memory from GET /hub/memory/default.
|
|
210
|
+
* 3. Write the seed to local file and return it.
|
|
211
|
+
*
|
|
212
|
+
* This is the ONLY entry point that should be used on the "display path"
|
|
213
|
+
* (i.e., injecting memory into the agent prompt). Write-tool internal reads
|
|
214
|
+
* (read-before-update) should continue using readWorkingMemory() directly.
|
|
215
|
+
*/
|
|
216
|
+
export async function readOrSeedWorkingMemory(params: {
|
|
217
|
+
client: BotCordClientType;
|
|
218
|
+
credentialsFile?: string;
|
|
219
|
+
memDir?: string;
|
|
220
|
+
}): Promise<WorkingMemory | null> {
|
|
221
|
+
const { client, credentialsFile, memDir } = params;
|
|
222
|
+
|
|
223
|
+
// 1. Local file exists → return as-is
|
|
224
|
+
const existing = readWorkingMemory(memDir);
|
|
225
|
+
if (existing) return existing;
|
|
226
|
+
|
|
227
|
+
// 2. Migration bridge: agent already onboarded under legacy system → skip seed
|
|
228
|
+
if (credentialsFile && isLegacyOnboarded(credentialsFile)) return null;
|
|
229
|
+
|
|
230
|
+
// 3. Fetch seed from Hub API
|
|
231
|
+
try {
|
|
232
|
+
const seed = await client.getDefaultMemory();
|
|
233
|
+
if (seed && typeof seed === "object" && seed.version === 2) {
|
|
234
|
+
const wm: WorkingMemory = {
|
|
235
|
+
version: 2,
|
|
236
|
+
goal: typeof seed.goal === "string" ? seed.goal : undefined,
|
|
237
|
+
sections: seed.sections && typeof seed.sections === "object" ? seed.sections as Record<string, string> : {},
|
|
238
|
+
updatedAt: new Date().toISOString(),
|
|
239
|
+
};
|
|
240
|
+
writeWorkingMemory(wm, memDir);
|
|
241
|
+
return wm;
|
|
242
|
+
}
|
|
243
|
+
} catch {
|
|
244
|
+
// Offline / network error → no onboarding guidance, but don't block
|
|
245
|
+
}
|
|
246
|
+
|
|
247
|
+
return null;
|
|
248
|
+
}
|
|
249
|
+
|
|
200
250
|
// ── Room State ─────────────────────────────────────────────────────
|
|
201
251
|
|
|
202
252
|
function roomStatePath(roomId: string, memDir?: string): string {
|
package/src/session-key.ts
CHANGED
|
@@ -1,59 +1 @@
|
|
|
1
|
-
|
|
2
|
-
* Deterministic session key derivation.
|
|
3
|
-
* Must match hub/forward.py build_session_key() exactly.
|
|
4
|
-
*/
|
|
5
|
-
import { createHash } from "node:crypto";
|
|
6
|
-
|
|
7
|
-
// UUID v5 namespace — must match hub/constants.py SESSION_KEY_NAMESPACE
|
|
8
|
-
const SESSION_KEY_NAMESPACE = "d4e8f2a1-3b6c-4d5e-9f0a-1b2c3d4e5f6a";
|
|
9
|
-
|
|
10
|
-
/**
|
|
11
|
-
* RFC 4122 UUID v5 (SHA-1 based, deterministic).
|
|
12
|
-
*/
|
|
13
|
-
function uuidV5(name: string, namespace: string): string {
|
|
14
|
-
// Parse namespace UUID to bytes
|
|
15
|
-
const nsHex = namespace.replace(/-/g, "");
|
|
16
|
-
const nsBytes = Buffer.from(nsHex, "hex");
|
|
17
|
-
|
|
18
|
-
const hash = createHash("sha1")
|
|
19
|
-
.update(nsBytes)
|
|
20
|
-
.update(Buffer.from(name, "utf-8"))
|
|
21
|
-
.digest();
|
|
22
|
-
|
|
23
|
-
// Set version (5) and variant (RFC 4122)
|
|
24
|
-
hash[6] = (hash[6] & 0x0f) | 0x50;
|
|
25
|
-
hash[8] = (hash[8] & 0x3f) | 0x80;
|
|
26
|
-
|
|
27
|
-
const hex = hash.subarray(0, 16).toString("hex");
|
|
28
|
-
return [
|
|
29
|
-
hex.slice(0, 8),
|
|
30
|
-
hex.slice(8, 12),
|
|
31
|
-
hex.slice(12, 16),
|
|
32
|
-
hex.slice(16, 20),
|
|
33
|
-
hex.slice(20, 32),
|
|
34
|
-
].join("-");
|
|
35
|
-
}
|
|
36
|
-
|
|
37
|
-
/**
|
|
38
|
-
* Derive a deterministic sessionKey from room_id, optional topic, and senderId.
|
|
39
|
-
* Same inputs always produce the same key.
|
|
40
|
-
*
|
|
41
|
-
* - Group room: seed from room_id (+ optional topic)
|
|
42
|
-
* - DM with room_id (rm_dm_*): seed from room_id (already unique per DM pair)
|
|
43
|
-
* - DM without room_id: seed from senderId to isolate per-sender conversations
|
|
44
|
-
*/
|
|
45
|
-
export function buildSessionKey(
|
|
46
|
-
roomId?: string,
|
|
47
|
-
topic?: string,
|
|
48
|
-
senderId?: string,
|
|
49
|
-
): string {
|
|
50
|
-
let seed: string;
|
|
51
|
-
if (roomId) {
|
|
52
|
-
seed = topic ? `${roomId}:${topic}` : roomId;
|
|
53
|
-
} else if (senderId) {
|
|
54
|
-
seed = `dm:${senderId}`;
|
|
55
|
-
} else {
|
|
56
|
-
seed = "default";
|
|
57
|
-
}
|
|
58
|
-
return `botcord:${uuidV5(seed, SESSION_KEY_NAMESPACE)}`;
|
|
59
|
-
}
|
|
1
|
+
export * from "@botcord/protocol-core";
|
package/src/tools/account.ts
CHANGED
|
@@ -1,14 +1,8 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* botcord_account — Manage the agent's own identity, profile, and settings.
|
|
3
3
|
*/
|
|
4
|
-
import {
|
|
5
|
-
|
|
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 createAccountTool() {
|
|
14
8
|
return {
|
|
@@ -41,35 +35,25 @@ export function createAccountTool() {
|
|
|
41
35
|
type: "string" as const,
|
|
42
36
|
description: "Message ID — for message_status",
|
|
43
37
|
},
|
|
38
|
+
dry_run: {
|
|
39
|
+
type: "boolean" as const,
|
|
40
|
+
description: "Preview the request without executing. Returns the API call that would be made.",
|
|
41
|
+
},
|
|
44
42
|
},
|
|
45
43
|
required: ["action"],
|
|
46
44
|
},
|
|
47
45
|
execute: async (toolCallId: any, args: any, signal?: any, onUpdate?: any) => {
|
|
48
|
-
|
|
49
|
-
if (!cfg) return { error: "No configuration available" };
|
|
50
|
-
const singleAccountError = getSingleAccountModeError(cfg);
|
|
51
|
-
if (singleAccountError) return { error: singleAccountError };
|
|
52
|
-
|
|
53
|
-
const acct = resolveAccountConfig(cfg);
|
|
54
|
-
if (!isAccountConfigured(acct)) {
|
|
55
|
-
return { error: "BotCord is not configured." };
|
|
56
|
-
}
|
|
57
|
-
|
|
58
|
-
const client = new BotCordClient(acct);
|
|
59
|
-
attachTokenPersistence(client, acct);
|
|
60
|
-
|
|
61
|
-
try {
|
|
46
|
+
return withClient(async (client) => {
|
|
62
47
|
switch (args.action) {
|
|
63
48
|
case "whoami":
|
|
64
49
|
return await client.resolve(client.getAgentId());
|
|
65
50
|
|
|
66
51
|
case "update_profile": {
|
|
67
|
-
if (!args.display_name && !args.bio)
|
|
68
|
-
return { error: "At least one of display_name or bio is required" };
|
|
69
|
-
}
|
|
52
|
+
if (!args.display_name && !args.bio) return validationError("At least one of display_name or bio is required");
|
|
70
53
|
const params: { display_name?: string; bio?: string } = {};
|
|
71
54
|
if (args.display_name) params.display_name = args.display_name;
|
|
72
55
|
if (args.bio) params.bio = args.bio;
|
|
56
|
+
if (args.dry_run) return dryRunResult("PATCH", `/registry/agents/${client.getAgentId()}/profile`, params);
|
|
73
57
|
await client.updateProfile(params);
|
|
74
58
|
return { ok: true, updated: params };
|
|
75
59
|
}
|
|
@@ -77,21 +61,21 @@ export function createAccountTool() {
|
|
|
77
61
|
case "get_policy":
|
|
78
62
|
return await client.getPolicy();
|
|
79
63
|
|
|
80
|
-
case "set_policy":
|
|
81
|
-
if (!args.policy) return
|
|
64
|
+
case "set_policy": {
|
|
65
|
+
if (!args.policy) return validationError("policy is required (open or contacts_only)");
|
|
66
|
+
if (args.dry_run) return dryRunResult("PATCH", `/registry/agents/${client.getAgentId()}/policy`, { message_policy: args.policy });
|
|
82
67
|
await client.setPolicy(args.policy);
|
|
83
68
|
return { ok: true, policy: args.policy };
|
|
69
|
+
}
|
|
84
70
|
|
|
85
71
|
case "message_status":
|
|
86
|
-
if (!args.msg_id) return
|
|
72
|
+
if (!args.msg_id) return validationError("msg_id is required");
|
|
87
73
|
return await client.getMessageStatus(args.msg_id);
|
|
88
74
|
|
|
89
75
|
default:
|
|
90
|
-
return
|
|
76
|
+
return validationError(`Unknown action: ${args.action}`);
|
|
91
77
|
}
|
|
92
|
-
}
|
|
93
|
-
return { error: `Account action failed: ${err.message}` };
|
|
94
|
-
}
|
|
78
|
+
});
|
|
95
79
|
},
|
|
96
80
|
};
|
|
97
81
|
}
|
package/src/tools/api.ts
ADDED
|
@@ -0,0 +1,112 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* botcord_api — Raw Hub API access for advanced use cases.
|
|
3
|
+
*
|
|
4
|
+
* This is the "escape hatch" tool: when the structured tools don't cover
|
|
5
|
+
* a particular endpoint, agents can call the Hub API directly.
|
|
6
|
+
*/
|
|
7
|
+
import { withClient } from "./with-client.js";
|
|
8
|
+
import { validationError } from "./tool-result.js";
|
|
9
|
+
|
|
10
|
+
export function createApiTool() {
|
|
11
|
+
return {
|
|
12
|
+
name: "botcord_api",
|
|
13
|
+
label: "Raw API",
|
|
14
|
+
description:
|
|
15
|
+
"Execute a raw authenticated request against the BotCord Hub API. " +
|
|
16
|
+
"Use this when the structured tools (botcord_send, botcord_rooms, etc.) " +
|
|
17
|
+
"don't cover the endpoint you need. The request is automatically authenticated with your agent's JWT.",
|
|
18
|
+
parameters: {
|
|
19
|
+
type: "object" as const,
|
|
20
|
+
properties: {
|
|
21
|
+
method: {
|
|
22
|
+
type: "string" as const,
|
|
23
|
+
enum: ["GET", "POST", "PUT", "PATCH", "DELETE"],
|
|
24
|
+
description: "HTTP method",
|
|
25
|
+
},
|
|
26
|
+
path: {
|
|
27
|
+
type: "string" as const,
|
|
28
|
+
description: "API path (e.g. /hub/inbox, /registry/agents/ag_xxx)",
|
|
29
|
+
},
|
|
30
|
+
query: {
|
|
31
|
+
type: "object" as const,
|
|
32
|
+
description: "Query parameters as key-value pairs",
|
|
33
|
+
},
|
|
34
|
+
data: {
|
|
35
|
+
type: "object" as const,
|
|
36
|
+
description: "Request body (for POST/PUT/PATCH)",
|
|
37
|
+
},
|
|
38
|
+
confirm: {
|
|
39
|
+
type: "boolean" as const,
|
|
40
|
+
description: "Must be true for write operations (POST/PUT/PATCH/DELETE). Safety gate to prevent unintended mutations.",
|
|
41
|
+
},
|
|
42
|
+
},
|
|
43
|
+
required: ["method", "path"],
|
|
44
|
+
},
|
|
45
|
+
execute: async (toolCallId: any, args: any, signal?: any, onUpdate?: any) => {
|
|
46
|
+
if (!args.method) return validationError("method is required");
|
|
47
|
+
if (!args.path) return validationError("path is required");
|
|
48
|
+
|
|
49
|
+
const method = (args.method as string).toUpperCase();
|
|
50
|
+
const path = args.path as string;
|
|
51
|
+
|
|
52
|
+
// Validate path to prevent SSRF / path traversal.
|
|
53
|
+
const ALLOWED_PREFIXES = ["/hub/", "/registry/", "/wallet/", "/subscriptions/", "/app/"];
|
|
54
|
+
|
|
55
|
+
// Reject absolute URLs (scheme://...) — path must be relative to Hub
|
|
56
|
+
if (/^[a-z][a-z0-9+.-]*:/i.test(path)) {
|
|
57
|
+
return validationError(
|
|
58
|
+
"Absolute URLs are not allowed — provide a path like /hub/inbox",
|
|
59
|
+
"Path traversal and arbitrary URLs are not allowed.",
|
|
60
|
+
);
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
// Reject query strings embedded in path — callers must use the query field.
|
|
64
|
+
// URL normalization strips ?… from path, so silently accepting them would
|
|
65
|
+
// drop parameters the caller intended to send.
|
|
66
|
+
if (path.includes("?")) {
|
|
67
|
+
return validationError(
|
|
68
|
+
"Query strings in path are not allowed — use the query parameter instead",
|
|
69
|
+
'e.g. path: "/hub/search", query: { q: "deploy" }',
|
|
70
|
+
);
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
// Resolve against dummy base to normalize percent-encoded traversal
|
|
74
|
+
// (e.g. /%2e%2e/ → /../ → resolved away by URL constructor)
|
|
75
|
+
let resolvedPath: string;
|
|
76
|
+
try {
|
|
77
|
+
resolvedPath = new URL(path, "http://localhost").pathname;
|
|
78
|
+
} catch {
|
|
79
|
+
return validationError("Invalid path", "Could not parse the provided path as a URL.");
|
|
80
|
+
}
|
|
81
|
+
const normalized = resolvedPath.replace(/\/+/g, "/"); // collapse duplicate slashes
|
|
82
|
+
if (normalized.includes("..") || !ALLOWED_PREFIXES.some((p) => normalized.startsWith(p))) {
|
|
83
|
+
return validationError(
|
|
84
|
+
`path must start with one of: ${ALLOWED_PREFIXES.join(", ")}`,
|
|
85
|
+
"Path traversal and arbitrary URLs are not allowed.",
|
|
86
|
+
);
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
// Write operations require explicit confirmation via confirm param
|
|
90
|
+
if (method !== "GET" && !args.confirm) {
|
|
91
|
+
return {
|
|
92
|
+
ok: false,
|
|
93
|
+
error: {
|
|
94
|
+
type: "validation" as const,
|
|
95
|
+
code: "confirmation_required",
|
|
96
|
+
message: `${method} ${path} is a write operation — set confirm: true to proceed`,
|
|
97
|
+
hint: "Raw API write operations bypass structured tool safeguards. Review the request carefully before confirming.",
|
|
98
|
+
},
|
|
99
|
+
};
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
return withClient(async (client) => {
|
|
103
|
+
// Use the normalized path so the request matches what was validated
|
|
104
|
+
const result = await client.request(method, normalized, {
|
|
105
|
+
body: args.data,
|
|
106
|
+
query: args.query,
|
|
107
|
+
});
|
|
108
|
+
return { response: result };
|
|
109
|
+
});
|
|
110
|
+
},
|
|
111
|
+
};
|
|
112
|
+
}
|
package/src/tools/bind.ts
CHANGED
|
@@ -4,44 +4,26 @@
|
|
|
4
4
|
* [POS]: plugin dashboard 认领执行器,把命令行参数翻译成稳定的绑定请求
|
|
5
5
|
* [PROTOCOL]: 变更时更新此头部,然后检查 README.md
|
|
6
6
|
*/
|
|
7
|
-
import {
|
|
8
|
-
|
|
9
|
-
|
|
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";
|
|
7
|
+
import { withClient } from "./with-client.js";
|
|
8
|
+
import { validationError } from "./tool-result.js";
|
|
9
|
+
import { HubApiError } from "../client.js";
|
|
15
10
|
|
|
16
11
|
/**
|
|
17
12
|
* Shared bind logic used by both the tool and the command.
|
|
18
13
|
*/
|
|
19
14
|
export async function executeBind(
|
|
20
15
|
bindCredential: string,
|
|
21
|
-
)
|
|
22
|
-
|
|
23
|
-
if (!cfg) return { error: "No configuration available" };
|
|
24
|
-
const singleAccountError = getSingleAccountModeError(cfg);
|
|
25
|
-
if (singleAccountError) return { error: singleAccountError };
|
|
26
|
-
|
|
27
|
-
const acct = resolveAccountConfig(cfg);
|
|
28
|
-
if (!isAccountConfigured(acct)) {
|
|
29
|
-
return { error: "BotCord is not configured." };
|
|
30
|
-
}
|
|
31
|
-
|
|
32
|
-
const client = new BotCordClient(acct);
|
|
33
|
-
attachTokenPersistence(client, acct);
|
|
34
|
-
|
|
35
|
-
try {
|
|
16
|
+
) {
|
|
17
|
+
return withClient(async (client) => {
|
|
36
18
|
const agentToken = await client.ensureToken();
|
|
37
19
|
const agentId = client.getAgentId();
|
|
38
20
|
|
|
39
21
|
const resolved = (await client.resolve(agentId)) as Record<string, unknown>;
|
|
40
22
|
const displayName = (resolved.display_name as string) || agentId;
|
|
41
23
|
|
|
42
|
-
const
|
|
24
|
+
const baseUrl = client.getHubUrl().replace(/\/+$/, "");
|
|
43
25
|
|
|
44
|
-
const res = await fetch(`${
|
|
26
|
+
const res = await fetch(`${baseUrl}/api/users/me/agents/bind`, {
|
|
45
27
|
method: "POST",
|
|
46
28
|
headers: { "Content-Type": "application/json" },
|
|
47
29
|
body: JSON.stringify({
|
|
@@ -59,13 +41,11 @@ export async function executeBind(
|
|
|
59
41
|
|
|
60
42
|
if (!res.ok) {
|
|
61
43
|
const msg = body?.error || body?.detail || body?.message || res.statusText;
|
|
62
|
-
|
|
44
|
+
throw new HubApiError(res.status, JSON.stringify({ detail: msg }), "/api/users/me/agents/bind");
|
|
63
45
|
}
|
|
64
46
|
|
|
65
47
|
return { ok: true, ...body };
|
|
66
|
-
}
|
|
67
|
-
return { error: `Bind failed: ${err.message}` };
|
|
68
|
-
}
|
|
48
|
+
});
|
|
69
49
|
}
|
|
70
50
|
|
|
71
51
|
export function createBindTool() {
|
|
@@ -86,7 +66,7 @@ export function createBindTool() {
|
|
|
86
66
|
},
|
|
87
67
|
execute: async (toolCallId: any, args: any, signal?: any, onUpdate?: any) => {
|
|
88
68
|
if (!args.bind_ticket) {
|
|
89
|
-
return
|
|
69
|
+
return validationError("bind_ticket is required");
|
|
90
70
|
}
|
|
91
71
|
return executeBind(args.bind_ticket);
|
|
92
72
|
},
|
package/src/tools/contacts.ts
CHANGED
|
@@ -1,14 +1,8 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* botcord_contacts — Manage social relationships: contacts, requests, blocks.
|
|
3
3
|
*/
|
|
4
|
-
import {
|
|
5
|
-
|
|
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
|
-
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
121
|
+
return validationError(`Unknown action: ${args.action}`);
|
|
131
122
|
}
|
|
132
|
-
}
|
|
133
|
-
return { error: `Contact action failed: ${err.message}` };
|
|
134
|
-
}
|
|
123
|
+
});
|
|
135
124
|
},
|
|
136
125
|
};
|
|
137
126
|
}
|