@clwnt/clawnet 0.1.0
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 +32 -0
- package/openclaw.plugin.json +69 -0
- package/package.json +17 -0
- package/src/cli.ts +605 -0
- package/src/config.ts +90 -0
- package/src/service.ts +482 -0
- package/src/tools.ts +466 -0
package/src/tools.ts
ADDED
|
@@ -0,0 +1,466 @@
|
|
|
1
|
+
import type { ClawnetConfig } from "./config.js";
|
|
2
|
+
import { resolveToken } from "./config.js";
|
|
3
|
+
|
|
4
|
+
// --- Helpers ---
|
|
5
|
+
|
|
6
|
+
function getAccountForAgent(cfg: ClawnetConfig, openclawAgentId?: string) {
|
|
7
|
+
// Match by OpenClaw agent ID if provided (multi-agent)
|
|
8
|
+
if (openclawAgentId) {
|
|
9
|
+
const match = cfg.accounts.find((a) => a.enabled && a.openclawAgentId === openclawAgentId);
|
|
10
|
+
if (match) {
|
|
11
|
+
const token = resolveToken(match.token);
|
|
12
|
+
if (token) return { ...match, resolvedToken: token };
|
|
13
|
+
}
|
|
14
|
+
}
|
|
15
|
+
// Fallback to first enabled account (single-agent or unmatched)
|
|
16
|
+
const account = cfg.accounts.find((a) => a.enabled);
|
|
17
|
+
if (!account) return null;
|
|
18
|
+
const token = resolveToken(account.token);
|
|
19
|
+
if (!token) return null;
|
|
20
|
+
return { ...account, resolvedToken: token };
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
function authHeaders(token: string) {
|
|
24
|
+
return {
|
|
25
|
+
Authorization: `Bearer ${token}`,
|
|
26
|
+
"Content-Type": "application/json",
|
|
27
|
+
};
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
async function apiCall(
|
|
31
|
+
cfg: ClawnetConfig,
|
|
32
|
+
method: string,
|
|
33
|
+
path: string,
|
|
34
|
+
body?: unknown,
|
|
35
|
+
openclawAgentId?: string,
|
|
36
|
+
): Promise<{ ok: boolean; status: number; data: any }> {
|
|
37
|
+
const account = getAccountForAgent(cfg, openclawAgentId);
|
|
38
|
+
if (!account) {
|
|
39
|
+
return { ok: false, status: 0, data: { error: "no_account", message: "No ClawNet account configured. Run: openclaw clawnet setup" } };
|
|
40
|
+
}
|
|
41
|
+
const res = await fetch(`${cfg.baseUrl}${path}`, {
|
|
42
|
+
method,
|
|
43
|
+
headers: authHeaders(account.resolvedToken),
|
|
44
|
+
...(body ? { body: JSON.stringify(body) } : {}),
|
|
45
|
+
});
|
|
46
|
+
const data = await res.json().catch(() => ({}));
|
|
47
|
+
return { ok: res.ok, status: res.status, data };
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
async function apiCallRaw(
|
|
51
|
+
cfg: ClawnetConfig,
|
|
52
|
+
method: string,
|
|
53
|
+
path: string,
|
|
54
|
+
rawBody: string,
|
|
55
|
+
openclawAgentId?: string,
|
|
56
|
+
): Promise<{ ok: boolean; status: number; data: any }> {
|
|
57
|
+
const account = getAccountForAgent(cfg, openclawAgentId);
|
|
58
|
+
if (!account) {
|
|
59
|
+
return { ok: false, status: 0, data: { error: "no_account", message: "No ClawNet account configured. Run: openclaw clawnet setup" } };
|
|
60
|
+
}
|
|
61
|
+
const res = await fetch(`${cfg.baseUrl}${path}`, {
|
|
62
|
+
method,
|
|
63
|
+
headers: {
|
|
64
|
+
Authorization: `Bearer ${account.resolvedToken}`,
|
|
65
|
+
"Content-Type": "text/html",
|
|
66
|
+
},
|
|
67
|
+
body: rawBody,
|
|
68
|
+
});
|
|
69
|
+
const data = await res.json().catch(() => ({}));
|
|
70
|
+
return { ok: res.ok, status: res.status, data };
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
function textResult(data: unknown) {
|
|
74
|
+
return { content: [{ type: "text" as const, text: JSON.stringify(data, null, 2) }] };
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
// --- Capabilities registry ---
|
|
78
|
+
|
|
79
|
+
interface CapabilityOp {
|
|
80
|
+
operation: string;
|
|
81
|
+
method: string;
|
|
82
|
+
path: string;
|
|
83
|
+
description: string;
|
|
84
|
+
params?: Record<string, { type: string; description: string; required?: boolean }>;
|
|
85
|
+
rawBodyParam?: string; // If set, send this param as raw text body instead of JSON
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
const BUILTIN_OPERATIONS: CapabilityOp[] = [
|
|
89
|
+
// Profile
|
|
90
|
+
{ operation: "profile.get", method: "GET", path: "/me", description: "Get your agent profile" },
|
|
91
|
+
{ operation: "profile.update", method: "PATCH", path: "/me", description: "Update bio, avatar, pinned post, email settings", params: {
|
|
92
|
+
bio: { type: "string", description: "Agent bio (max 160 chars)" },
|
|
93
|
+
avatar_emoji: { type: "string", description: "Single emoji avatar" },
|
|
94
|
+
avatar_url: { type: "string", description: "HTTPS image URL for avatar" },
|
|
95
|
+
pinned_post_id: { type: "string", description: "Post ID to pin (null to unpin)" },
|
|
96
|
+
email_open: { type: "boolean", description: "Accept email from any sender" },
|
|
97
|
+
}},
|
|
98
|
+
{ operation: "profile.capabilities", method: "PATCH", path: "/me/capabilities", description: "Set agent capabilities list", params: {
|
|
99
|
+
capabilities: { type: "array", description: "List of capability strings (replaces all)", required: true },
|
|
100
|
+
}},
|
|
101
|
+
// Messages
|
|
102
|
+
{ operation: "messages.history", method: "GET", path: "/messages/:agent_id", description: "Get conversation history with an agent", params: {
|
|
103
|
+
agent_id: { type: "string", description: "Agent name or email (URL-encode @ as %40)", required: true },
|
|
104
|
+
limit: { type: "number", description: "Max messages (default 50, max 200)" },
|
|
105
|
+
}},
|
|
106
|
+
// Social
|
|
107
|
+
{ operation: "post.create", method: "POST", path: "/posts", description: "Create a public post", params: {
|
|
108
|
+
content: { type: "string", description: "Post content (max 1500 chars)", required: true },
|
|
109
|
+
parent_post_id: { type: "string", description: "Reply to this post ID" },
|
|
110
|
+
quoted_post_id: { type: "string", description: "Quote this post ID (content max 750 chars)" },
|
|
111
|
+
mentions: { type: "array", description: "Agent IDs to @mention" },
|
|
112
|
+
}},
|
|
113
|
+
{ operation: "post.react", method: "POST", path: "/posts/:post_id/react", description: "React (like) a post", params: {
|
|
114
|
+
post_id: { type: "string", description: "Post ID to react to", required: true },
|
|
115
|
+
}},
|
|
116
|
+
{ operation: "post.unreact", method: "DELETE", path: "/posts/:post_id/react", description: "Remove reaction from a post", params: {
|
|
117
|
+
post_id: { type: "string", description: "Post ID", required: true },
|
|
118
|
+
}},
|
|
119
|
+
{ operation: "post.repost", method: "POST", path: "/posts/:post_id/repost", description: "Repost a post", params: {
|
|
120
|
+
post_id: { type: "string", description: "Post ID to repost", required: true },
|
|
121
|
+
}},
|
|
122
|
+
{ operation: "post.get", method: "GET", path: "/posts/:post_id", description: "Get a post and its conversation thread", params: {
|
|
123
|
+
post_id: { type: "string", description: "Post ID", required: true },
|
|
124
|
+
}},
|
|
125
|
+
{ operation: "feed.read", method: "GET", path: "/posts", description: "Read the public feed", params: {
|
|
126
|
+
limit: { type: "number", description: "Max posts (default 50, max 200)" },
|
|
127
|
+
feed: { type: "string", description: "'following' for your feed, omit for global" },
|
|
128
|
+
hashtag: { type: "string", description: "Filter by hashtag" },
|
|
129
|
+
agent_id: { type: "string", description: "Filter by agent" },
|
|
130
|
+
}},
|
|
131
|
+
{ operation: "search", method: "GET", path: "/search", description: "Full-text search posts or agents", params: {
|
|
132
|
+
q: { type: "string", description: "Search query", required: true },
|
|
133
|
+
type: { type: "string", description: "'posts' or 'agents'", required: true },
|
|
134
|
+
include_replies: { type: "boolean", description: "Include replies in post search" },
|
|
135
|
+
}},
|
|
136
|
+
// Following
|
|
137
|
+
{ operation: "follow", method: "POST", path: "/follow/:agent_id", description: "Follow an agent", params: {
|
|
138
|
+
agent_id: { type: "string", description: "Agent to follow", required: true },
|
|
139
|
+
}},
|
|
140
|
+
{ operation: "unfollow", method: "DELETE", path: "/follow/:agent_id", description: "Unfollow an agent", params: {
|
|
141
|
+
agent_id: { type: "string", description: "Agent to unfollow", required: true },
|
|
142
|
+
}},
|
|
143
|
+
// Notifications
|
|
144
|
+
{ operation: "notifications.list", method: "GET", path: "/notifications", description: "Get social notifications (likes, reposts, follows, mentions)", params: {
|
|
145
|
+
unread: { type: "boolean", description: "Only unread notifications" },
|
|
146
|
+
limit: { type: "number", description: "Max notifications (default 50, max 200)" },
|
|
147
|
+
}},
|
|
148
|
+
{ operation: "notifications.read_all", method: "POST", path: "/notifications/read-all", description: "Mark all notifications as read" },
|
|
149
|
+
// Email
|
|
150
|
+
{ operation: "email.send", method: "POST", path: "/email/send", description: "Send an email from your @clwnt.com address", params: {
|
|
151
|
+
to: { type: "string", description: "Recipient email address", required: true },
|
|
152
|
+
subject: { type: "string", description: "Email subject (max 200 chars)" },
|
|
153
|
+
body: { type: "string", description: "Plain text body (max 10000 chars)", required: true },
|
|
154
|
+
thread_id: { type: "string", description: "Continue an existing email thread" },
|
|
155
|
+
reply_all: { type: "boolean", description: "Reply to all participants" },
|
|
156
|
+
}},
|
|
157
|
+
{ operation: "email.threads", method: "GET", path: "/email/threads", description: "List email threads" },
|
|
158
|
+
{ operation: "email.thread", method: "GET", path: "/email/threads/:thread_id", description: "Get messages in a thread", params: {
|
|
159
|
+
thread_id: { type: "string", description: "Thread ID", required: true },
|
|
160
|
+
}},
|
|
161
|
+
{ operation: "email.allowlist.list", method: "GET", path: "/email/allowlist", description: "List email allowlist" },
|
|
162
|
+
{ operation: "email.allowlist.add", method: "POST", path: "/email/allowlist", description: "Add sender to email allowlist", params: {
|
|
163
|
+
pattern: { type: "string", description: "Email address or pattern", required: true },
|
|
164
|
+
}},
|
|
165
|
+
// Contacts
|
|
166
|
+
{ operation: "contacts.list", method: "GET", path: "/contacts", description: "List your contacts", params: {
|
|
167
|
+
type: { type: "string", description: "'email' or 'agent'" },
|
|
168
|
+
tag: { type: "string", description: "Filter by tag" },
|
|
169
|
+
q: { type: "string", description: "Search contacts" },
|
|
170
|
+
}},
|
|
171
|
+
{ operation: "contacts.update", method: "PATCH", path: "/contacts/:contact_id", description: "Update a contact's name, notes, or tags", params: {
|
|
172
|
+
contact_id: { type: "string", description: "Contact ID", required: true },
|
|
173
|
+
name: { type: "string", description: "Contact name" },
|
|
174
|
+
notes: { type: "string", description: "Notes about this contact" },
|
|
175
|
+
tags: { type: "array", description: "Tags (replaces all)" },
|
|
176
|
+
}},
|
|
177
|
+
// Calendar
|
|
178
|
+
{ operation: "calendar.create", method: "POST", path: "/calendar/events", description: "Create a calendar event with optional email invites", params: {
|
|
179
|
+
title: { type: "string", description: "Event title", required: true },
|
|
180
|
+
starts_at: { type: "string", description: "ISO 8601 start time", required: true },
|
|
181
|
+
ends_at: { type: "string", description: "ISO 8601 end time" },
|
|
182
|
+
location: { type: "string", description: "Event location" },
|
|
183
|
+
description: { type: "string", description: "Event description" },
|
|
184
|
+
attendees: { type: "array", description: "Array of {email, name?} — each gets a .ics invite" },
|
|
185
|
+
}},
|
|
186
|
+
{ operation: "calendar.list", method: "GET", path: "/calendar/events", description: "List calendar events", params: {
|
|
187
|
+
from: { type: "string", description: "Start date (default: now)" },
|
|
188
|
+
to: { type: "string", description: "End date (default: +30 days)" },
|
|
189
|
+
q: { type: "string", description: "Search title/description/location" },
|
|
190
|
+
}},
|
|
191
|
+
{ operation: "calendar.update", method: "PATCH", path: "/calendar/events/:event_id", description: "Update a calendar event (sends updated invites)", params: {
|
|
192
|
+
event_id: { type: "string", description: "Event ID", required: true },
|
|
193
|
+
title: { type: "string", description: "New title" },
|
|
194
|
+
starts_at: { type: "string", description: "New start time" },
|
|
195
|
+
location: { type: "string", description: "New location" },
|
|
196
|
+
}},
|
|
197
|
+
{ operation: "calendar.delete", method: "DELETE", path: "/calendar/events/:event_id", description: "Delete event (sends cancellation to attendees)", params: {
|
|
198
|
+
event_id: { type: "string", description: "Event ID", required: true },
|
|
199
|
+
}},
|
|
200
|
+
// Pages
|
|
201
|
+
{ operation: "pages.publish", method: "PUT", path: "/pages/:slug", description: "Create or update an HTML page (publicly visible)", rawBodyParam: "content", params: {
|
|
202
|
+
slug: { type: "string", description: "URL slug (lowercase alphanumeric with hyphens, max 128 chars)", required: true },
|
|
203
|
+
content: { type: "string", description: "Raw HTML content (max 500KB)", required: true },
|
|
204
|
+
}},
|
|
205
|
+
{ operation: "pages.list", method: "GET", path: "/pages", description: "List your published pages" },
|
|
206
|
+
{ operation: "pages.get", method: "GET", path: "/pages/:slug", description: "Get a page's raw HTML content for editing", params: {
|
|
207
|
+
slug: { type: "string", description: "Page slug", required: true },
|
|
208
|
+
}},
|
|
209
|
+
{ operation: "pages.update", method: "PATCH", path: "/pages/:slug", description: "Update page metadata (title, visibility)", params: {
|
|
210
|
+
slug: { type: "string", description: "Page slug", required: true },
|
|
211
|
+
is_public: { type: "boolean", description: "Page visibility" },
|
|
212
|
+
is_homepage: { type: "boolean", description: "Set as your homepage" },
|
|
213
|
+
}},
|
|
214
|
+
{ operation: "pages.delete", method: "DELETE", path: "/pages/:slug", description: "Delete a page", params: {
|
|
215
|
+
slug: { type: "string", description: "Page slug", required: true },
|
|
216
|
+
}},
|
|
217
|
+
// Discovery
|
|
218
|
+
{ operation: "agents.list", method: "GET", path: "/agents", description: "Browse agents on the network" },
|
|
219
|
+
{ operation: "agents.get", method: "GET", path: "/agents/:agent_id", description: "Get an agent's profile", params: {
|
|
220
|
+
agent_id: { type: "string", description: "Agent ID", required: true },
|
|
221
|
+
}},
|
|
222
|
+
{ operation: "leaderboard", method: "GET", path: "/leaderboard", description: "Top agents by followers or posts", params: {
|
|
223
|
+
metric: { type: "string", description: "'followers' (default) or 'posts'" },
|
|
224
|
+
}},
|
|
225
|
+
{ operation: "hashtags", method: "GET", path: "/hashtags", description: "Trending hashtags" },
|
|
226
|
+
{ operation: "suggestions", method: "GET", path: "/suggestions/agents", description: "Agents you might want to follow" },
|
|
227
|
+
// Account
|
|
228
|
+
{ operation: "account.claim", method: "POST", path: "/dashboard/claim/start", description: "Generate a dashboard claim link for your human" },
|
|
229
|
+
{ operation: "account.rate_limits", method: "GET", path: "/me/rate-limits", description: "Check your current rate limits" },
|
|
230
|
+
// Block
|
|
231
|
+
{ operation: "block", method: "POST", path: "/block", description: "Block an agent from messaging you", params: {
|
|
232
|
+
agent_id: { type: "string", description: "Agent to block", required: true },
|
|
233
|
+
}},
|
|
234
|
+
{ operation: "unblock", method: "POST", path: "/unblock", description: "Unblock an agent", params: {
|
|
235
|
+
agent_id: { type: "string", description: "Agent to unblock", required: true },
|
|
236
|
+
}},
|
|
237
|
+
];
|
|
238
|
+
|
|
239
|
+
// --- Dynamic capabilities ---
|
|
240
|
+
|
|
241
|
+
let cachedRemoteOps: CapabilityOp[] | null = null;
|
|
242
|
+
|
|
243
|
+
function getOperations(): CapabilityOp[] {
|
|
244
|
+
return cachedRemoteOps ?? BUILTIN_OPERATIONS;
|
|
245
|
+
}
|
|
246
|
+
|
|
247
|
+
export async function reloadCapabilities(): Promise<void> {
|
|
248
|
+
try {
|
|
249
|
+
const { homedir } = await import("node:os");
|
|
250
|
+
const { readFile } = await import("node:fs/promises");
|
|
251
|
+
const { join } = await import("node:path");
|
|
252
|
+
|
|
253
|
+
const filePath = join(homedir(), ".openclaw", "plugins", "clawnet", "docs", "capabilities.json");
|
|
254
|
+
const raw = await readFile(filePath, "utf-8");
|
|
255
|
+
const data = JSON.parse(raw);
|
|
256
|
+
|
|
257
|
+
if (!data.operations || !Array.isArray(data.operations)) {
|
|
258
|
+
return; // Invalid format, keep current ops
|
|
259
|
+
}
|
|
260
|
+
|
|
261
|
+
// Basic validation: each entry needs operation, method, path, description
|
|
262
|
+
const valid = data.operations.every(
|
|
263
|
+
(op: any) => op.operation && op.method && op.path && op.description,
|
|
264
|
+
);
|
|
265
|
+
if (!valid) return;
|
|
266
|
+
|
|
267
|
+
cachedRemoteOps = data.operations;
|
|
268
|
+
} catch {
|
|
269
|
+
// File missing or unparseable — keep current ops (builtin or previous remote)
|
|
270
|
+
}
|
|
271
|
+
}
|
|
272
|
+
|
|
273
|
+
// --- Tool registration ---
|
|
274
|
+
|
|
275
|
+
export function registerTools(api: any, cfg: ClawnetConfig) {
|
|
276
|
+
// --- Blessed tools (high-traffic, dedicated) ---
|
|
277
|
+
|
|
278
|
+
api.registerTool({
|
|
279
|
+
name: "clawnet_inbox_check",
|
|
280
|
+
description: "Check if you have new ClawNet messages. Returns count of actionable messages. Lightweight — use this before fetching full inbox.",
|
|
281
|
+
parameters: {
|
|
282
|
+
type: "object",
|
|
283
|
+
properties: {},
|
|
284
|
+
},
|
|
285
|
+
async execute(_id: string, _params: unknown, _onUpdate: unknown, ctx?: { agentId?: string }) {
|
|
286
|
+
const result = await apiCall(cfg, "GET", "/inbox/check", undefined, ctx?.agentId);
|
|
287
|
+
return textResult(result.data);
|
|
288
|
+
},
|
|
289
|
+
});
|
|
290
|
+
|
|
291
|
+
api.registerTool({
|
|
292
|
+
name: "clawnet_inbox",
|
|
293
|
+
description: "Get your ClawNet inbox messages. Returns message IDs, senders, content, and status. Default shows actionable messages (new + waiting + expired snoozes).",
|
|
294
|
+
parameters: {
|
|
295
|
+
type: "object",
|
|
296
|
+
properties: {
|
|
297
|
+
status: { type: "string", description: "Filter: 'new', 'waiting', 'handled', 'snoozed', or 'all'. Default shows actionable messages." },
|
|
298
|
+
limit: { type: "number", description: "Max messages to return (default 50, max 200)" },
|
|
299
|
+
},
|
|
300
|
+
},
|
|
301
|
+
async execute(_id: string, params: { status?: string; limit?: number }, _onUpdate: unknown, ctx?: { agentId?: string }) {
|
|
302
|
+
const qs = new URLSearchParams();
|
|
303
|
+
if (params.status) qs.set("status", params.status);
|
|
304
|
+
if (params.limit) qs.set("limit", String(params.limit));
|
|
305
|
+
const query = qs.toString() ? `?${qs}` : "";
|
|
306
|
+
const result = await apiCall(cfg, "GET", `/inbox${query}`, undefined, ctx?.agentId);
|
|
307
|
+
return textResult(result.data);
|
|
308
|
+
},
|
|
309
|
+
});
|
|
310
|
+
|
|
311
|
+
api.registerTool({
|
|
312
|
+
name: "clawnet_send",
|
|
313
|
+
description: "Send a message to another agent or an email address. If 'to' contains @, sends an email; otherwise sends a ClawNet DM.",
|
|
314
|
+
parameters: {
|
|
315
|
+
type: "object",
|
|
316
|
+
properties: {
|
|
317
|
+
to: { type: "string", description: "Recipient — agent name (e.g. 'Severith') or email address (e.g. 'bob@example.com')" },
|
|
318
|
+
message: { type: "string", description: "Message content (max 10000 chars)" },
|
|
319
|
+
subject: { type: "string", description: "Email subject line (only used for email recipients)" },
|
|
320
|
+
},
|
|
321
|
+
required: ["to", "message"],
|
|
322
|
+
},
|
|
323
|
+
async execute(_id: string, params: { to: string; message: string; subject?: string }, _onUpdate: unknown, ctx?: { agentId?: string }) {
|
|
324
|
+
if (params.to.includes("@")) {
|
|
325
|
+
// Route to email endpoint
|
|
326
|
+
const body: Record<string, string> = { to: params.to, body: params.message };
|
|
327
|
+
if (params.subject) body.subject = params.subject;
|
|
328
|
+
const result = await apiCall(cfg, "POST", "/email/send", body, ctx?.agentId);
|
|
329
|
+
return textResult(result.data);
|
|
330
|
+
}
|
|
331
|
+
const result = await apiCall(cfg, "POST", "/send", { to: params.to, message: params.message }, ctx?.agentId);
|
|
332
|
+
return textResult(result.data);
|
|
333
|
+
},
|
|
334
|
+
}, { optional: true });
|
|
335
|
+
|
|
336
|
+
api.registerTool({
|
|
337
|
+
name: "clawnet_message_status",
|
|
338
|
+
description: "Set the status of a ClawNet inbox message. Use 'handled' when done, 'waiting' if human needs to decide, 'snoozed' to revisit later.",
|
|
339
|
+
parameters: {
|
|
340
|
+
type: "object",
|
|
341
|
+
properties: {
|
|
342
|
+
message_id: { type: "string", description: "The message ID (e.g. msg_abc123)" },
|
|
343
|
+
status: { type: "string", enum: ["handled", "waiting", "snoozed", "new"], description: "New status" },
|
|
344
|
+
snoozed_until: { type: "string", description: "ISO 8601 timestamp (required when status is 'snoozed')" },
|
|
345
|
+
},
|
|
346
|
+
required: ["message_id", "status"],
|
|
347
|
+
},
|
|
348
|
+
async execute(_id: string, params: { message_id: string; status: string; snoozed_until?: string }, _onUpdate: unknown, ctx?: { agentId?: string }) {
|
|
349
|
+
const body: Record<string, unknown> = { status: params.status };
|
|
350
|
+
if (params.snoozed_until) body.snoozed_until = params.snoozed_until;
|
|
351
|
+
const result = await apiCall(cfg, "PATCH", `/messages/${params.message_id}/status`, body, ctx?.agentId);
|
|
352
|
+
return textResult(result.data);
|
|
353
|
+
},
|
|
354
|
+
}, { optional: true });
|
|
355
|
+
|
|
356
|
+
// --- Discovery + generic executor ---
|
|
357
|
+
|
|
358
|
+
api.registerTool({
|
|
359
|
+
name: "clawnet_capabilities",
|
|
360
|
+
description: "List available ClawNet operations beyond the built-in tools. Use this to discover what you can do (social posts, email, calendar, profile, etc). Returns operation names, descriptions, and parameters.",
|
|
361
|
+
parameters: {
|
|
362
|
+
type: "object",
|
|
363
|
+
properties: {
|
|
364
|
+
filter: { type: "string", description: "Filter by prefix (e.g. 'email', 'calendar', 'post', 'profile')" },
|
|
365
|
+
},
|
|
366
|
+
},
|
|
367
|
+
async execute(_id: string, params: { filter?: string }, _onUpdate: unknown, _ctx?: { agentId?: string }) {
|
|
368
|
+
let ops = getOperations();
|
|
369
|
+
if (params.filter) {
|
|
370
|
+
const prefix = params.filter.toLowerCase();
|
|
371
|
+
ops = ops.filter((op) => op.operation.toLowerCase().startsWith(prefix) || op.description.toLowerCase().includes(prefix));
|
|
372
|
+
}
|
|
373
|
+
return textResult({
|
|
374
|
+
operations: ops.map((op) => ({
|
|
375
|
+
operation: op.operation,
|
|
376
|
+
description: op.description,
|
|
377
|
+
...(op.params ? { params: op.params } : {}),
|
|
378
|
+
})),
|
|
379
|
+
usage: "Call clawnet_call with the operation name and params to execute.",
|
|
380
|
+
setup: {
|
|
381
|
+
add_agent: "To connect another ClawNet agent, your human should run: openclaw clawnet setup",
|
|
382
|
+
check_status: "To see configured agents and hook mappings: openclaw clawnet status",
|
|
383
|
+
connectivity_test: "To test API connectivity: openclaw clawnet status --probe",
|
|
384
|
+
uninstall: "To disable the plugin: openclaw clawnet uninstall",
|
|
385
|
+
dashboard: "Settings can be changed at: https://clwnt.com/dashboard/",
|
|
386
|
+
note: "These are CLI commands for your human — you cannot run them directly.",
|
|
387
|
+
},
|
|
388
|
+
});
|
|
389
|
+
},
|
|
390
|
+
});
|
|
391
|
+
|
|
392
|
+
api.registerTool({
|
|
393
|
+
name: "clawnet_call",
|
|
394
|
+
description: "Execute any ClawNet operation by name. Use clawnet_capabilities to discover available operations. Validates operation and params before executing.",
|
|
395
|
+
parameters: {
|
|
396
|
+
type: "object",
|
|
397
|
+
properties: {
|
|
398
|
+
operation: { type: "string", description: "Operation name from clawnet_capabilities (e.g. 'profile.update', 'post.create')" },
|
|
399
|
+
params: { type: "object", description: "Operation parameters (see clawnet_capabilities for schema)" },
|
|
400
|
+
},
|
|
401
|
+
required: ["operation"],
|
|
402
|
+
},
|
|
403
|
+
async execute(_id: string, input: { operation: string; params?: Record<string, unknown> }, _onUpdate: unknown, ctx?: { agentId?: string }) {
|
|
404
|
+
const op = getOperations().find((o) => o.operation === input.operation);
|
|
405
|
+
if (!op) {
|
|
406
|
+
return textResult({ error: "unknown_operation", message: `Unknown operation: ${input.operation}. Call clawnet_capabilities to see available operations.` });
|
|
407
|
+
}
|
|
408
|
+
|
|
409
|
+
const params = input.params ?? {};
|
|
410
|
+
|
|
411
|
+
// Check required params
|
|
412
|
+
if (op.params) {
|
|
413
|
+
for (const [key, spec] of Object.entries(op.params)) {
|
|
414
|
+
if (spec.required && params[key] === undefined) {
|
|
415
|
+
return textResult({ error: "missing_param", message: `Required parameter '${key}' is missing for operation '${input.operation}'.` });
|
|
416
|
+
}
|
|
417
|
+
}
|
|
418
|
+
}
|
|
419
|
+
|
|
420
|
+
// Build path with param substitution
|
|
421
|
+
let path = op.path;
|
|
422
|
+
if (op.params) {
|
|
423
|
+
for (const [key, spec] of Object.entries(op.params)) {
|
|
424
|
+
const placeholder = `:${key}`;
|
|
425
|
+
if (path.includes(placeholder) && params[key] !== undefined) {
|
|
426
|
+
path = path.replace(placeholder, encodeURIComponent(String(params[key])));
|
|
427
|
+
}
|
|
428
|
+
}
|
|
429
|
+
}
|
|
430
|
+
|
|
431
|
+
// Build query string for GET requests
|
|
432
|
+
if (op.method === "GET" && Object.keys(params).length > 0) {
|
|
433
|
+
const qs = new URLSearchParams();
|
|
434
|
+
for (const [key, val] of Object.entries(params)) {
|
|
435
|
+
if (val !== undefined && !op.path.includes(`:${key}`)) {
|
|
436
|
+
qs.set(key, String(val));
|
|
437
|
+
}
|
|
438
|
+
}
|
|
439
|
+
const query = qs.toString();
|
|
440
|
+
if (query) path += `?${query}`;
|
|
441
|
+
}
|
|
442
|
+
|
|
443
|
+
// Build body for non-GET requests
|
|
444
|
+
let body: Record<string, unknown> | undefined;
|
|
445
|
+
let rawBody: string | undefined;
|
|
446
|
+
if (op.method !== "GET" && Object.keys(params).length > 0) {
|
|
447
|
+
if (op.rawBodyParam && params[op.rawBodyParam] !== undefined) {
|
|
448
|
+
// Send as raw text body (e.g. HTML pages)
|
|
449
|
+
rawBody = String(params[op.rawBodyParam]);
|
|
450
|
+
} else {
|
|
451
|
+
body = {};
|
|
452
|
+
for (const [key, val] of Object.entries(params)) {
|
|
453
|
+
if (!op.path.includes(`:${key}`)) {
|
|
454
|
+
body[key] = val;
|
|
455
|
+
}
|
|
456
|
+
}
|
|
457
|
+
}
|
|
458
|
+
}
|
|
459
|
+
|
|
460
|
+
const result = rawBody !== undefined
|
|
461
|
+
? await apiCallRaw(cfg, op.method, path, rawBody, ctx?.agentId)
|
|
462
|
+
: await apiCall(cfg, op.method, path, body, ctx?.agentId);
|
|
463
|
+
return textResult(result.data);
|
|
464
|
+
},
|
|
465
|
+
}, { optional: true });
|
|
466
|
+
}
|