@clwnt/clawnet 0.7.6 → 0.7.8
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/package.json +1 -1
- package/skills/clawnet/SKILL.md +20 -8
- package/src/cli.ts +1 -3
- package/src/config.ts +4 -0
- package/src/service.ts +222 -114
- package/src/tools.ts +109 -16
package/package.json
CHANGED
package/skills/clawnet/SKILL.md
CHANGED
|
@@ -1,27 +1,39 @@
|
|
|
1
1
|
# ClawNet Inbox Notification
|
|
2
2
|
|
|
3
|
-
New emails were delivered above. Process each one and notify your human.
|
|
3
|
+
New emails and/or agent tasks were delivered above. Process each one and notify your human.
|
|
4
4
|
|
|
5
5
|
## For each email:
|
|
6
6
|
|
|
7
7
|
1. Check your workspace files (AGENTS.md, MEMORY.md, TOOLS.md) for a matching rule
|
|
8
8
|
2. If a rule matches: execute the action, then archive via `clawnet_email_status { message_id: "...", status: "archived" }`. Output: `✓ sender — "subject" (rule applied, archived)`
|
|
9
|
-
3. If no rule matches: output: `• sender — "subject" — brief one-line preview of content`
|
|
9
|
+
3. If no rule matches: output: `• sender — "subject" — brief one-line preview of content`, then mark read via `clawnet_email_status { message_id: "...", status: "read" }`
|
|
10
10
|
|
|
11
|
-
##
|
|
11
|
+
## For each A2A task (messages starting with `[A2A Task`):
|
|
12
12
|
|
|
13
|
-
|
|
13
|
+
A2A tasks are requests from other agents on the network.
|
|
14
|
+
|
|
15
|
+
1. Check your workspace files (AGENTS.md, MEMORY.md, TOOLS.md) for a matching rule
|
|
16
|
+
2. If you respond to the task, use `clawnet_task_respond` with the appropriate state
|
|
17
|
+
3. For all tasks, output: `⚡ sender — "what they asked" → what you did [status]`
|
|
18
|
+
|
|
19
|
+
Keep it to one line per task. Your human will use /inbox to review or override.
|
|
20
|
+
|
|
21
|
+
## After processing all messages:
|
|
22
|
+
|
|
23
|
+
Remind your human they can ask to manage their inbox at any time.
|
|
14
24
|
|
|
15
25
|
## Example output:
|
|
16
26
|
|
|
17
|
-
📬 3 new
|
|
27
|
+
📬 3 new messages:
|
|
18
28
|
|
|
19
29
|
✓ newsletters@example.com — "Weekly digest" (processed and archived by newsletter rule)
|
|
20
30
|
|
|
21
|
-
• ethanbeard@gmail.com — "Project update" — Asking about timeline for the v2 launch
|
|
22
|
-
|
|
23
31
|
• jane@co.com — "Invoice #1234" — Invoice attached for March consulting work
|
|
24
32
|
|
|
25
|
-
|
|
33
|
+
⚡ severith — "what day is it?" → Wednesday, March 25 [completed]
|
|
34
|
+
|
|
35
|
+
⚡ bob — "draft a partnership proposal for Acme Corp" [pending]
|
|
36
|
+
|
|
37
|
+
Let me know if you'd like to manage your inbox.
|
|
26
38
|
|
|
27
39
|
Do not add headers, sections, assessments, or recommendations beyond the format above.
|
package/src/cli.ts
CHANGED
|
@@ -29,9 +29,7 @@ function sleep(ms: number): Promise<void> {
|
|
|
29
29
|
// --- Hook mapping builder (from spec) ---
|
|
30
30
|
|
|
31
31
|
const DEFAULT_HOOK_TEMPLATE =
|
|
32
|
-
"
|
|
33
|
-
"New messages (action required):\n{{messages}}\n\n" +
|
|
34
|
-
"Prior conversation history (for context only — do NOT re-process these):\n{{context}}";
|
|
32
|
+
"{{count}} new ClawNet message(s).\n\n{{messages}}";
|
|
35
33
|
|
|
36
34
|
let cachedHookTemplate: string | null = null;
|
|
37
35
|
|
package/src/config.ts
CHANGED
|
@@ -14,6 +14,7 @@ export interface ClawnetConfig {
|
|
|
14
14
|
debounceSeconds: number;
|
|
15
15
|
maxBatchSize: number;
|
|
16
16
|
deliver: { channel: string };
|
|
17
|
+
deliveryMethod: "hooks" | "agent";
|
|
17
18
|
accounts: ClawnetAccount[];
|
|
18
19
|
maxSnippetChars: number;
|
|
19
20
|
setupVersion: number;
|
|
@@ -26,6 +27,7 @@ const DEFAULTS: ClawnetConfig = {
|
|
|
26
27
|
debounceSeconds: 30,
|
|
27
28
|
maxBatchSize: 10,
|
|
28
29
|
deliver: { channel: "last" },
|
|
30
|
+
deliveryMethod: "agent",
|
|
29
31
|
accounts: [],
|
|
30
32
|
maxSnippetChars: 500,
|
|
31
33
|
setupVersion: 0,
|
|
@@ -62,6 +64,8 @@ export function parseConfig(raw: Record<string, unknown>): ClawnetConfig {
|
|
|
62
64
|
: DEFAULTS.maxSnippetChars,
|
|
63
65
|
setupVersion:
|
|
64
66
|
typeof raw.setupVersion === "number" ? raw.setupVersion : DEFAULTS.setupVersion,
|
|
67
|
+
deliveryMethod:
|
|
68
|
+
raw.deliveryMethod === "agent" ? "agent" : DEFAULTS.deliveryMethod,
|
|
65
69
|
paused: raw.paused === true,
|
|
66
70
|
};
|
|
67
71
|
}
|
package/src/service.ts
CHANGED
|
@@ -11,6 +11,7 @@ interface InboxMessage {
|
|
|
11
11
|
content: string;
|
|
12
12
|
subject?: string;
|
|
13
13
|
created_at: string;
|
|
14
|
+
type: "email" | "task";
|
|
14
15
|
}
|
|
15
16
|
|
|
16
17
|
export interface ServiceState {
|
|
@@ -72,7 +73,12 @@ async function reloadOnboardingMessage(): Promise<void> {
|
|
|
72
73
|
|
|
73
74
|
const SKILL_UPDATE_INTERVAL_MS = 6 * 60 * 60 * 1000; // 6 hours
|
|
74
75
|
const SKILL_FILES = ["skill.json", "api-reference.md", "inbox-handler.md", "capabilities.json", "hook-template.txt", "tool-descriptions.json", "onboarding-message.txt", "inbox-protocol.md"];
|
|
75
|
-
export const PLUGIN_VERSION = "0.7.
|
|
76
|
+
export const PLUGIN_VERSION = "0.7.8"; // Reported to server via PATCH /me every 6h
|
|
77
|
+
|
|
78
|
+
function loadFreshConfig(api: any): ClawnetConfig {
|
|
79
|
+
const raw = api.runtime?.config?.loadConfig?.()?.plugins?.entries?.clawnet?.config ?? {};
|
|
80
|
+
return parseConfig(raw as Record<string, unknown>);
|
|
81
|
+
}
|
|
76
82
|
|
|
77
83
|
// --- Service ---
|
|
78
84
|
|
|
@@ -127,46 +133,10 @@ export function createClawnetService(params: { api: any; cfg: ClawnetConfig }) {
|
|
|
127
133
|
};
|
|
128
134
|
}
|
|
129
135
|
|
|
130
|
-
// ---
|
|
131
|
-
|
|
132
|
-
async function fetchConversationHistory(
|
|
133
|
-
senderIds: string[],
|
|
134
|
-
resolvedToken: string,
|
|
135
|
-
): Promise<string> {
|
|
136
|
-
if (senderIds.length === 0) return "";
|
|
137
|
-
|
|
138
|
-
const sections: string[] = [];
|
|
139
|
-
for (const sender of senderIds) {
|
|
140
|
-
try {
|
|
141
|
-
const encoded = encodeURIComponent(sender);
|
|
142
|
-
const res = await fetch(`${cfg.baseUrl}/messages/${encoded}?limit=10`, {
|
|
143
|
-
headers: {
|
|
144
|
-
Authorization: `Bearer ${resolvedToken}`,
|
|
145
|
-
"Content-Type": "application/json",
|
|
146
|
-
},
|
|
147
|
-
});
|
|
148
|
-
if (!res.ok) continue;
|
|
149
|
-
const data = (await res.json()) as {
|
|
150
|
-
messages: { from: string; to: string; content: string; created_at: string }[];
|
|
151
|
-
};
|
|
152
|
-
if (!data.messages || data.messages.length === 0) continue;
|
|
153
|
-
|
|
154
|
-
// Format oldest-first for natural reading order
|
|
155
|
-
const lines = data.messages
|
|
156
|
-
.reverse()
|
|
157
|
-
.map((m) => ` [${m.created_at}] ${m.from} → ${m.to}: ${m.content}`);
|
|
158
|
-
sections.push(`Conversation with ${sender} (last ${data.messages.length} messages):\n${lines.join("\n")}`);
|
|
159
|
-
} catch {
|
|
160
|
-
// Non-fatal — skip this sender's history
|
|
161
|
-
}
|
|
162
|
-
}
|
|
136
|
+
// --- Batch delivery ---
|
|
163
137
|
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
: "";
|
|
167
|
-
}
|
|
168
|
-
|
|
169
|
-
// --- Batch delivery to hook ---
|
|
138
|
+
// Per-account auth context for mark-notified calls from deliverBatch
|
|
139
|
+
const accountAuth = new Map<string, { token: string; baseUrl: string }>();
|
|
170
140
|
|
|
171
141
|
async function deliverBatch(accountId: string, agentId: string, messages: InboxMessage[]) {
|
|
172
142
|
if (messages.length === 0) return;
|
|
@@ -174,7 +144,6 @@ export function createClawnetService(params: { api: any; cfg: ClawnetConfig }) {
|
|
|
174
144
|
// Concurrency guard
|
|
175
145
|
if (accountBusy.has(accountId)) {
|
|
176
146
|
api.logger.info(`[clawnet] ${accountId}: LLM run in progress, requeueing ${messages.length} message(s)`);
|
|
177
|
-
// Put them back in pending for next cycle
|
|
178
147
|
const existing = pendingMessages.get(accountId) ?? [];
|
|
179
148
|
pendingMessages.set(accountId, [...existing, ...messages]);
|
|
180
149
|
return;
|
|
@@ -183,54 +152,65 @@ export function createClawnetService(params: { api: any; cfg: ClawnetConfig }) {
|
|
|
183
152
|
accountBusy.add(accountId);
|
|
184
153
|
|
|
185
154
|
try {
|
|
186
|
-
|
|
187
|
-
const
|
|
155
|
+
// Re-read config to pick up deliveryMethod changes without restart
|
|
156
|
+
const freshCfg = loadFreshConfig(api);
|
|
188
157
|
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
let context = "";
|
|
194
|
-
const account = cfg.accounts.find((a) => a.id === accountId);
|
|
195
|
-
const apiToken = account ? resolveToken(account.token) : "";
|
|
196
|
-
if (apiToken) {
|
|
197
|
-
const dmSenders = [...new Set(
|
|
198
|
-
messages
|
|
199
|
-
.map((m) => m.from_agent)
|
|
200
|
-
.filter((sender) => !sender.includes("@")),
|
|
201
|
-
)];
|
|
202
|
-
context = await fetchConversationHistory(dmSenders, apiToken);
|
|
203
|
-
}
|
|
204
|
-
|
|
205
|
-
const payload: Record<string, unknown> = {
|
|
206
|
-
agent_id: agentId,
|
|
207
|
-
count: items.length,
|
|
208
|
-
messages: items,
|
|
209
|
-
};
|
|
210
|
-
if (context) {
|
|
211
|
-
payload.context = context;
|
|
212
|
-
}
|
|
213
|
-
|
|
214
|
-
const res = await fetch(`${hooksUrl}/clawnet/${accountId}`, {
|
|
215
|
-
method: "POST",
|
|
216
|
-
headers: {
|
|
217
|
-
"Content-Type": "application/json",
|
|
218
|
-
...(hooksToken ? { Authorization: `Bearer ${hooksToken}` } : {}),
|
|
219
|
-
},
|
|
220
|
-
body: JSON.stringify(payload),
|
|
221
|
-
});
|
|
222
|
-
|
|
223
|
-
if (!res.ok) {
|
|
224
|
-
const body = await res.text().catch(() => "");
|
|
225
|
-
throw new Error(`Hook POST (${messages.length} msgs) returned ${res.status}: ${body}`);
|
|
158
|
+
if (freshCfg.deliveryMethod === "agent") {
|
|
159
|
+
await deliverViaAgent(accountId, agentId, messages);
|
|
160
|
+
} else {
|
|
161
|
+
await deliverViaHooks(accountId, agentId, messages);
|
|
226
162
|
}
|
|
227
163
|
|
|
228
164
|
state.counters.batchesSent++;
|
|
229
165
|
state.counters.delivered += messages.length;
|
|
230
166
|
deliveryLock.set(accountId, new Date(Date.now() + DELIVERY_LOCK_TTL_MS));
|
|
231
167
|
api.logger.info(
|
|
232
|
-
`[clawnet] ${accountId}: delivered ${messages.length} message(s) to ${agentId}`,
|
|
168
|
+
`[clawnet] ${accountId}: delivered ${messages.length} message(s) to ${agentId} via ${freshCfg.deliveryMethod}`,
|
|
233
169
|
);
|
|
170
|
+
|
|
171
|
+
// Post-delivery: mark items as notified + mark A2A tasks as working
|
|
172
|
+
const auth = accountAuth.get(accountId);
|
|
173
|
+
if (auth) {
|
|
174
|
+
const emailIds = messages.filter((m) => m.type === "email").map((m) => m.id);
|
|
175
|
+
const taskIds = messages.filter((m) => m.type === "task").map((m) => m.id);
|
|
176
|
+
|
|
177
|
+
// Mark notified (non-fatal)
|
|
178
|
+
if (emailIds.length > 0 || taskIds.length > 0) {
|
|
179
|
+
try {
|
|
180
|
+
await fetch(`${auth.baseUrl}/inbox/mark-notified`, {
|
|
181
|
+
method: "POST",
|
|
182
|
+
headers: { Authorization: `Bearer ${auth.token}`, "Content-Type": "application/json" },
|
|
183
|
+
body: JSON.stringify({
|
|
184
|
+
...(emailIds.length > 0 ? { message_ids: emailIds } : {}),
|
|
185
|
+
...(taskIds.length > 0 ? { task_ids: taskIds } : {}),
|
|
186
|
+
}),
|
|
187
|
+
});
|
|
188
|
+
api.logger.debug?.(`[clawnet] ${accountId}: marked ${emailIds.length} message(s) + ${taskIds.length} task(s) notified`);
|
|
189
|
+
} catch (err: any) {
|
|
190
|
+
api.logger.warn(`[clawnet] ${accountId}: mark-notified failed (non-fatal): ${err.message}`);
|
|
191
|
+
}
|
|
192
|
+
}
|
|
193
|
+
|
|
194
|
+
// Mark incoming A2A tasks as 'working' (protocol semantics, separate from notification tracking)
|
|
195
|
+
for (const msg of messages) {
|
|
196
|
+
if (msg.type === "task" && msg.content.startsWith("[A2A Task ")) {
|
|
197
|
+
try {
|
|
198
|
+
await fetch(`${auth.baseUrl}/a2a`, {
|
|
199
|
+
method: "POST",
|
|
200
|
+
headers: { Authorization: `Bearer ${auth.token}`, "Content-Type": "application/json" },
|
|
201
|
+
body: JSON.stringify({
|
|
202
|
+
jsonrpc: "2.0",
|
|
203
|
+
id: `ack-${msg.id}`,
|
|
204
|
+
method: "tasks/respond",
|
|
205
|
+
params: { id: msg.id, state: "working" },
|
|
206
|
+
}),
|
|
207
|
+
});
|
|
208
|
+
} catch {
|
|
209
|
+
// Non-fatal — task may get re-delivered next cycle
|
|
210
|
+
}
|
|
211
|
+
}
|
|
212
|
+
}
|
|
213
|
+
}
|
|
234
214
|
} catch (err: any) {
|
|
235
215
|
state.lastError = { message: err.message, at: new Date() };
|
|
236
216
|
state.counters.errors++;
|
|
@@ -240,6 +220,77 @@ export function createClawnetService(params: { api: any; cfg: ClawnetConfig }) {
|
|
|
240
220
|
}
|
|
241
221
|
}
|
|
242
222
|
|
|
223
|
+
// --- Delivery via hooks (original method) ---
|
|
224
|
+
|
|
225
|
+
async function deliverViaHooks(accountId: string, agentId: string, messages: InboxMessage[]) {
|
|
226
|
+
const hooksUrl = getHooksUrl(api);
|
|
227
|
+
const hooksToken = getHooksToken(api);
|
|
228
|
+
|
|
229
|
+
const items = messages.map((msg) => formatMessage(msg));
|
|
230
|
+
const payload: Record<string, unknown> = {
|
|
231
|
+
agent_id: agentId,
|
|
232
|
+
count: items.length,
|
|
233
|
+
messages: items,
|
|
234
|
+
};
|
|
235
|
+
|
|
236
|
+
const res = await fetch(`${hooksUrl}/clawnet/${accountId}`, {
|
|
237
|
+
method: "POST",
|
|
238
|
+
headers: {
|
|
239
|
+
"Content-Type": "application/json",
|
|
240
|
+
...(hooksToken ? { Authorization: `Bearer ${hooksToken}` } : {}),
|
|
241
|
+
},
|
|
242
|
+
body: JSON.stringify(payload),
|
|
243
|
+
});
|
|
244
|
+
|
|
245
|
+
if (!res.ok) {
|
|
246
|
+
const body = await res.text().catch(() => "");
|
|
247
|
+
throw new Error(`Hook POST (${messages.length} msgs) returned ${res.status}: ${body}`);
|
|
248
|
+
}
|
|
249
|
+
}
|
|
250
|
+
|
|
251
|
+
// --- Delivery via openclaw agent CLI (routes correctly per agent) ---
|
|
252
|
+
|
|
253
|
+
async function deliverViaAgent(accountId: string, agentId: string, messages: InboxMessage[]) {
|
|
254
|
+
const { execFile } = await import("node:child_process");
|
|
255
|
+
const { promisify } = await import("node:util");
|
|
256
|
+
const execFileAsync = promisify(execFile);
|
|
257
|
+
|
|
258
|
+
// Find the right OpenClaw agent ID for routing
|
|
259
|
+
const freshCfg = loadFreshConfig(api);
|
|
260
|
+
const account = freshCfg.accounts.find((a) => a.id === accountId);
|
|
261
|
+
const openclawAgentId = account?.openclawAgentId ?? "main";
|
|
262
|
+
|
|
263
|
+
// Format messages for the LLM
|
|
264
|
+
const lines = messages.map((msg, i) => {
|
|
265
|
+
const from = msg.from_agent;
|
|
266
|
+
const subject = msg.subject ? ` — ${msg.subject}` : "";
|
|
267
|
+
const snippet = msg.content.length > 300 ? msg.content.slice(0, 300) + "…" : msg.content;
|
|
268
|
+
return `${i + 1}. **${from}**${subject}: ${snippet}`;
|
|
269
|
+
});
|
|
270
|
+
|
|
271
|
+
const message = [
|
|
272
|
+
`📬 ${messages.length} new ClawNet message${messages.length === 1 ? "" : "s"} for ${agentId}:`,
|
|
273
|
+
"",
|
|
274
|
+
...lines,
|
|
275
|
+
"",
|
|
276
|
+
"Apply your rules to these messages. Present a brief summary of what arrived.",
|
|
277
|
+
"End with: Type /inbox to manage your inbox.",
|
|
278
|
+
].join("\n");
|
|
279
|
+
|
|
280
|
+
const args = [
|
|
281
|
+
"agent",
|
|
282
|
+
"--agent", openclawAgentId,
|
|
283
|
+
"--message", message,
|
|
284
|
+
"--deliver",
|
|
285
|
+
];
|
|
286
|
+
|
|
287
|
+
try {
|
|
288
|
+
await execFileAsync("openclaw", args, { timeout: 120_000 });
|
|
289
|
+
} catch (err: any) {
|
|
290
|
+
throw new Error(`openclaw agent --deliver failed: ${err.message?.slice(0, 200)}`);
|
|
291
|
+
}
|
|
292
|
+
}
|
|
293
|
+
|
|
243
294
|
// --- Debounced flush: wait for more messages, then deliver ---
|
|
244
295
|
|
|
245
296
|
function scheduleFlush(accountId: string, agentId: string) {
|
|
@@ -283,13 +334,16 @@ export function createClawnetService(params: { api: any; cfg: ClawnetConfig }) {
|
|
|
283
334
|
|
|
284
335
|
// --- Poll ---
|
|
285
336
|
|
|
286
|
-
async function pollAccount(account: ClawnetAccount): Promise<number> {
|
|
337
|
+
async function pollAccount(account: ClawnetAccount): Promise<{ a2aDmCount: number; sentTaskUpdates: number; notifyCount: number }> {
|
|
287
338
|
const resolvedToken = resolveToken(account.token);
|
|
288
339
|
if (!resolvedToken) {
|
|
289
340
|
api.logger.warn(`[clawnet] No token resolved for account "${account.id}", skipping`);
|
|
290
|
-
return 0;
|
|
341
|
+
return { a2aDmCount: 0, sentTaskUpdates: 0, notifyCount: 0 };
|
|
291
342
|
}
|
|
292
343
|
|
|
344
|
+
// Store auth context for deliverBatch to use for mark-notified calls
|
|
345
|
+
accountAuth.set(account.id, { token: resolvedToken, baseUrl: cfg.baseUrl });
|
|
346
|
+
|
|
293
347
|
const headers = {
|
|
294
348
|
Authorization: `Bearer ${resolvedToken}`,
|
|
295
349
|
"Content-Type": "application/json",
|
|
@@ -302,12 +356,16 @@ export function createClawnetService(params: { api: any; cfg: ClawnetConfig }) {
|
|
|
302
356
|
}
|
|
303
357
|
const checkData = (await checkRes.json()) as {
|
|
304
358
|
count: number;
|
|
305
|
-
|
|
359
|
+
task_count?: number;
|
|
360
|
+
sent_task_updates?: number;
|
|
361
|
+
notify_count?: number;
|
|
306
362
|
plugin_config?: {
|
|
307
363
|
poll_seconds: number;
|
|
308
364
|
debounce_seconds: number;
|
|
309
365
|
max_batch_size: number;
|
|
310
366
|
deliver_channel: string;
|
|
367
|
+
notify_on_new?: boolean;
|
|
368
|
+
remind_after_hours?: number | null;
|
|
311
369
|
};
|
|
312
370
|
};
|
|
313
371
|
|
|
@@ -336,24 +394,31 @@ export function createClawnetService(params: { api: any; cfg: ClawnetConfig }) {
|
|
|
336
394
|
}
|
|
337
395
|
}
|
|
338
396
|
|
|
339
|
-
const a2aDmCount = checkData.
|
|
397
|
+
const a2aDmCount = checkData.task_count ?? 0;
|
|
398
|
+
const sentTaskUpdates = checkData.sent_task_updates ?? 0;
|
|
399
|
+
const notifyCount = checkData.notify_count ?? (checkData.count + a2aDmCount + sentTaskUpdates);
|
|
340
400
|
|
|
341
401
|
if (checkData.count === 0) {
|
|
342
402
|
// Email inbox clear — release any delivery lock (agent finished processing)
|
|
343
403
|
deliveryLock.delete(account.id);
|
|
344
|
-
return a2aDmCount;
|
|
404
|
+
return { a2aDmCount, sentTaskUpdates, notifyCount };
|
|
405
|
+
}
|
|
406
|
+
|
|
407
|
+
// If nothing needs notification, skip fetch (but don't release lock — inbox still has items)
|
|
408
|
+
if (notifyCount === 0) {
|
|
409
|
+
return { a2aDmCount, sentTaskUpdates, notifyCount };
|
|
345
410
|
}
|
|
346
411
|
|
|
347
|
-
// Skip if a recent
|
|
412
|
+
// Skip if a recent delivery is still being processed.
|
|
348
413
|
// TTL-based lock: after successful POST, lock for 10 min to let the agent work.
|
|
349
414
|
const lockUntil = deliveryLock.get(account.id);
|
|
350
415
|
if (lockUntil && new Date() < lockUntil) {
|
|
351
416
|
api.logger.debug?.(`[clawnet] ${account.id}: ${checkData.count} message(s) waiting (delivery lock active, skipping)`);
|
|
352
|
-
return a2aDmCount;
|
|
417
|
+
return { a2aDmCount, sentTaskUpdates, notifyCount };
|
|
353
418
|
}
|
|
354
419
|
|
|
355
420
|
state.lastInboxNonEmptyAt = new Date();
|
|
356
|
-
api.logger.info(`[clawnet] ${account.id}: ${checkData.count} message(s) waiting`);
|
|
421
|
+
api.logger.info(`[clawnet] ${account.id}: ${checkData.count} message(s) waiting (${notifyCount} to notify)`);
|
|
357
422
|
|
|
358
423
|
// Fetch full messages
|
|
359
424
|
const inboxRes = await fetch(`${cfg.baseUrl}/inbox`, { headers });
|
|
@@ -362,7 +427,7 @@ export function createClawnetService(params: { api: any; cfg: ClawnetConfig }) {
|
|
|
362
427
|
}
|
|
363
428
|
const inboxData = (await inboxRes.json()) as { messages: Array<Record<string, any>> };
|
|
364
429
|
|
|
365
|
-
if (inboxData.messages.length === 0) return a2aDmCount;
|
|
430
|
+
if (inboxData.messages.length === 0) return { a2aDmCount, sentTaskUpdates, notifyCount };
|
|
366
431
|
|
|
367
432
|
// Normalize API field names: API returns "from", plugin uses "from_agent"
|
|
368
433
|
const normalized: InboxMessage[] = inboxData.messages.map((m) => ({
|
|
@@ -371,6 +436,7 @@ export function createClawnetService(params: { api: any; cfg: ClawnetConfig }) {
|
|
|
371
436
|
content: m.content,
|
|
372
437
|
subject: m.email?.subject ?? m.subject,
|
|
373
438
|
created_at: m.created_at,
|
|
439
|
+
type: "email" as const,
|
|
374
440
|
}));
|
|
375
441
|
|
|
376
442
|
state.counters.messagesSeen += normalized.length;
|
|
@@ -380,7 +446,7 @@ export function createClawnetService(params: { api: any; cfg: ClawnetConfig }) {
|
|
|
380
446
|
pendingMessages.set(account.id, [...existing, ...normalized]);
|
|
381
447
|
scheduleFlush(account.id, account.agentId);
|
|
382
448
|
|
|
383
|
-
return a2aDmCount;
|
|
449
|
+
return { a2aDmCount, sentTaskUpdates, notifyCount };
|
|
384
450
|
}
|
|
385
451
|
|
|
386
452
|
async function pollAccountA2A(account: ClawnetAccount, a2aDmCount: number) {
|
|
@@ -422,7 +488,8 @@ export function createClawnetService(params: { api: any; cfg: ClawnetConfig }) {
|
|
|
422
488
|
|
|
423
489
|
api.logger.info(`[clawnet] ${account.id}: ${tasks.length} A2A task(s) to deliver`);
|
|
424
490
|
|
|
425
|
-
// Convert A2A tasks to the message format
|
|
491
|
+
// Convert A2A tasks to the message format for delivery
|
|
492
|
+
// Working transition + mark-notified happen post-delivery in deliverBatch
|
|
426
493
|
const messages: InboxMessage[] = tasks.map((task) => {
|
|
427
494
|
const history = task.history as Array<{ role: string; parts: Array<{ text?: string }> }> ?? [];
|
|
428
495
|
const lastMsg = history[history.length - 1];
|
|
@@ -433,6 +500,7 @@ export function createClawnetService(params: { api: any; cfg: ClawnetConfig }) {
|
|
|
433
500
|
from_agent: task.from,
|
|
434
501
|
content: `[A2A Task ${task.id}]${contactInfo}\n${text}`,
|
|
435
502
|
created_at: (task.status as any)?.timestamp ?? new Date().toISOString(),
|
|
503
|
+
type: "task" as const,
|
|
436
504
|
};
|
|
437
505
|
});
|
|
438
506
|
|
|
@@ -440,28 +508,59 @@ export function createClawnetService(params: { api: any; cfg: ClawnetConfig }) {
|
|
|
440
508
|
const existing = pendingMessages.get(account.id) ?? [];
|
|
441
509
|
pendingMessages.set(account.id, [...existing, ...messages]);
|
|
442
510
|
scheduleFlush(account.id, account.agentId);
|
|
511
|
+
}
|
|
443
512
|
|
|
444
|
-
|
|
445
|
-
|
|
446
|
-
|
|
447
|
-
|
|
448
|
-
|
|
449
|
-
|
|
450
|
-
|
|
451
|
-
|
|
452
|
-
|
|
453
|
-
|
|
454
|
-
|
|
455
|
-
|
|
456
|
-
|
|
457
|
-
|
|
458
|
-
|
|
459
|
-
|
|
460
|
-
|
|
461
|
-
|
|
462
|
-
|
|
463
|
-
|
|
464
|
-
|
|
513
|
+
async function pollSentTaskUpdates(account: ClawnetAccount) {
|
|
514
|
+
const resolvedToken = resolveToken(account.token);
|
|
515
|
+
if (!resolvedToken) return;
|
|
516
|
+
|
|
517
|
+
// Skip if delivery lock active
|
|
518
|
+
const lockUntil = deliveryLock.get(account.id);
|
|
519
|
+
if (lockUntil && new Date() < lockUntil) return;
|
|
520
|
+
|
|
521
|
+
// Fetch tasks I sent that need my attention or have finished
|
|
522
|
+
const body = {
|
|
523
|
+
jsonrpc: "2.0",
|
|
524
|
+
id: `sent-poll-${Date.now()}`,
|
|
525
|
+
method: "tasks/list",
|
|
526
|
+
params: { role: "sender", status: "input-required,completed,failed", limit: 50 },
|
|
527
|
+
};
|
|
528
|
+
const res = await fetch(`${cfg.baseUrl}/a2a`, {
|
|
529
|
+
method: "POST",
|
|
530
|
+
headers: {
|
|
531
|
+
Authorization: `Bearer ${resolvedToken}`,
|
|
532
|
+
"Content-Type": "application/json",
|
|
533
|
+
},
|
|
534
|
+
body: JSON.stringify(body),
|
|
535
|
+
});
|
|
536
|
+
if (!res.ok) return;
|
|
537
|
+
|
|
538
|
+
const data = (await res.json()) as {
|
|
539
|
+
result?: { tasks: Array<Record<string, any>> };
|
|
540
|
+
};
|
|
541
|
+
const tasks = data.result?.tasks ?? [];
|
|
542
|
+
if (tasks.length === 0) return;
|
|
543
|
+
|
|
544
|
+
api.logger.info(`[clawnet] ${account.id}: ${tasks.length} sent task update(s) to deliver`);
|
|
545
|
+
|
|
546
|
+
const messages: InboxMessage[] = tasks.map((task) => {
|
|
547
|
+
const history = task.history as Array<{ role: string; parts: Array<{ text?: string }> }> ?? [];
|
|
548
|
+
const lastMsg = history[history.length - 1];
|
|
549
|
+
const text = lastMsg?.parts?.map((p: any) => p.text).filter(Boolean).join("\n") ?? "";
|
|
550
|
+
const taskState = task.state ?? "unknown";
|
|
551
|
+
return {
|
|
552
|
+
id: task.id,
|
|
553
|
+
from_agent: task.to, // the agent that responded
|
|
554
|
+
content: `[Task update: ${taskState}] Re: "${text.slice(0, 100)}${text.length > 100 ? "…" : ""}"`,
|
|
555
|
+
created_at: (task.status as any)?.timestamp ?? new Date().toISOString(),
|
|
556
|
+
type: "task" as const,
|
|
557
|
+
};
|
|
558
|
+
});
|
|
559
|
+
|
|
560
|
+
state.counters.messagesSeen += messages.length;
|
|
561
|
+
const existing = pendingMessages.get(account.id) ?? [];
|
|
562
|
+
pendingMessages.set(account.id, [...existing, ...messages]);
|
|
563
|
+
scheduleFlush(account.id, account.agentId);
|
|
465
564
|
}
|
|
466
565
|
|
|
467
566
|
async function tick() {
|
|
@@ -508,7 +607,7 @@ export function createClawnetService(params: { api: any; cfg: ClawnetConfig }) {
|
|
|
508
607
|
let hadError = false;
|
|
509
608
|
for (const account of enabledAccounts) {
|
|
510
609
|
try {
|
|
511
|
-
const a2aDmCount = await pollAccount(account);
|
|
610
|
+
const { a2aDmCount, sentTaskUpdates } = await pollAccount(account);
|
|
512
611
|
|
|
513
612
|
// Also poll for A2A DMs if any pending
|
|
514
613
|
if (a2aDmCount > 0) {
|
|
@@ -518,6 +617,15 @@ export function createClawnetService(params: { api: any; cfg: ClawnetConfig }) {
|
|
|
518
617
|
api.logger.error(`[clawnet] A2A poll error for ${account.id}: ${a2aErr.message}`);
|
|
519
618
|
}
|
|
520
619
|
}
|
|
620
|
+
|
|
621
|
+
// Poll for sent task updates (tasks I sent that got a response)
|
|
622
|
+
if (sentTaskUpdates > 0) {
|
|
623
|
+
try {
|
|
624
|
+
await pollSentTaskUpdates(account);
|
|
625
|
+
} catch (err: any) {
|
|
626
|
+
api.logger.error(`[clawnet] Sent task updates error for ${account.id}: ${err.message}`);
|
|
627
|
+
}
|
|
628
|
+
}
|
|
521
629
|
} catch (err: any) {
|
|
522
630
|
hadError = true;
|
|
523
631
|
state.lastError = { message: err.message, at: new Date() };
|
package/src/tools.ts
CHANGED
|
@@ -155,6 +155,8 @@ interface CapabilityOp {
|
|
|
155
155
|
description: string;
|
|
156
156
|
params?: Record<string, { type: string; description: string; required?: boolean }>;
|
|
157
157
|
rawBodyParam?: string; // If set, send this param as raw text body instead of JSON
|
|
158
|
+
jsonrpc?: boolean; // If true, dispatch via a2aCall() instead of REST
|
|
159
|
+
rpc_method?: string; // JSON-RPC method name (required when jsonrpc is true)
|
|
158
160
|
}
|
|
159
161
|
|
|
160
162
|
const BUILTIN_OPERATIONS: CapabilityOp[] = [
|
|
@@ -173,15 +175,11 @@ const BUILTIN_OPERATIONS: CapabilityOp[] = [
|
|
|
173
175
|
{ operation: "email.allowlist.remove", method: "DELETE", path: "/email/allowlist", description: "Remove sender from email allowlist", params: {
|
|
174
176
|
pattern: { type: "string", description: "Email address or pattern to remove", required: true },
|
|
175
177
|
}},
|
|
176
|
-
//
|
|
177
|
-
{ operation: "
|
|
178
|
-
to: { type: "string", description: "Recipient agent name", required: true },
|
|
179
|
-
message: { type: "string", description: "Message content (max 10000 chars)", required: true },
|
|
180
|
-
}},
|
|
181
|
-
{ operation: "dm.block", method: "POST", path: "/block", description: "Block an agent from DMing you", params: {
|
|
178
|
+
// Agent moderation
|
|
179
|
+
{ operation: "agent.block", method: "POST", path: "/block", description: "Block an agent from contacting you", params: {
|
|
182
180
|
agent_id: { type: "string", description: "Agent to block", required: true },
|
|
183
181
|
}},
|
|
184
|
-
{ operation: "
|
|
182
|
+
{ operation: "agent.unblock", method: "POST", path: "/unblock", description: "Unblock an agent", params: {
|
|
185
183
|
agent_id: { type: "string", description: "Agent to unblock", required: true },
|
|
186
184
|
}},
|
|
187
185
|
// Messages (cross-cutting)
|
|
@@ -222,8 +220,10 @@ const BUILTIN_OPERATIONS: CapabilityOp[] = [
|
|
|
222
220
|
title: { type: "string", description: "Event title", required: true },
|
|
223
221
|
starts_at: { type: "string", description: "ISO 8601 start time", required: true },
|
|
224
222
|
ends_at: { type: "string", description: "ISO 8601 end time" },
|
|
223
|
+
all_day: { type: "boolean", description: "Mark as all-day event (spans full calendar day)" },
|
|
225
224
|
location: { type: "string", description: "Event location" },
|
|
226
225
|
description: { type: "string", description: "Event description" },
|
|
226
|
+
remind_minutes: { type: "number", description: "Minutes before event to send notification (0-10080, default 15, null to disable)" },
|
|
227
227
|
attendees: { type: "array", description: "Array of {email, name?} — each gets a .ics invite" },
|
|
228
228
|
}},
|
|
229
229
|
{ operation: "calendar.list", method: "GET", path: "/calendar/events", description: "List calendar events", params: {
|
|
@@ -235,7 +235,11 @@ const BUILTIN_OPERATIONS: CapabilityOp[] = [
|
|
|
235
235
|
event_id: { type: "string", description: "Event ID", required: true },
|
|
236
236
|
title: { type: "string", description: "New title" },
|
|
237
237
|
starts_at: { type: "string", description: "New start time" },
|
|
238
|
+
ends_at: { type: "string", description: "New end time" },
|
|
239
|
+
all_day: { type: "boolean", description: "Mark as all-day event" },
|
|
238
240
|
location: { type: "string", description: "New location" },
|
|
241
|
+
description: { type: "string", description: "New description" },
|
|
242
|
+
remind_minutes: { type: "number", description: "Minutes before event to send notification (0-10080, null to disable)" },
|
|
239
243
|
}},
|
|
240
244
|
{ operation: "calendar.delete", method: "DELETE", path: "/calendar/events/:event_id", description: "Delete event (sends cancellation to attendees)", params: {
|
|
241
245
|
event_id: { type: "string", description: "Event ID", required: true },
|
|
@@ -266,6 +270,30 @@ const BUILTIN_OPERATIONS: CapabilityOp[] = [
|
|
|
266
270
|
{ operation: "account.rate_limits", method: "GET", path: "/me/rate-limits", description: "Check your current rate limits" },
|
|
267
271
|
// Docs
|
|
268
272
|
{ operation: "docs.help", method: "GET", path: "/docs/skill", description: "Get the full ClawNet documentation — features, usage examples, safety rules, setup, troubleshooting, and rate limits" },
|
|
273
|
+
// A2A (JSON-RPC via /a2a)
|
|
274
|
+
{ operation: "a2a.card.update", jsonrpc: true, method: "POST", path: "/a2a", rpc_method: "card/update", description: "Update your A2A Agent Card skills", params: {
|
|
275
|
+
skills: { type: "array", description: "Array of {id, name, description} skill objects", required: true },
|
|
276
|
+
}},
|
|
277
|
+
{ operation: "a2a.tasks.list", jsonrpc: true, method: "POST", path: "/a2a", rpc_method: "tasks/list", description: "List your A2A tasks", params: {
|
|
278
|
+
status: { type: "string", description: "Filter by status (e.g. 'submitted', 'working', 'completed', 'failed', comma-separated for multiple)" },
|
|
279
|
+
role: { type: "string", description: "'sender' or 'recipient'" },
|
|
280
|
+
limit: { type: "number", description: "Max tasks to return" },
|
|
281
|
+
}},
|
|
282
|
+
{ operation: "a2a.tasks.get", jsonrpc: true, method: "POST", path: "/a2a", rpc_method: "tasks/get", description: "Get a specific A2A task by ID", params: {
|
|
283
|
+
id: { type: "string", description: "Task ID", required: true },
|
|
284
|
+
}},
|
|
285
|
+
{ operation: "a2a.tasks.respond", jsonrpc: true, method: "POST", path: "/a2a", rpc_method: "tasks/respond", description: "Respond to an A2A task", params: {
|
|
286
|
+
id: { type: "string", description: "Task ID", required: true },
|
|
287
|
+
state: { type: "string", description: "New state: completed, input-required, working, or failed", required: true },
|
|
288
|
+
message: { type: "string", description: "Response message text" },
|
|
289
|
+
artifacts: { type: "array", description: "Array of artifact objects (for completed tasks)" },
|
|
290
|
+
}},
|
|
291
|
+
{ operation: "a2a.tasks.cancel", jsonrpc: true, method: "POST", path: "/a2a", rpc_method: "tasks/cancel", description: "Cancel an A2A task", params: {
|
|
292
|
+
id: { type: "string", description: "Task ID", required: true },
|
|
293
|
+
}},
|
|
294
|
+
{ operation: "a2a.tasks.count", jsonrpc: true, method: "POST", path: "/a2a", rpc_method: "tasks/count", description: "Count A2A tasks by status", params: {
|
|
295
|
+
status: { type: "string", description: "Filter by status" },
|
|
296
|
+
}},
|
|
269
297
|
];
|
|
270
298
|
|
|
271
299
|
// --- Dynamic capabilities ---
|
|
@@ -335,7 +363,7 @@ export function registerTools(api: any) {
|
|
|
335
363
|
|
|
336
364
|
api.registerTool((ctx: { agentId?: string; sessionKey?: string }) => ({
|
|
337
365
|
name: "clawnet_inbox_check",
|
|
338
|
-
description: toolDesc("clawnet_inbox_check", "Check if you have new messages. Returns total count and breakdown by type (email,
|
|
366
|
+
description: toolDesc("clawnet_inbox_check", "Check if you have new messages. Returns total count and breakdown by type (email, tasks). Lightweight — use this before fetching full inbox. Use clawnet_email_inbox for emails, or clawnet_task_inbox for agent tasks."),
|
|
339
367
|
parameters: {
|
|
340
368
|
type: "object",
|
|
341
369
|
properties: {},
|
|
@@ -436,7 +464,7 @@ export function registerTools(api: any) {
|
|
|
436
464
|
|
|
437
465
|
api.registerTool((ctx: { agentId?: string; sessionKey?: string }) => ({
|
|
438
466
|
name: "clawnet_inbox_session",
|
|
439
|
-
description: toolDesc("clawnet_inbox_session", "Start an interactive
|
|
467
|
+
description: toolDesc("clawnet_inbox_session", "Start an interactive inbox session. Returns your emails with assigned numbers and a triage protocol. IMPORTANT: After calling this tool, also call clawnet_task_inbox to get pending agent tasks — present both emails and tasks together to your human."),
|
|
440
468
|
parameters: {
|
|
441
469
|
type: "object",
|
|
442
470
|
properties: {
|
|
@@ -493,10 +521,40 @@ export function registerTools(api: any) {
|
|
|
493
521
|
};
|
|
494
522
|
});
|
|
495
523
|
|
|
524
|
+
// Fetch A2A tasks via REST-style POST to /a2a
|
|
525
|
+
let tasks: Array<Record<string, unknown>> = [];
|
|
526
|
+
try {
|
|
527
|
+
const taskResult = await apiCall(cfg, "POST", "/a2a", {
|
|
528
|
+
jsonrpc: "2.0",
|
|
529
|
+
id: `inbox-${Date.now()}`,
|
|
530
|
+
method: "tasks/list",
|
|
531
|
+
params: { status: "submitted,working" },
|
|
532
|
+
}, ctx?.agentId, ctx?.sessionKey);
|
|
533
|
+
const taskData = taskResult.data as any;
|
|
534
|
+
const rawTasks = taskData?.result?.tasks ?? taskData?.tasks ?? [];
|
|
535
|
+
tasks = rawTasks.map((t: any, i: number) => {
|
|
536
|
+
const lastMsg = (t.history ?? []).slice(-1)[0];
|
|
537
|
+
const text = lastMsg?.parts?.map((p: any) => p.text).filter(Boolean).join("\n") ?? "";
|
|
538
|
+
return {
|
|
539
|
+
n: emails.length + i + 1,
|
|
540
|
+
id: t.id,
|
|
541
|
+
type: "a2a_task",
|
|
542
|
+
from: t.from,
|
|
543
|
+
trust_tier: t.trustTier ?? "public",
|
|
544
|
+
content: text.slice(0, 200),
|
|
545
|
+
state: t.status?.state ?? "unknown",
|
|
546
|
+
received_at: t.status?.timestamp,
|
|
547
|
+
};
|
|
548
|
+
});
|
|
549
|
+
} catch {
|
|
550
|
+
// Non-fatal — show emails even if task fetch fails
|
|
551
|
+
}
|
|
552
|
+
|
|
496
553
|
return textResult({
|
|
497
554
|
protocol,
|
|
498
555
|
emails,
|
|
499
|
-
|
|
556
|
+
tasks,
|
|
557
|
+
counts: { total: emails.length + tasks.length, emails: emails.length, tasks: tasks.length, new: newCount, read: readCount },
|
|
500
558
|
});
|
|
501
559
|
},
|
|
502
560
|
}));
|
|
@@ -505,11 +563,11 @@ export function registerTools(api: any) {
|
|
|
505
563
|
|
|
506
564
|
api.registerTool((ctx: { agentId?: string; sessionKey?: string }) => ({
|
|
507
565
|
name: "clawnet_task_send",
|
|
508
|
-
description: toolDesc("clawnet_task_send", "Send a task to another ClawNet agent. Use this when you need something from another agent — a question answered, an action performed, information looked up. Returns a task ID to check for their response later. For fire-and-forget notifications, use email instead."),
|
|
566
|
+
description: toolDesc("clawnet_task_send", "Send a task to another agent. Works with ClawNet agent names (e.g. 'Tom') or external A2A endpoint URLs (e.g. 'https://example.com/a2a/agent'). Use this when you need something from another agent — a question answered, an action performed, information looked up. Returns a task ID to check for their response later. For fire-and-forget notifications, use email instead."),
|
|
509
567
|
parameters: {
|
|
510
568
|
type: "object",
|
|
511
569
|
properties: {
|
|
512
|
-
to: { type: "string", description: "Recipient agent name" },
|
|
570
|
+
to: { type: "string", description: "Recipient: agent name (e.g. 'Tom') or external A2A URL (e.g. 'https://example.com/a2a/agent')" },
|
|
513
571
|
message: { type: "string", description: "Message content" },
|
|
514
572
|
task_id: { type: "string", description: "If following up on a task (after agent asked for input), provide the task ID" },
|
|
515
573
|
},
|
|
@@ -517,6 +575,26 @@ export function registerTools(api: any) {
|
|
|
517
575
|
},
|
|
518
576
|
async execute(_id: string, params: { to: string; message: string; task_id?: string }) {
|
|
519
577
|
const cfg = loadFreshConfig(api);
|
|
578
|
+
const isExternalUrl = params.to.startsWith("https://") || params.to.startsWith("http://");
|
|
579
|
+
|
|
580
|
+
if (params.to.startsWith("http://")) {
|
|
581
|
+
return textResult({ error: "Only HTTPS URLs are supported for external A2A endpoints. Use https:// instead of http://." });
|
|
582
|
+
}
|
|
583
|
+
|
|
584
|
+
if (isExternalUrl) {
|
|
585
|
+
// External A2A: route through tasks/send-external on internal endpoint
|
|
586
|
+
const a2aParams: Record<string, unknown> = {
|
|
587
|
+
url: params.to,
|
|
588
|
+
message: { role: "user", parts: [{ kind: "text", text: params.message }] },
|
|
589
|
+
};
|
|
590
|
+
if (params.task_id) {
|
|
591
|
+
a2aParams.taskId = params.task_id;
|
|
592
|
+
}
|
|
593
|
+
const result = await a2aCall(cfg, "/a2a", "tasks/send-external", a2aParams, ctx?.agentId, ctx?.sessionKey);
|
|
594
|
+
return textResult(result.data);
|
|
595
|
+
}
|
|
596
|
+
|
|
597
|
+
// Internal ClawNet agent: existing path
|
|
520
598
|
const a2aParams: Record<string, unknown> = {
|
|
521
599
|
message: { role: "user", parts: [{ kind: "text", text: params.message }] },
|
|
522
600
|
};
|
|
@@ -553,14 +631,19 @@ export function registerTools(api: any) {
|
|
|
553
631
|
parameters: {
|
|
554
632
|
type: "object",
|
|
555
633
|
properties: {
|
|
556
|
-
status: { type: "string", description: "Filter: '
|
|
634
|
+
status: { type: "string", description: "Filter: 'pending' (default — shows submitted + working), 'submitted', 'working', 'completed', 'failed', or 'all'" },
|
|
557
635
|
limit: { type: "number", description: "Max tasks (default 50, max 100)" },
|
|
558
636
|
},
|
|
559
637
|
},
|
|
560
638
|
async execute(_id: string, params: { status?: string; limit?: number }) {
|
|
561
639
|
const cfg = loadFreshConfig(api);
|
|
562
|
-
const a2aParams: Record<string, unknown> = {};
|
|
563
|
-
|
|
640
|
+
const a2aParams: Record<string, unknown> = { role: "recipient" };
|
|
641
|
+
const statusFilter = params.status || "pending";
|
|
642
|
+
if (statusFilter === "pending") {
|
|
643
|
+
a2aParams.status = "submitted,working";
|
|
644
|
+
} else {
|
|
645
|
+
a2aParams.status = statusFilter;
|
|
646
|
+
}
|
|
564
647
|
if (params.limit) a2aParams.limit = params.limit;
|
|
565
648
|
const result = await a2aCall(cfg, "/a2a", "tasks/list", a2aParams, ctx?.agentId, ctx?.sessionKey);
|
|
566
649
|
return textResult(result.data);
|
|
@@ -637,7 +720,7 @@ export function registerTools(api: any) {
|
|
|
637
720
|
parameters: {
|
|
638
721
|
type: "object",
|
|
639
722
|
properties: {
|
|
640
|
-
operation: { type: "string", description: "Operation name from clawnet_capabilities (e.g. '
|
|
723
|
+
operation: { type: "string", description: "Operation name from clawnet_capabilities (e.g. 'agent.block', 'profile.update', 'calendar.create')" },
|
|
641
724
|
params: { type: "object", description: "Operation parameters (see clawnet_capabilities for schema)" },
|
|
642
725
|
},
|
|
643
726
|
required: ["operation"],
|
|
@@ -683,6 +766,16 @@ export function registerTools(api: any) {
|
|
|
683
766
|
if (query) path += (path.includes('?') ? '&' : '?') + query;
|
|
684
767
|
}
|
|
685
768
|
|
|
769
|
+
// JSON-RPC operations: dispatch via a2aCall()
|
|
770
|
+
if (op.jsonrpc && op.rpc_method) {
|
|
771
|
+
const rpcParams: Record<string, unknown> = {};
|
|
772
|
+
for (const [key, val] of Object.entries(params)) {
|
|
773
|
+
if (val !== undefined) rpcParams[key] = val;
|
|
774
|
+
}
|
|
775
|
+
const result = await a2aCall(cfg, op.path, op.rpc_method, Object.keys(rpcParams).length > 0 ? rpcParams : undefined, ctx?.agentId, ctx?.sessionKey);
|
|
776
|
+
return textResult(result.data);
|
|
777
|
+
}
|
|
778
|
+
|
|
686
779
|
// Build body for non-GET requests
|
|
687
780
|
let body: Record<string, unknown> | undefined;
|
|
688
781
|
let rawBody: string | undefined;
|