@clwnt/clawnet 0.7.5 → 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 +29 -9
- package/src/cli.ts +1 -3
- package/src/config.ts +4 -0
- package/src/service.ts +157 -95
- package/src/tools.ts +51 -14
package/package.json
CHANGED
package/skills/clawnet/SKILL.md
CHANGED
|
@@ -1,19 +1,39 @@
|
|
|
1
1
|
# ClawNet Inbox Notification
|
|
2
2
|
|
|
3
|
-
New
|
|
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
|
-
Check your workspace files (AGENTS.md, MEMORY.md, TOOLS.md) for
|
|
7
|
+
1. Check your workspace files (AGENTS.md, MEMORY.md, TOOLS.md) for a matching rule
|
|
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`, then mark read via `clawnet_email_status { message_id: "...", status: "read" }`
|
|
8
10
|
|
|
9
|
-
##
|
|
11
|
+
## For each A2A task (messages starting with `[A2A Task`):
|
|
10
12
|
|
|
11
|
-
|
|
13
|
+
A2A tasks are requests from other agents on the network.
|
|
12
14
|
|
|
13
|
-
|
|
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]`
|
|
14
18
|
|
|
15
|
-
|
|
19
|
+
Keep it to one line per task. Your human will use /inbox to review or override.
|
|
16
20
|
|
|
17
|
-
|
|
21
|
+
## After processing all messages:
|
|
18
22
|
|
|
19
|
-
|
|
23
|
+
Remind your human they can ask to manage their inbox at any time.
|
|
24
|
+
|
|
25
|
+
## Example output:
|
|
26
|
+
|
|
27
|
+
📬 3 new messages:
|
|
28
|
+
|
|
29
|
+
✓ newsletters@example.com — "Weekly digest" (processed and archived by newsletter rule)
|
|
30
|
+
|
|
31
|
+
• jane@co.com — "Invoice #1234" — Invoice attached for March consulting work
|
|
32
|
+
|
|
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.
|
|
38
|
+
|
|
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
|
|
|
@@ -118,59 +123,16 @@ export function createClawnetService(params: { api: any; cfg: ClawnetConfig }) {
|
|
|
118
123
|
// --- Message formatting ---
|
|
119
124
|
|
|
120
125
|
function formatMessage(msg: InboxMessage) {
|
|
121
|
-
const isEmail = msg.from_agent.includes("@");
|
|
122
126
|
return {
|
|
123
127
|
id: msg.id,
|
|
124
128
|
from_agent: msg.from_agent,
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
...(isEmail
|
|
128
|
-
? { subject: msg.subject ?? "(no subject)" }
|
|
129
|
-
: { content: msg.content }),
|
|
129
|
+
content: msg.content,
|
|
130
|
+
...(msg.subject ? { subject: msg.subject } : {}),
|
|
130
131
|
created_at: msg.created_at,
|
|
131
132
|
};
|
|
132
133
|
}
|
|
133
134
|
|
|
134
|
-
// ---
|
|
135
|
-
|
|
136
|
-
async function fetchConversationHistory(
|
|
137
|
-
senderIds: string[],
|
|
138
|
-
resolvedToken: string,
|
|
139
|
-
): Promise<string> {
|
|
140
|
-
if (senderIds.length === 0) return "";
|
|
141
|
-
|
|
142
|
-
const sections: string[] = [];
|
|
143
|
-
for (const sender of senderIds) {
|
|
144
|
-
try {
|
|
145
|
-
const encoded = encodeURIComponent(sender);
|
|
146
|
-
const res = await fetch(`${cfg.baseUrl}/messages/${encoded}?limit=10`, {
|
|
147
|
-
headers: {
|
|
148
|
-
Authorization: `Bearer ${resolvedToken}`,
|
|
149
|
-
"Content-Type": "application/json",
|
|
150
|
-
},
|
|
151
|
-
});
|
|
152
|
-
if (!res.ok) continue;
|
|
153
|
-
const data = (await res.json()) as {
|
|
154
|
-
messages: { from: string; to: string; content: string; created_at: string }[];
|
|
155
|
-
};
|
|
156
|
-
if (!data.messages || data.messages.length === 0) continue;
|
|
157
|
-
|
|
158
|
-
// Format oldest-first for natural reading order
|
|
159
|
-
const lines = data.messages
|
|
160
|
-
.reverse()
|
|
161
|
-
.map((m) => ` [${m.created_at}] ${m.from} → ${m.to}: ${m.content}`);
|
|
162
|
-
sections.push(`Conversation with ${sender} (last ${data.messages.length} messages):\n${lines.join("\n")}`);
|
|
163
|
-
} catch {
|
|
164
|
-
// Non-fatal — skip this sender's history
|
|
165
|
-
}
|
|
166
|
-
}
|
|
167
|
-
|
|
168
|
-
return sections.length > 0
|
|
169
|
-
? sections.join("\n\n")
|
|
170
|
-
: "";
|
|
171
|
-
}
|
|
172
|
-
|
|
173
|
-
// --- Batch delivery to hook ---
|
|
135
|
+
// --- Batch delivery ---
|
|
174
136
|
|
|
175
137
|
async function deliverBatch(accountId: string, agentId: string, messages: InboxMessage[]) {
|
|
176
138
|
if (messages.length === 0) return;
|
|
@@ -178,7 +140,6 @@ export function createClawnetService(params: { api: any; cfg: ClawnetConfig }) {
|
|
|
178
140
|
// Concurrency guard
|
|
179
141
|
if (accountBusy.has(accountId)) {
|
|
180
142
|
api.logger.info(`[clawnet] ${accountId}: LLM run in progress, requeueing ${messages.length} message(s)`);
|
|
181
|
-
// Put them back in pending for next cycle
|
|
182
143
|
const existing = pendingMessages.get(accountId) ?? [];
|
|
183
144
|
pendingMessages.set(accountId, [...existing, ...messages]);
|
|
184
145
|
return;
|
|
@@ -187,53 +148,20 @@ export function createClawnetService(params: { api: any; cfg: ClawnetConfig }) {
|
|
|
187
148
|
accountBusy.add(accountId);
|
|
188
149
|
|
|
189
150
|
try {
|
|
190
|
-
|
|
191
|
-
const
|
|
192
|
-
|
|
193
|
-
// Always send as array — same field names as the API response
|
|
194
|
-
const items = messages.map((msg) => formatMessage(msg));
|
|
195
|
-
|
|
196
|
-
// Fetch conversation history for DM senders (non-email)
|
|
197
|
-
let context = "";
|
|
198
|
-
const account = cfg.accounts.find((a) => a.id === accountId);
|
|
199
|
-
const apiToken = account ? resolveToken(account.token) : "";
|
|
200
|
-
if (apiToken) {
|
|
201
|
-
const dmSenders = [...new Set(
|
|
202
|
-
messages
|
|
203
|
-
.map((m) => m.from_agent)
|
|
204
|
-
.filter((sender) => !sender.includes("@")),
|
|
205
|
-
)];
|
|
206
|
-
context = await fetchConversationHistory(dmSenders, apiToken);
|
|
207
|
-
}
|
|
208
|
-
|
|
209
|
-
const payload: Record<string, unknown> = {
|
|
210
|
-
agent_id: agentId,
|
|
211
|
-
count: items.length,
|
|
212
|
-
messages: items,
|
|
213
|
-
};
|
|
214
|
-
if (context) {
|
|
215
|
-
payload.context = context;
|
|
216
|
-
}
|
|
151
|
+
// Re-read config to pick up deliveryMethod changes without restart
|
|
152
|
+
const freshCfg = loadFreshConfig(api);
|
|
217
153
|
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
...(hooksToken ? { Authorization: `Bearer ${hooksToken}` } : {}),
|
|
223
|
-
},
|
|
224
|
-
body: JSON.stringify(payload),
|
|
225
|
-
});
|
|
226
|
-
|
|
227
|
-
if (!res.ok) {
|
|
228
|
-
const body = await res.text().catch(() => "");
|
|
229
|
-
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);
|
|
230
158
|
}
|
|
231
159
|
|
|
232
160
|
state.counters.batchesSent++;
|
|
233
161
|
state.counters.delivered += messages.length;
|
|
234
162
|
deliveryLock.set(accountId, new Date(Date.now() + DELIVERY_LOCK_TTL_MS));
|
|
235
163
|
api.logger.info(
|
|
236
|
-
`[clawnet] ${accountId}: delivered ${messages.length} message(s) to ${agentId}`,
|
|
164
|
+
`[clawnet] ${accountId}: delivered ${messages.length} message(s) to ${agentId} via ${freshCfg.deliveryMethod}`,
|
|
237
165
|
);
|
|
238
166
|
} catch (err: any) {
|
|
239
167
|
state.lastError = { message: err.message, at: new Date() };
|
|
@@ -244,6 +172,77 @@ export function createClawnetService(params: { api: any; cfg: ClawnetConfig }) {
|
|
|
244
172
|
}
|
|
245
173
|
}
|
|
246
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
|
+
|
|
247
246
|
// --- Debounced flush: wait for more messages, then deliver ---
|
|
248
247
|
|
|
249
248
|
function scheduleFlush(accountId: string, agentId: string) {
|
|
@@ -306,7 +305,8 @@ export function createClawnetService(params: { api: any; cfg: ClawnetConfig }) {
|
|
|
306
305
|
}
|
|
307
306
|
const checkData = (await checkRes.json()) as {
|
|
308
307
|
count: number;
|
|
309
|
-
|
|
308
|
+
task_count?: number;
|
|
309
|
+
sent_task_updates?: number;
|
|
310
310
|
plugin_config?: {
|
|
311
311
|
poll_seconds: number;
|
|
312
312
|
debounce_seconds: number;
|
|
@@ -340,12 +340,13 @@ export function createClawnetService(params: { api: any; cfg: ClawnetConfig }) {
|
|
|
340
340
|
}
|
|
341
341
|
}
|
|
342
342
|
|
|
343
|
-
const a2aDmCount = checkData.
|
|
343
|
+
const a2aDmCount = checkData.task_count ?? 0;
|
|
344
|
+
const sentTaskUpdates = checkData.sent_task_updates ?? 0;
|
|
344
345
|
|
|
345
346
|
if (checkData.count === 0) {
|
|
346
347
|
// Email inbox clear — release any delivery lock (agent finished processing)
|
|
347
348
|
deliveryLock.delete(account.id);
|
|
348
|
-
return a2aDmCount;
|
|
349
|
+
return { a2aDmCount, sentTaskUpdates };
|
|
349
350
|
}
|
|
350
351
|
|
|
351
352
|
// Skip if a recent webhook delivery is still being processed by the LLM.
|
|
@@ -353,7 +354,7 @@ export function createClawnetService(params: { api: any; cfg: ClawnetConfig }) {
|
|
|
353
354
|
const lockUntil = deliveryLock.get(account.id);
|
|
354
355
|
if (lockUntil && new Date() < lockUntil) {
|
|
355
356
|
api.logger.debug?.(`[clawnet] ${account.id}: ${checkData.count} message(s) waiting (delivery lock active, skipping)`);
|
|
356
|
-
return a2aDmCount;
|
|
357
|
+
return { a2aDmCount, sentTaskUpdates };
|
|
357
358
|
}
|
|
358
359
|
|
|
359
360
|
state.lastInboxNonEmptyAt = new Date();
|
|
@@ -366,7 +367,7 @@ export function createClawnetService(params: { api: any; cfg: ClawnetConfig }) {
|
|
|
366
367
|
}
|
|
367
368
|
const inboxData = (await inboxRes.json()) as { messages: Array<Record<string, any>> };
|
|
368
369
|
|
|
369
|
-
if (inboxData.messages.length === 0) return a2aDmCount;
|
|
370
|
+
if (inboxData.messages.length === 0) return { a2aDmCount, sentTaskUpdates };
|
|
370
371
|
|
|
371
372
|
// Normalize API field names: API returns "from", plugin uses "from_agent"
|
|
372
373
|
const normalized: InboxMessage[] = inboxData.messages.map((m) => ({
|
|
@@ -384,7 +385,7 @@ export function createClawnetService(params: { api: any; cfg: ClawnetConfig }) {
|
|
|
384
385
|
pendingMessages.set(account.id, [...existing, ...normalized]);
|
|
385
386
|
scheduleFlush(account.id, account.agentId);
|
|
386
387
|
|
|
387
|
-
return a2aDmCount;
|
|
388
|
+
return { a2aDmCount, sentTaskUpdates };
|
|
388
389
|
}
|
|
389
390
|
|
|
390
391
|
async function pollAccountA2A(account: ClawnetAccount, a2aDmCount: number) {
|
|
@@ -468,6 +469,58 @@ export function createClawnetService(params: { api: any; cfg: ClawnetConfig }) {
|
|
|
468
469
|
}
|
|
469
470
|
}
|
|
470
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
|
+
|
|
471
524
|
async function tick() {
|
|
472
525
|
if (stopped) return;
|
|
473
526
|
|
|
@@ -512,7 +565,7 @@ export function createClawnetService(params: { api: any; cfg: ClawnetConfig }) {
|
|
|
512
565
|
let hadError = false;
|
|
513
566
|
for (const account of enabledAccounts) {
|
|
514
567
|
try {
|
|
515
|
-
const a2aDmCount = await pollAccount(account);
|
|
568
|
+
const { a2aDmCount, sentTaskUpdates } = await pollAccount(account);
|
|
516
569
|
|
|
517
570
|
// Also poll for A2A DMs if any pending
|
|
518
571
|
if (a2aDmCount > 0) {
|
|
@@ -522,6 +575,15 @@ export function createClawnetService(params: { api: any; cfg: ClawnetConfig }) {
|
|
|
522
575
|
api.logger.error(`[clawnet] A2A poll error for ${account.id}: ${a2aErr.message}`);
|
|
523
576
|
}
|
|
524
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
|
+
}
|
|
525
587
|
} catch (err: any) {
|
|
526
588
|
hadError = true;
|
|
527
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"],
|