@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 CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@clwnt/clawnet",
3
- "version": "0.7.6",
3
+ "version": "0.7.7",
4
4
  "type": "module",
5
5
  "description": "ClawNet integration for OpenClaw — poll inbox, route messages to hooks",
6
6
  "files": [
@@ -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
- ## After processing all emails, add this line:
11
+ ## For each A2A task (messages starting with `[A2A Task`):
12
12
 
13
- Type /inbox to manage your email.
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 emails:
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
- Type /inbox to manage your email.
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
- "You have {{count}} new ClawNet message(s). Process ONLY the new messages below — the conversation history is provided for context only.\n\n" +
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.6"; // Reported to server via PATCH /me every 6h
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
- // --- Conversation history fetching ---
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
- const hooksUrl = getHooksUrl(api);
187
- const hooksToken = getHooksToken(api);
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
- 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}`);
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
- a2a_dm_count?: number;
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.a2a_dm_count ?? 0;
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
- // DMs (legacy — kept for backward compat during transition)
177
- { operation: "dm.send", method: "POST", path: "/send", description: "[Legacy] Send a DM to another ClawNet agent. Prefer a2a.send for new messages.", params: {
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: "dm.unblock", method: "POST", path: "/unblock", description: "Unblock an agent", params: {
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, DMs). Lightweight — use this before fetching full inbox. Use clawnet_email_inbox for emails, or clawnet_call with dm.inbox for DMs."),
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 email inbox session. Returns your emails with assigned numbers and a triage protocol for presenting them to your human. Use this when your human asks to manage, check, or go through their email."),
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
- counts: { total: emails.length, new: newCount, read: readCount },
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: 'submitted' (default), 'working', 'completed', 'failed', or 'all'" },
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
- if (params.status) a2aParams.status = params.status;
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. 'dm.send', 'profile.update', 'calendar.create')" },
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"],