@clwnt/clawnet 0.7.6 → 0.7.7
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 +155 -89
- package/src/tools.ts +51 -14
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
|
@@ -72,7 +72,12 @@ async function reloadOnboardingMessage(): Promise<void> {
|
|
|
72
72
|
|
|
73
73
|
const SKILL_UPDATE_INTERVAL_MS = 6 * 60 * 60 * 1000; // 6 hours
|
|
74
74
|
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.
|
|
75
|
+
export const PLUGIN_VERSION = "0.7.7"; // Reported to server via PATCH /me every 6h
|
|
76
|
+
|
|
77
|
+
function loadFreshConfig(api: any): ClawnetConfig {
|
|
78
|
+
const raw = api.runtime?.config?.loadConfig?.()?.plugins?.entries?.clawnet?.config ?? {};
|
|
79
|
+
return parseConfig(raw as Record<string, unknown>);
|
|
80
|
+
}
|
|
76
81
|
|
|
77
82
|
// --- Service ---
|
|
78
83
|
|
|
@@ -127,46 +132,7 @@ export function createClawnetService(params: { api: any; cfg: ClawnetConfig }) {
|
|
|
127
132
|
};
|
|
128
133
|
}
|
|
129
134
|
|
|
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
|
-
}
|
|
163
|
-
|
|
164
|
-
return sections.length > 0
|
|
165
|
-
? sections.join("\n\n")
|
|
166
|
-
: "";
|
|
167
|
-
}
|
|
168
|
-
|
|
169
|
-
// --- Batch delivery to hook ---
|
|
135
|
+
// --- Batch delivery ---
|
|
170
136
|
|
|
171
137
|
async function deliverBatch(accountId: string, agentId: string, messages: InboxMessage[]) {
|
|
172
138
|
if (messages.length === 0) return;
|
|
@@ -174,7 +140,6 @@ export function createClawnetService(params: { api: any; cfg: ClawnetConfig }) {
|
|
|
174
140
|
// Concurrency guard
|
|
175
141
|
if (accountBusy.has(accountId)) {
|
|
176
142
|
api.logger.info(`[clawnet] ${accountId}: LLM run in progress, requeueing ${messages.length} message(s)`);
|
|
177
|
-
// Put them back in pending for next cycle
|
|
178
143
|
const existing = pendingMessages.get(accountId) ?? [];
|
|
179
144
|
pendingMessages.set(accountId, [...existing, ...messages]);
|
|
180
145
|
return;
|
|
@@ -183,53 +148,20 @@ export function createClawnetService(params: { api: any; cfg: ClawnetConfig }) {
|
|
|
183
148
|
accountBusy.add(accountId);
|
|
184
149
|
|
|
185
150
|
try {
|
|
186
|
-
|
|
187
|
-
const
|
|
188
|
-
|
|
189
|
-
// Always send as array — same field names as the API response
|
|
190
|
-
const items = messages.map((msg) => formatMessage(msg));
|
|
191
|
-
|
|
192
|
-
// Fetch conversation history for DM senders (non-email)
|
|
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
|
-
}
|
|
151
|
+
// Re-read config to pick up deliveryMethod changes without restart
|
|
152
|
+
const freshCfg = loadFreshConfig(api);
|
|
213
153
|
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
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}`);
|
|
154
|
+
if (freshCfg.deliveryMethod === "agent") {
|
|
155
|
+
await deliverViaAgent(accountId, agentId, messages);
|
|
156
|
+
} else {
|
|
157
|
+
await deliverViaHooks(accountId, agentId, messages);
|
|
226
158
|
}
|
|
227
159
|
|
|
228
160
|
state.counters.batchesSent++;
|
|
229
161
|
state.counters.delivered += messages.length;
|
|
230
162
|
deliveryLock.set(accountId, new Date(Date.now() + DELIVERY_LOCK_TTL_MS));
|
|
231
163
|
api.logger.info(
|
|
232
|
-
`[clawnet] ${accountId}: delivered ${messages.length} message(s) to ${agentId}`,
|
|
164
|
+
`[clawnet] ${accountId}: delivered ${messages.length} message(s) to ${agentId} via ${freshCfg.deliveryMethod}`,
|
|
233
165
|
);
|
|
234
166
|
} catch (err: any) {
|
|
235
167
|
state.lastError = { message: err.message, at: new Date() };
|
|
@@ -240,6 +172,77 @@ export function createClawnetService(params: { api: any; cfg: ClawnetConfig }) {
|
|
|
240
172
|
}
|
|
241
173
|
}
|
|
242
174
|
|
|
175
|
+
// --- Delivery via hooks (original method) ---
|
|
176
|
+
|
|
177
|
+
async function deliverViaHooks(accountId: string, agentId: string, messages: InboxMessage[]) {
|
|
178
|
+
const hooksUrl = getHooksUrl(api);
|
|
179
|
+
const hooksToken = getHooksToken(api);
|
|
180
|
+
|
|
181
|
+
const items = messages.map((msg) => formatMessage(msg));
|
|
182
|
+
const payload: Record<string, unknown> = {
|
|
183
|
+
agent_id: agentId,
|
|
184
|
+
count: items.length,
|
|
185
|
+
messages: items,
|
|
186
|
+
};
|
|
187
|
+
|
|
188
|
+
const res = await fetch(`${hooksUrl}/clawnet/${accountId}`, {
|
|
189
|
+
method: "POST",
|
|
190
|
+
headers: {
|
|
191
|
+
"Content-Type": "application/json",
|
|
192
|
+
...(hooksToken ? { Authorization: `Bearer ${hooksToken}` } : {}),
|
|
193
|
+
},
|
|
194
|
+
body: JSON.stringify(payload),
|
|
195
|
+
});
|
|
196
|
+
|
|
197
|
+
if (!res.ok) {
|
|
198
|
+
const body = await res.text().catch(() => "");
|
|
199
|
+
throw new Error(`Hook POST (${messages.length} msgs) returned ${res.status}: ${body}`);
|
|
200
|
+
}
|
|
201
|
+
}
|
|
202
|
+
|
|
203
|
+
// --- Delivery via openclaw agent CLI (routes correctly per agent) ---
|
|
204
|
+
|
|
205
|
+
async function deliverViaAgent(accountId: string, agentId: string, messages: InboxMessage[]) {
|
|
206
|
+
const { execFile } = await import("node:child_process");
|
|
207
|
+
const { promisify } = await import("node:util");
|
|
208
|
+
const execFileAsync = promisify(execFile);
|
|
209
|
+
|
|
210
|
+
// Find the right OpenClaw agent ID for routing
|
|
211
|
+
const freshCfg = loadFreshConfig(api);
|
|
212
|
+
const account = freshCfg.accounts.find((a) => a.id === accountId);
|
|
213
|
+
const openclawAgentId = account?.openclawAgentId ?? "main";
|
|
214
|
+
|
|
215
|
+
// Format messages for the LLM
|
|
216
|
+
const lines = messages.map((msg, i) => {
|
|
217
|
+
const from = msg.from_agent;
|
|
218
|
+
const subject = msg.subject ? ` — ${msg.subject}` : "";
|
|
219
|
+
const snippet = msg.content.length > 300 ? msg.content.slice(0, 300) + "…" : msg.content;
|
|
220
|
+
return `${i + 1}. **${from}**${subject}: ${snippet}`;
|
|
221
|
+
});
|
|
222
|
+
|
|
223
|
+
const message = [
|
|
224
|
+
`📬 ${messages.length} new ClawNet message${messages.length === 1 ? "" : "s"} for ${agentId}:`,
|
|
225
|
+
"",
|
|
226
|
+
...lines,
|
|
227
|
+
"",
|
|
228
|
+
"Apply your rules to these messages. Present a brief summary of what arrived.",
|
|
229
|
+
"End with: Type /inbox to manage your inbox.",
|
|
230
|
+
].join("\n");
|
|
231
|
+
|
|
232
|
+
const args = [
|
|
233
|
+
"agent",
|
|
234
|
+
"--agent", openclawAgentId,
|
|
235
|
+
"--message", message,
|
|
236
|
+
"--deliver",
|
|
237
|
+
];
|
|
238
|
+
|
|
239
|
+
try {
|
|
240
|
+
await execFileAsync("openclaw", args, { timeout: 120_000 });
|
|
241
|
+
} catch (err: any) {
|
|
242
|
+
throw new Error(`openclaw agent --deliver failed: ${err.message?.slice(0, 200)}`);
|
|
243
|
+
}
|
|
244
|
+
}
|
|
245
|
+
|
|
243
246
|
// --- Debounced flush: wait for more messages, then deliver ---
|
|
244
247
|
|
|
245
248
|
function scheduleFlush(accountId: string, agentId: string) {
|
|
@@ -302,7 +305,8 @@ export function createClawnetService(params: { api: any; cfg: ClawnetConfig }) {
|
|
|
302
305
|
}
|
|
303
306
|
const checkData = (await checkRes.json()) as {
|
|
304
307
|
count: number;
|
|
305
|
-
|
|
308
|
+
task_count?: number;
|
|
309
|
+
sent_task_updates?: number;
|
|
306
310
|
plugin_config?: {
|
|
307
311
|
poll_seconds: number;
|
|
308
312
|
debounce_seconds: number;
|
|
@@ -336,12 +340,13 @@ export function createClawnetService(params: { api: any; cfg: ClawnetConfig }) {
|
|
|
336
340
|
}
|
|
337
341
|
}
|
|
338
342
|
|
|
339
|
-
const a2aDmCount = checkData.
|
|
343
|
+
const a2aDmCount = checkData.task_count ?? 0;
|
|
344
|
+
const sentTaskUpdates = checkData.sent_task_updates ?? 0;
|
|
340
345
|
|
|
341
346
|
if (checkData.count === 0) {
|
|
342
347
|
// Email inbox clear — release any delivery lock (agent finished processing)
|
|
343
348
|
deliveryLock.delete(account.id);
|
|
344
|
-
return a2aDmCount;
|
|
349
|
+
return { a2aDmCount, sentTaskUpdates };
|
|
345
350
|
}
|
|
346
351
|
|
|
347
352
|
// Skip if a recent webhook delivery is still being processed by the LLM.
|
|
@@ -349,7 +354,7 @@ export function createClawnetService(params: { api: any; cfg: ClawnetConfig }) {
|
|
|
349
354
|
const lockUntil = deliveryLock.get(account.id);
|
|
350
355
|
if (lockUntil && new Date() < lockUntil) {
|
|
351
356
|
api.logger.debug?.(`[clawnet] ${account.id}: ${checkData.count} message(s) waiting (delivery lock active, skipping)`);
|
|
352
|
-
return a2aDmCount;
|
|
357
|
+
return { a2aDmCount, sentTaskUpdates };
|
|
353
358
|
}
|
|
354
359
|
|
|
355
360
|
state.lastInboxNonEmptyAt = new Date();
|
|
@@ -362,7 +367,7 @@ export function createClawnetService(params: { api: any; cfg: ClawnetConfig }) {
|
|
|
362
367
|
}
|
|
363
368
|
const inboxData = (await inboxRes.json()) as { messages: Array<Record<string, any>> };
|
|
364
369
|
|
|
365
|
-
if (inboxData.messages.length === 0) return a2aDmCount;
|
|
370
|
+
if (inboxData.messages.length === 0) return { a2aDmCount, sentTaskUpdates };
|
|
366
371
|
|
|
367
372
|
// Normalize API field names: API returns "from", plugin uses "from_agent"
|
|
368
373
|
const normalized: InboxMessage[] = inboxData.messages.map((m) => ({
|
|
@@ -380,7 +385,7 @@ export function createClawnetService(params: { api: any; cfg: ClawnetConfig }) {
|
|
|
380
385
|
pendingMessages.set(account.id, [...existing, ...normalized]);
|
|
381
386
|
scheduleFlush(account.id, account.agentId);
|
|
382
387
|
|
|
383
|
-
return a2aDmCount;
|
|
388
|
+
return { a2aDmCount, sentTaskUpdates };
|
|
384
389
|
}
|
|
385
390
|
|
|
386
391
|
async function pollAccountA2A(account: ClawnetAccount, a2aDmCount: number) {
|
|
@@ -464,6 +469,58 @@ export function createClawnetService(params: { api: any; cfg: ClawnetConfig }) {
|
|
|
464
469
|
}
|
|
465
470
|
}
|
|
466
471
|
|
|
472
|
+
async function pollSentTaskUpdates(account: ClawnetAccount) {
|
|
473
|
+
const resolvedToken = resolveToken(account.token);
|
|
474
|
+
if (!resolvedToken) return;
|
|
475
|
+
|
|
476
|
+
// Skip if delivery lock active
|
|
477
|
+
const lockUntil = deliveryLock.get(account.id);
|
|
478
|
+
if (lockUntil && new Date() < lockUntil) return;
|
|
479
|
+
|
|
480
|
+
// Fetch tasks I sent that need attention
|
|
481
|
+
const body = {
|
|
482
|
+
jsonrpc: "2.0",
|
|
483
|
+
id: `sent-poll-${Date.now()}`,
|
|
484
|
+
method: "tasks/list",
|
|
485
|
+
params: { role: "sender", status: "input-required", limit: 50 },
|
|
486
|
+
};
|
|
487
|
+
const res = await fetch(`${cfg.baseUrl}/a2a`, {
|
|
488
|
+
method: "POST",
|
|
489
|
+
headers: {
|
|
490
|
+
Authorization: `Bearer ${resolvedToken}`,
|
|
491
|
+
"Content-Type": "application/json",
|
|
492
|
+
},
|
|
493
|
+
body: JSON.stringify(body),
|
|
494
|
+
});
|
|
495
|
+
if (!res.ok) return;
|
|
496
|
+
|
|
497
|
+
const data = (await res.json()) as {
|
|
498
|
+
result?: { tasks: Array<Record<string, any>> };
|
|
499
|
+
};
|
|
500
|
+
const tasks = data.result?.tasks ?? [];
|
|
501
|
+
if (tasks.length === 0) return;
|
|
502
|
+
|
|
503
|
+
api.logger.info(`[clawnet] ${account.id}: ${tasks.length} sent task update(s) to deliver`);
|
|
504
|
+
|
|
505
|
+
const messages: InboxMessage[] = tasks.map((task) => {
|
|
506
|
+
const history = task.history as Array<{ role: string; parts: Array<{ text?: string }> }> ?? [];
|
|
507
|
+
const lastMsg = history[history.length - 1];
|
|
508
|
+
const text = lastMsg?.parts?.map((p: any) => p.text).filter(Boolean).join("\n") ?? "";
|
|
509
|
+
const taskState = task.state ?? "unknown";
|
|
510
|
+
return {
|
|
511
|
+
id: task.id,
|
|
512
|
+
from_agent: task.to, // the agent that responded
|
|
513
|
+
content: `[Task update: ${taskState}] Re: "${text.slice(0, 100)}${text.length > 100 ? "…" : ""}"`,
|
|
514
|
+
created_at: (task.status as any)?.timestamp ?? new Date().toISOString(),
|
|
515
|
+
};
|
|
516
|
+
});
|
|
517
|
+
|
|
518
|
+
state.counters.messagesSeen += messages.length;
|
|
519
|
+
const existing = pendingMessages.get(account.id) ?? [];
|
|
520
|
+
pendingMessages.set(account.id, [...existing, ...messages]);
|
|
521
|
+
scheduleFlush(account.id, account.agentId);
|
|
522
|
+
}
|
|
523
|
+
|
|
467
524
|
async function tick() {
|
|
468
525
|
if (stopped) return;
|
|
469
526
|
|
|
@@ -508,7 +565,7 @@ export function createClawnetService(params: { api: any; cfg: ClawnetConfig }) {
|
|
|
508
565
|
let hadError = false;
|
|
509
566
|
for (const account of enabledAccounts) {
|
|
510
567
|
try {
|
|
511
|
-
const a2aDmCount = await pollAccount(account);
|
|
568
|
+
const { a2aDmCount, sentTaskUpdates } = await pollAccount(account);
|
|
512
569
|
|
|
513
570
|
// Also poll for A2A DMs if any pending
|
|
514
571
|
if (a2aDmCount > 0) {
|
|
@@ -518,6 +575,15 @@ export function createClawnetService(params: { api: any; cfg: ClawnetConfig }) {
|
|
|
518
575
|
api.logger.error(`[clawnet] A2A poll error for ${account.id}: ${a2aErr.message}`);
|
|
519
576
|
}
|
|
520
577
|
}
|
|
578
|
+
|
|
579
|
+
// Poll for sent task updates (tasks I sent that got a response)
|
|
580
|
+
if (sentTaskUpdates > 0) {
|
|
581
|
+
try {
|
|
582
|
+
await pollSentTaskUpdates(account);
|
|
583
|
+
} catch (err: any) {
|
|
584
|
+
api.logger.error(`[clawnet] Sent task updates error for ${account.id}: ${err.message}`);
|
|
585
|
+
}
|
|
586
|
+
}
|
|
521
587
|
} catch (err: any) {
|
|
522
588
|
hadError = true;
|
|
523
589
|
state.lastError = { message: err.message, at: new Date() };
|
package/src/tools.ts
CHANGED
|
@@ -173,15 +173,11 @@ const BUILTIN_OPERATIONS: CapabilityOp[] = [
|
|
|
173
173
|
{ operation: "email.allowlist.remove", method: "DELETE", path: "/email/allowlist", description: "Remove sender from email allowlist", params: {
|
|
174
174
|
pattern: { type: "string", description: "Email address or pattern to remove", required: true },
|
|
175
175
|
}},
|
|
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: {
|
|
176
|
+
// Agent moderation
|
|
177
|
+
{ operation: "agent.block", method: "POST", path: "/block", description: "Block an agent from contacting you", params: {
|
|
182
178
|
agent_id: { type: "string", description: "Agent to block", required: true },
|
|
183
179
|
}},
|
|
184
|
-
{ operation: "
|
|
180
|
+
{ operation: "agent.unblock", method: "POST", path: "/unblock", description: "Unblock an agent", params: {
|
|
185
181
|
agent_id: { type: "string", description: "Agent to unblock", required: true },
|
|
186
182
|
}},
|
|
187
183
|
// Messages (cross-cutting)
|
|
@@ -222,8 +218,10 @@ const BUILTIN_OPERATIONS: CapabilityOp[] = [
|
|
|
222
218
|
title: { type: "string", description: "Event title", required: true },
|
|
223
219
|
starts_at: { type: "string", description: "ISO 8601 start time", required: true },
|
|
224
220
|
ends_at: { type: "string", description: "ISO 8601 end time" },
|
|
221
|
+
all_day: { type: "boolean", description: "Mark as all-day event (spans full calendar day)" },
|
|
225
222
|
location: { type: "string", description: "Event location" },
|
|
226
223
|
description: { type: "string", description: "Event description" },
|
|
224
|
+
remind_minutes: { type: "number", description: "Minutes before event to send notification (0-10080, default 15, null to disable)" },
|
|
227
225
|
attendees: { type: "array", description: "Array of {email, name?} — each gets a .ics invite" },
|
|
228
226
|
}},
|
|
229
227
|
{ operation: "calendar.list", method: "GET", path: "/calendar/events", description: "List calendar events", params: {
|
|
@@ -235,7 +233,11 @@ const BUILTIN_OPERATIONS: CapabilityOp[] = [
|
|
|
235
233
|
event_id: { type: "string", description: "Event ID", required: true },
|
|
236
234
|
title: { type: "string", description: "New title" },
|
|
237
235
|
starts_at: { type: "string", description: "New start time" },
|
|
236
|
+
ends_at: { type: "string", description: "New end time" },
|
|
237
|
+
all_day: { type: "boolean", description: "Mark as all-day event" },
|
|
238
238
|
location: { type: "string", description: "New location" },
|
|
239
|
+
description: { type: "string", description: "New description" },
|
|
240
|
+
remind_minutes: { type: "number", description: "Minutes before event to send notification (0-10080, null to disable)" },
|
|
239
241
|
}},
|
|
240
242
|
{ operation: "calendar.delete", method: "DELETE", path: "/calendar/events/:event_id", description: "Delete event (sends cancellation to attendees)", params: {
|
|
241
243
|
event_id: { type: "string", description: "Event ID", required: true },
|
|
@@ -335,7 +337,7 @@ export function registerTools(api: any) {
|
|
|
335
337
|
|
|
336
338
|
api.registerTool((ctx: { agentId?: string; sessionKey?: string }) => ({
|
|
337
339
|
name: "clawnet_inbox_check",
|
|
338
|
-
description: toolDesc("clawnet_inbox_check", "Check if you have new messages. Returns total count and breakdown by type (email,
|
|
340
|
+
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
341
|
parameters: {
|
|
340
342
|
type: "object",
|
|
341
343
|
properties: {},
|
|
@@ -436,7 +438,7 @@ export function registerTools(api: any) {
|
|
|
436
438
|
|
|
437
439
|
api.registerTool((ctx: { agentId?: string; sessionKey?: string }) => ({
|
|
438
440
|
name: "clawnet_inbox_session",
|
|
439
|
-
description: toolDesc("clawnet_inbox_session", "Start an interactive
|
|
441
|
+
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
442
|
parameters: {
|
|
441
443
|
type: "object",
|
|
442
444
|
properties: {
|
|
@@ -493,10 +495,40 @@ export function registerTools(api: any) {
|
|
|
493
495
|
};
|
|
494
496
|
});
|
|
495
497
|
|
|
498
|
+
// Fetch A2A tasks via REST-style POST to /a2a
|
|
499
|
+
let tasks: Array<Record<string, unknown>> = [];
|
|
500
|
+
try {
|
|
501
|
+
const taskResult = await apiCall(cfg, "POST", "/a2a", {
|
|
502
|
+
jsonrpc: "2.0",
|
|
503
|
+
id: `inbox-${Date.now()}`,
|
|
504
|
+
method: "tasks/list",
|
|
505
|
+
params: { status: "submitted,working" },
|
|
506
|
+
}, ctx?.agentId, ctx?.sessionKey);
|
|
507
|
+
const taskData = taskResult.data as any;
|
|
508
|
+
const rawTasks = taskData?.result?.tasks ?? taskData?.tasks ?? [];
|
|
509
|
+
tasks = rawTasks.map((t: any, i: number) => {
|
|
510
|
+
const lastMsg = (t.history ?? []).slice(-1)[0];
|
|
511
|
+
const text = lastMsg?.parts?.map((p: any) => p.text).filter(Boolean).join("\n") ?? "";
|
|
512
|
+
return {
|
|
513
|
+
n: emails.length + i + 1,
|
|
514
|
+
id: t.id,
|
|
515
|
+
type: "a2a_task",
|
|
516
|
+
from: t.from,
|
|
517
|
+
trust_tier: t.trustTier ?? "public",
|
|
518
|
+
content: text.slice(0, 200),
|
|
519
|
+
state: t.status?.state ?? "unknown",
|
|
520
|
+
received_at: t.status?.timestamp,
|
|
521
|
+
};
|
|
522
|
+
});
|
|
523
|
+
} catch {
|
|
524
|
+
// Non-fatal — show emails even if task fetch fails
|
|
525
|
+
}
|
|
526
|
+
|
|
496
527
|
return textResult({
|
|
497
528
|
protocol,
|
|
498
529
|
emails,
|
|
499
|
-
|
|
530
|
+
tasks,
|
|
531
|
+
counts: { total: emails.length + tasks.length, emails: emails.length, tasks: tasks.length, new: newCount, read: readCount },
|
|
500
532
|
});
|
|
501
533
|
},
|
|
502
534
|
}));
|
|
@@ -553,14 +585,19 @@ export function registerTools(api: any) {
|
|
|
553
585
|
parameters: {
|
|
554
586
|
type: "object",
|
|
555
587
|
properties: {
|
|
556
|
-
status: { type: "string", description: "Filter: '
|
|
588
|
+
status: { type: "string", description: "Filter: 'pending' (default — shows submitted + working), 'submitted', 'working', 'completed', 'failed', or 'all'" },
|
|
557
589
|
limit: { type: "number", description: "Max tasks (default 50, max 100)" },
|
|
558
590
|
},
|
|
559
591
|
},
|
|
560
592
|
async execute(_id: string, params: { status?: string; limit?: number }) {
|
|
561
593
|
const cfg = loadFreshConfig(api);
|
|
562
|
-
const a2aParams: Record<string, unknown> = {};
|
|
563
|
-
|
|
594
|
+
const a2aParams: Record<string, unknown> = { role: "recipient" };
|
|
595
|
+
const statusFilter = params.status || "pending";
|
|
596
|
+
if (statusFilter === "pending") {
|
|
597
|
+
a2aParams.status = "submitted,working";
|
|
598
|
+
} else {
|
|
599
|
+
a2aParams.status = statusFilter;
|
|
600
|
+
}
|
|
564
601
|
if (params.limit) a2aParams.limit = params.limit;
|
|
565
602
|
const result = await a2aCall(cfg, "/a2a", "tasks/list", a2aParams, ctx?.agentId, ctx?.sessionKey);
|
|
566
603
|
return textResult(result.data);
|
|
@@ -637,7 +674,7 @@ export function registerTools(api: any) {
|
|
|
637
674
|
parameters: {
|
|
638
675
|
type: "object",
|
|
639
676
|
properties: {
|
|
640
|
-
operation: { type: "string", description: "Operation name from clawnet_capabilities (e.g. '
|
|
677
|
+
operation: { type: "string", description: "Operation name from clawnet_capabilities (e.g. 'agent.block', 'profile.update', 'calendar.create')" },
|
|
641
678
|
params: { type: "object", description: "Operation parameters (see clawnet_capabilities for schema)" },
|
|
642
679
|
},
|
|
643
680
|
required: ["operation"],
|