@clwnt/clawnet 0.7.7 → 0.7.9

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.7",
3
+ "version": "0.7.9",
4
4
  "type": "module",
5
5
  "description": "ClawNet integration for OpenClaw — poll inbox, route messages to hooks",
6
6
  "files": [
package/src/cli.ts CHANGED
@@ -154,6 +154,10 @@ export function buildStatusText(api: any): string {
154
154
  lines.push("Polling: **PAUSED** (run /clawnet resume to restart)");
155
155
  }
156
156
  lines.push(`Poll interval: ${pluginCfg.pollEverySeconds ?? "?"}s`);
157
+ const notifyOn = pluginCfg.notifyOnNew ?? true;
158
+ const remindH = pluginCfg.remindAfterHours ?? null;
159
+ lines.push(`Notify on new: ${notifyOn ? "on" : "off"}`);
160
+ lines.push(`Reminder interval: ${remindH ? `${remindH}h` : "never"}`);
157
161
 
158
162
  const accounts: any[] = pluginCfg.accounts ?? [];
159
163
  const agentList: any[] = currentConfig?.agents?.list ?? [];
package/src/config.ts CHANGED
@@ -19,6 +19,8 @@ export interface ClawnetConfig {
19
19
  maxSnippetChars: number;
20
20
  setupVersion: number;
21
21
  paused: boolean;
22
+ notifyOnNew: boolean;
23
+ remindAfterHours: number | null;
22
24
  }
23
25
 
24
26
  const DEFAULTS: ClawnetConfig = {
@@ -32,6 +34,8 @@ const DEFAULTS: ClawnetConfig = {
32
34
  maxSnippetChars: 500,
33
35
  setupVersion: 0,
34
36
  paused: false,
37
+ notifyOnNew: true,
38
+ remindAfterHours: 4,
35
39
  };
36
40
 
37
41
  export function parseConfig(raw: Record<string, unknown>): ClawnetConfig {
@@ -67,6 +71,8 @@ export function parseConfig(raw: Record<string, unknown>): ClawnetConfig {
67
71
  deliveryMethod:
68
72
  raw.deliveryMethod === "agent" ? "agent" : DEFAULTS.deliveryMethod,
69
73
  paused: raw.paused === true,
74
+ notifyOnNew: raw.notifyOnNew !== false,
75
+ remindAfterHours: typeof raw.remindAfterHours === "number" ? raw.remindAfterHours : null,
70
76
  };
71
77
  }
72
78
 
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,7 @@ 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.7"; // Reported to server via PATCH /me every 6h
76
+ export const PLUGIN_VERSION = "0.7.8"; // Reported to server via PATCH /me every 6h
76
77
 
77
78
  function loadFreshConfig(api: any): ClawnetConfig {
78
79
  const raw = api.runtime?.config?.loadConfig?.()?.plugins?.entries?.clawnet?.config ?? {};
@@ -134,6 +135,9 @@ export function createClawnetService(params: { api: any; cfg: ClawnetConfig }) {
134
135
 
135
136
  // --- Batch delivery ---
136
137
 
138
+ // Per-account auth context for mark-notified calls from deliverBatch
139
+ const accountAuth = new Map<string, { token: string; baseUrl: string }>();
140
+
137
141
  async function deliverBatch(accountId: string, agentId: string, messages: InboxMessage[]) {
138
142
  if (messages.length === 0) return;
139
143
 
@@ -163,6 +167,37 @@ export function createClawnetService(params: { api: any; cfg: ClawnetConfig }) {
163
167
  api.logger.info(
164
168
  `[clawnet] ${accountId}: delivered ${messages.length} message(s) to ${agentId} via ${freshCfg.deliveryMethod}`,
165
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
+ const markRes = 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
+ if (markRes.ok) {
189
+ const markData = await markRes.json().catch(() => ({})) as Record<string, unknown>;
190
+ api.logger.info(`[clawnet] ${accountId}: marked notified (${markData.marked_messages ?? 0} msgs, ${markData.marked_tasks ?? 0} tasks)`);
191
+ } else {
192
+ const errText = await markRes.text().catch(() => "");
193
+ api.logger.warn(`[clawnet] ${accountId}: mark-notified returned ${markRes.status}: ${errText}`);
194
+ }
195
+ } catch (err: any) {
196
+ api.logger.warn(`[clawnet] ${accountId}: mark-notified failed (non-fatal): ${err.message}`);
197
+ }
198
+ }
199
+
200
+ }
166
201
  } catch (err: any) {
167
202
  state.lastError = { message: err.message, at: new Date() };
168
203
  state.counters.errors++;
@@ -286,13 +321,16 @@ export function createClawnetService(params: { api: any; cfg: ClawnetConfig }) {
286
321
 
287
322
  // --- Poll ---
288
323
 
289
- async function pollAccount(account: ClawnetAccount): Promise<number> {
324
+ async function pollAccount(account: ClawnetAccount): Promise<{ a2aDmCount: number; sentTaskUpdates: number; notifyCount: number }> {
290
325
  const resolvedToken = resolveToken(account.token);
291
326
  if (!resolvedToken) {
292
327
  api.logger.warn(`[clawnet] No token resolved for account "${account.id}", skipping`);
293
- return 0;
328
+ return { a2aDmCount: 0, sentTaskUpdates: 0, notifyCount: 0 };
294
329
  }
295
330
 
331
+ // Store auth context for deliverBatch to use for mark-notified calls
332
+ accountAuth.set(account.id, { token: resolvedToken, baseUrl: cfg.baseUrl });
333
+
296
334
  const headers = {
297
335
  Authorization: `Bearer ${resolvedToken}`,
298
336
  "Content-Type": "application/json",
@@ -307,11 +345,14 @@ export function createClawnetService(params: { api: any; cfg: ClawnetConfig }) {
307
345
  count: number;
308
346
  task_count?: number;
309
347
  sent_task_updates?: number;
348
+ notify_count?: number;
310
349
  plugin_config?: {
311
350
  poll_seconds: number;
312
351
  debounce_seconds: number;
313
352
  max_batch_size: number;
314
353
  deliver_channel: string;
354
+ notify_on_new?: boolean;
355
+ remind_after_hours?: number | null;
315
356
  };
316
357
  };
317
358
 
@@ -335,30 +376,44 @@ export function createClawnetService(params: { api: any; cfg: ClawnetConfig }) {
335
376
  cfg.deliver.channel = pc.deliver_channel;
336
377
  changed = true;
337
378
  }
379
+ if (pc.notify_on_new !== undefined && pc.notify_on_new !== cfg.notifyOnNew) {
380
+ cfg.notifyOnNew = pc.notify_on_new;
381
+ changed = true;
382
+ }
383
+ if (pc.remind_after_hours !== undefined && pc.remind_after_hours !== cfg.remindAfterHours) {
384
+ cfg.remindAfterHours = pc.remind_after_hours;
385
+ changed = true;
386
+ }
338
387
  if (changed) {
339
- api.logger.info(`[clawnet] Config updated from server: poll=${cfg.pollEverySeconds}s debounce=${cfg.debounceSeconds}s batch=${cfg.maxBatchSize}`);
388
+ api.logger.info(`[clawnet] Config updated from server: poll=${cfg.pollEverySeconds}s debounce=${cfg.debounceSeconds}s batch=${cfg.maxBatchSize} notify=${cfg.notifyOnNew} remind=${cfg.remindAfterHours ?? "never"}`);
340
389
  }
341
390
  }
342
391
 
343
392
  const a2aDmCount = checkData.task_count ?? 0;
344
393
  const sentTaskUpdates = checkData.sent_task_updates ?? 0;
394
+ const notifyCount = checkData.notify_count ?? (checkData.count + a2aDmCount + sentTaskUpdates);
345
395
 
346
396
  if (checkData.count === 0) {
347
397
  // Email inbox clear — release any delivery lock (agent finished processing)
348
398
  deliveryLock.delete(account.id);
349
- return { a2aDmCount, sentTaskUpdates };
399
+ return { a2aDmCount, sentTaskUpdates, notifyCount };
400
+ }
401
+
402
+ // If nothing needs notification, skip fetch (but don't release lock — inbox still has items)
403
+ if (notifyCount === 0) {
404
+ return { a2aDmCount, sentTaskUpdates, notifyCount };
350
405
  }
351
406
 
352
- // Skip if a recent webhook delivery is still being processed by the LLM.
407
+ // Skip if a recent delivery is still being processed.
353
408
  // TTL-based lock: after successful POST, lock for 10 min to let the agent work.
354
409
  const lockUntil = deliveryLock.get(account.id);
355
410
  if (lockUntil && new Date() < lockUntil) {
356
411
  api.logger.debug?.(`[clawnet] ${account.id}: ${checkData.count} message(s) waiting (delivery lock active, skipping)`);
357
- return { a2aDmCount, sentTaskUpdates };
412
+ return { a2aDmCount, sentTaskUpdates, notifyCount };
358
413
  }
359
414
 
360
415
  state.lastInboxNonEmptyAt = new Date();
361
- api.logger.info(`[clawnet] ${account.id}: ${checkData.count} message(s) waiting`);
416
+ api.logger.info(`[clawnet] ${account.id}: ${checkData.count} message(s) waiting (${notifyCount} to notify)`);
362
417
 
363
418
  // Fetch full messages
364
419
  const inboxRes = await fetch(`${cfg.baseUrl}/inbox`, { headers });
@@ -367,7 +422,7 @@ export function createClawnetService(params: { api: any; cfg: ClawnetConfig }) {
367
422
  }
368
423
  const inboxData = (await inboxRes.json()) as { messages: Array<Record<string, any>> };
369
424
 
370
- if (inboxData.messages.length === 0) return { a2aDmCount, sentTaskUpdates };
425
+ if (inboxData.messages.length === 0) return { a2aDmCount, sentTaskUpdates, notifyCount };
371
426
 
372
427
  // Normalize API field names: API returns "from", plugin uses "from_agent"
373
428
  const normalized: InboxMessage[] = inboxData.messages.map((m) => ({
@@ -376,6 +431,7 @@ export function createClawnetService(params: { api: any; cfg: ClawnetConfig }) {
376
431
  content: m.content,
377
432
  subject: m.email?.subject ?? m.subject,
378
433
  created_at: m.created_at,
434
+ type: "email" as const,
379
435
  }));
380
436
 
381
437
  state.counters.messagesSeen += normalized.length;
@@ -385,7 +441,7 @@ export function createClawnetService(params: { api: any; cfg: ClawnetConfig }) {
385
441
  pendingMessages.set(account.id, [...existing, ...normalized]);
386
442
  scheduleFlush(account.id, account.agentId);
387
443
 
388
- return { a2aDmCount, sentTaskUpdates };
444
+ return { a2aDmCount, sentTaskUpdates, notifyCount };
389
445
  }
390
446
 
391
447
  async function pollAccountA2A(account: ClawnetAccount, a2aDmCount: number) {
@@ -427,7 +483,8 @@ export function createClawnetService(params: { api: any; cfg: ClawnetConfig }) {
427
483
 
428
484
  api.logger.info(`[clawnet] ${account.id}: ${tasks.length} A2A task(s) to deliver`);
429
485
 
430
- // Convert A2A tasks to the message format the hook expects
486
+ // Convert A2A tasks to the message format for delivery
487
+ // mark-notified happens post-delivery in deliverBatch
431
488
  const messages: InboxMessage[] = tasks.map((task) => {
432
489
  const history = task.history as Array<{ role: string; parts: Array<{ text?: string }> }> ?? [];
433
490
  const lastMsg = history[history.length - 1];
@@ -438,6 +495,7 @@ export function createClawnetService(params: { api: any; cfg: ClawnetConfig }) {
438
495
  from_agent: task.from,
439
496
  content: `[A2A Task ${task.id}]${contactInfo}\n${text}`,
440
497
  created_at: (task.status as any)?.timestamp ?? new Date().toISOString(),
498
+ type: "task" as const,
441
499
  };
442
500
  });
443
501
 
@@ -445,28 +503,6 @@ export function createClawnetService(params: { api: any; cfg: ClawnetConfig }) {
445
503
  const existing = pendingMessages.get(account.id) ?? [];
446
504
  pendingMessages.set(account.id, [...existing, ...messages]);
447
505
  scheduleFlush(account.id, account.agentId);
448
-
449
- // Mark delivered tasks as 'working' so they don't get re-delivered on next poll.
450
- // This is the equivalent of marking emails 'read' — acknowledges receipt.
451
- for (const task of tasks) {
452
- try {
453
- await fetch(`${cfg.baseUrl}/a2a`, {
454
- method: "POST",
455
- headers: {
456
- Authorization: `Bearer ${resolvedToken}`,
457
- "Content-Type": "application/json",
458
- },
459
- body: JSON.stringify({
460
- jsonrpc: "2.0",
461
- id: `ack-${task.id}`,
462
- method: "tasks/respond",
463
- params: { id: task.id, state: "working" },
464
- }),
465
- });
466
- } catch {
467
- // Non-fatal — task may get re-delivered next cycle
468
- }
469
- }
470
506
  }
471
507
 
472
508
  async function pollSentTaskUpdates(account: ClawnetAccount) {
@@ -477,12 +513,12 @@ export function createClawnetService(params: { api: any; cfg: ClawnetConfig }) {
477
513
  const lockUntil = deliveryLock.get(account.id);
478
514
  if (lockUntil && new Date() < lockUntil) return;
479
515
 
480
- // Fetch tasks I sent that need attention
516
+ // Fetch tasks I sent that need my attention or have finished
481
517
  const body = {
482
518
  jsonrpc: "2.0",
483
519
  id: `sent-poll-${Date.now()}`,
484
520
  method: "tasks/list",
485
- params: { role: "sender", status: "input-required", limit: 50 },
521
+ params: { role: "sender", status: "input-required,completed,failed", limit: 50 },
486
522
  };
487
523
  const res = await fetch(`${cfg.baseUrl}/a2a`, {
488
524
  method: "POST",
@@ -512,6 +548,7 @@ export function createClawnetService(params: { api: any; cfg: ClawnetConfig }) {
512
548
  from_agent: task.to, // the agent that responded
513
549
  content: `[Task update: ${taskState}] Re: "${text.slice(0, 100)}${text.length > 100 ? "…" : ""}"`,
514
550
  created_at: (task.status as any)?.timestamp ?? new Date().toISOString(),
551
+ type: "task" as const,
515
552
  };
516
553
  });
517
554
 
@@ -565,10 +602,10 @@ export function createClawnetService(params: { api: any; cfg: ClawnetConfig }) {
565
602
  let hadError = false;
566
603
  for (const account of enabledAccounts) {
567
604
  try {
568
- const { a2aDmCount, sentTaskUpdates } = await pollAccount(account);
605
+ const { a2aDmCount, sentTaskUpdates, notifyCount } = await pollAccount(account);
569
606
 
570
607
  // Also poll for A2A DMs if any pending
571
- if (a2aDmCount > 0) {
608
+ if (a2aDmCount > 0 && notifyCount > 0) {
572
609
  try {
573
610
  await pollAccountA2A(account, a2aDmCount);
574
611
  } catch (a2aErr: any) {
@@ -577,7 +614,7 @@ export function createClawnetService(params: { api: any; cfg: ClawnetConfig }) {
577
614
  }
578
615
 
579
616
  // Poll for sent task updates (tasks I sent that got a response)
580
- if (sentTaskUpdates > 0) {
617
+ if (sentTaskUpdates > 0 && notifyCount > 0) {
581
618
  try {
582
619
  await pollSentTaskUpdates(account);
583
620
  } catch (err: any) {
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[] = [
@@ -201,6 +203,12 @@ const BUILTIN_OPERATIONS: CapabilityOp[] = [
201
203
  { operation: "profile.capabilities", method: "PATCH", path: "/me/capabilities", description: "Set agent capabilities list", params: {
202
204
  capabilities: { type: "array", description: "List of capability strings (replaces all)", required: true },
203
205
  }},
206
+ // Notification settings
207
+ { operation: "notifications.get", method: "GET", path: "/me/notifications", description: "Get notification preferences (notify_on_new, remind_after_hours)" },
208
+ { operation: "notifications.update", method: "PATCH", path: "/me/notifications", description: "Update notification preferences", params: {
209
+ notify_on_new: { type: "boolean", description: "Notify when new messages/tasks arrive (default: true)" },
210
+ remind_after_hours: { type: "number", description: "Re-notify about unresolved items every N hours (1-168), or null to disable reminders" },
211
+ }},
204
212
  // Contacts
205
213
  { operation: "contacts.list", method: "GET", path: "/contacts", description: "List your contacts", params: {
206
214
  type: { type: "string", description: "'email' or 'agent'" },
@@ -268,6 +276,30 @@ const BUILTIN_OPERATIONS: CapabilityOp[] = [
268
276
  { operation: "account.rate_limits", method: "GET", path: "/me/rate-limits", description: "Check your current rate limits" },
269
277
  // Docs
270
278
  { operation: "docs.help", method: "GET", path: "/docs/skill", description: "Get the full ClawNet documentation — features, usage examples, safety rules, setup, troubleshooting, and rate limits" },
279
+ // A2A (JSON-RPC via /a2a)
280
+ { operation: "a2a.card.update", jsonrpc: true, method: "POST", path: "/a2a", rpc_method: "card/update", description: "Update your A2A Agent Card skills", params: {
281
+ skills: { type: "array", description: "Array of {id, name, description} skill objects", required: true },
282
+ }},
283
+ { operation: "a2a.tasks.list", jsonrpc: true, method: "POST", path: "/a2a", rpc_method: "tasks/list", description: "List your A2A tasks", params: {
284
+ status: { type: "string", description: "Filter by status (e.g. 'submitted', 'working', 'completed', 'failed', comma-separated for multiple)" },
285
+ role: { type: "string", description: "'sender' or 'recipient'" },
286
+ limit: { type: "number", description: "Max tasks to return" },
287
+ }},
288
+ { operation: "a2a.tasks.get", jsonrpc: true, method: "POST", path: "/a2a", rpc_method: "tasks/get", description: "Get a specific A2A task by ID", params: {
289
+ id: { type: "string", description: "Task ID", required: true },
290
+ }},
291
+ { operation: "a2a.tasks.respond", jsonrpc: true, method: "POST", path: "/a2a", rpc_method: "tasks/respond", description: "Respond to an A2A task", params: {
292
+ id: { type: "string", description: "Task ID", required: true },
293
+ state: { type: "string", description: "New state: completed, input-required, working, or failed", required: true },
294
+ message: { type: "string", description: "Response message text" },
295
+ artifacts: { type: "array", description: "Array of artifact objects (for completed tasks)" },
296
+ }},
297
+ { operation: "a2a.tasks.cancel", jsonrpc: true, method: "POST", path: "/a2a", rpc_method: "tasks/cancel", description: "Cancel an A2A task", params: {
298
+ id: { type: "string", description: "Task ID", required: true },
299
+ }},
300
+ { operation: "a2a.tasks.count", jsonrpc: true, method: "POST", path: "/a2a", rpc_method: "tasks/count", description: "Count A2A tasks by status", params: {
301
+ status: { type: "string", description: "Filter by status" },
302
+ }},
271
303
  ];
272
304
 
273
305
  // --- Dynamic capabilities ---
@@ -537,11 +569,11 @@ export function registerTools(api: any) {
537
569
 
538
570
  api.registerTool((ctx: { agentId?: string; sessionKey?: string }) => ({
539
571
  name: "clawnet_task_send",
540
- 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."),
572
+ 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."),
541
573
  parameters: {
542
574
  type: "object",
543
575
  properties: {
544
- to: { type: "string", description: "Recipient agent name" },
576
+ to: { type: "string", description: "Recipient: agent name (e.g. 'Tom') or external A2A URL (e.g. 'https://example.com/a2a/agent')" },
545
577
  message: { type: "string", description: "Message content" },
546
578
  task_id: { type: "string", description: "If following up on a task (after agent asked for input), provide the task ID" },
547
579
  },
@@ -549,6 +581,26 @@ export function registerTools(api: any) {
549
581
  },
550
582
  async execute(_id: string, params: { to: string; message: string; task_id?: string }) {
551
583
  const cfg = loadFreshConfig(api);
584
+ const isExternalUrl = params.to.startsWith("https://") || params.to.startsWith("http://");
585
+
586
+ if (params.to.startsWith("http://")) {
587
+ return textResult({ error: "Only HTTPS URLs are supported for external A2A endpoints. Use https:// instead of http://." });
588
+ }
589
+
590
+ if (isExternalUrl) {
591
+ // External A2A: route through tasks/send-external on internal endpoint
592
+ const a2aParams: Record<string, unknown> = {
593
+ url: params.to,
594
+ message: { role: "user", parts: [{ kind: "text", text: params.message }] },
595
+ };
596
+ if (params.task_id) {
597
+ a2aParams.taskId = params.task_id;
598
+ }
599
+ const result = await a2aCall(cfg, "/a2a", "tasks/send-external", a2aParams, ctx?.agentId, ctx?.sessionKey);
600
+ return textResult(result.data);
601
+ }
602
+
603
+ // Internal ClawNet agent: existing path
552
604
  const a2aParams: Record<string, unknown> = {
553
605
  message: { role: "user", parts: [{ kind: "text", text: params.message }] },
554
606
  };
@@ -720,6 +772,16 @@ export function registerTools(api: any) {
720
772
  if (query) path += (path.includes('?') ? '&' : '?') + query;
721
773
  }
722
774
 
775
+ // JSON-RPC operations: dispatch via a2aCall()
776
+ if (op.jsonrpc && op.rpc_method) {
777
+ const rpcParams: Record<string, unknown> = {};
778
+ for (const [key, val] of Object.entries(params)) {
779
+ if (val !== undefined) rpcParams[key] = val;
780
+ }
781
+ const result = await a2aCall(cfg, op.path, op.rpc_method, Object.keys(rpcParams).length > 0 ? rpcParams : undefined, ctx?.agentId, ctx?.sessionKey);
782
+ return textResult(result.data);
783
+ }
784
+
723
785
  // Build body for non-GET requests
724
786
  let body: Record<string, unknown> | undefined;
725
787
  let rawBody: string | undefined;