@slock-ai/daemon 0.40.0 → 0.40.2

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.
@@ -2,12 +2,137 @@
2
2
  import {
3
3
  buildFetchDispatcher,
4
4
  logger
5
- } from "./chunk-E6OOH3IC.js";
5
+ } from "./chunk-JG7ONJZ6.js";
6
6
 
7
7
  // src/chat-bridge.ts
8
8
  import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
9
9
  import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
10
+ import { z as z2 } from "zod";
11
+
12
+ // src/deprecatedMcpShim.ts
10
13
  import { z } from "zod";
14
+ var DEPRECATED_MCP_SHIM_HEADER = "This MCP tool is deprecated. Use slock CLI instead.";
15
+ var sendMessageDeprecatedSchema = {
16
+ target: z.string().describe("Deprecated target argument."),
17
+ content: z.string().optional().describe("Deprecated content argument."),
18
+ attachment_ids: z.array(z.string()).optional().describe("Deprecated attachment ids.")
19
+ };
20
+ var checkMessagesDeprecatedSchema = {};
21
+ var readHistoryDeprecatedSchema = {
22
+ channel: z.string().describe("Deprecated channel argument."),
23
+ limit: z.number().optional().describe("Deprecated limit argument."),
24
+ around: z.union([z.string(), z.number()]).optional().describe("Deprecated around argument."),
25
+ before: z.number().optional().describe("Deprecated before argument."),
26
+ after: z.number().optional().describe("Deprecated after argument.")
27
+ };
28
+ var searchMessagesDeprecatedSchema = {
29
+ query: z.string().describe("Deprecated query argument."),
30
+ limit: z.number().optional().describe("Deprecated limit argument.")
31
+ };
32
+ var listTasksDeprecatedSchema = {
33
+ channel: z.string().describe("Deprecated channel argument."),
34
+ status: z.enum(["all", "todo", "in_progress", "in_review", "done"]).optional().describe("Deprecated status argument.")
35
+ };
36
+ var claimTasksDeprecatedSchema = {
37
+ channel: z.string().describe("Deprecated channel argument."),
38
+ task_numbers: z.array(z.number()).optional().describe("Deprecated task numbers."),
39
+ message_ids: z.array(z.string()).optional().describe("Deprecated message ids.")
40
+ };
41
+ var unclaimTaskDeprecatedSchema = {
42
+ channel: z.string().describe("Deprecated channel argument."),
43
+ task_number: z.number().describe("Deprecated task number.")
44
+ };
45
+ var updateTaskStatusDeprecatedSchema = {
46
+ channel: z.string().describe("Deprecated channel argument."),
47
+ task_number: z.number().describe("Deprecated task number."),
48
+ status: z.enum(["todo", "in_progress", "in_review", "done"]).describe("Deprecated status argument.")
49
+ };
50
+ var DEPRECATED_MCP_TOOL_DEFINITIONS = [
51
+ {
52
+ toolName: "send_message",
53
+ description: `${DEPRECATED_MCP_SHIM_HEADER}
54
+ Use \`slock message send\` and pass the message body on stdin.`,
55
+ schema: sendMessageDeprecatedSchema,
56
+ cliExamples: [
57
+ "slock message send --target '#channel-or-dm' <<'EOF2'",
58
+ "message body",
59
+ "EOF2"
60
+ ]
61
+ },
62
+ {
63
+ toolName: "check_messages",
64
+ description: `${DEPRECATED_MCP_SHIM_HEADER}
65
+ Use \`slock message check\`.`,
66
+ schema: checkMessagesDeprecatedSchema,
67
+ cliExamples: [
68
+ "slock message check"
69
+ ]
70
+ },
71
+ {
72
+ toolName: "read_history",
73
+ description: `${DEPRECATED_MCP_SHIM_HEADER}
74
+ Use \`slock message read --channel ...\`.`,
75
+ schema: readHistoryDeprecatedSchema,
76
+ cliExamples: [
77
+ "slock message read --channel '#channel'",
78
+ "slock message read --channel 'dm:@peer'",
79
+ "slock message read --channel '#channel:threadId'"
80
+ ]
81
+ },
82
+ {
83
+ toolName: "search_messages",
84
+ description: `${DEPRECATED_MCP_SHIM_HEADER}
85
+ Use \`slock message search --query ...\`.`,
86
+ schema: searchMessagesDeprecatedSchema,
87
+ cliExamples: [
88
+ "slock message search --query 'keyword'"
89
+ ]
90
+ },
91
+ {
92
+ toolName: "list_tasks",
93
+ description: `${DEPRECATED_MCP_SHIM_HEADER}
94
+ Use \`slock task list --channel ...\`.`,
95
+ schema: listTasksDeprecatedSchema,
96
+ cliExamples: [
97
+ "slock task list --channel '#channel'"
98
+ ]
99
+ },
100
+ {
101
+ toolName: "claim_tasks",
102
+ description: `${DEPRECATED_MCP_SHIM_HEADER}
103
+ Use \`slock task claim ...\`.`,
104
+ schema: claimTasksDeprecatedSchema,
105
+ cliExamples: [
106
+ "slock task claim --channel '#channel' --number 123",
107
+ "slock task claim --channel '#channel' --message-id <messageId>"
108
+ ]
109
+ },
110
+ {
111
+ toolName: "unclaim_task",
112
+ description: `${DEPRECATED_MCP_SHIM_HEADER}
113
+ Use \`slock task unclaim ...\`.`,
114
+ schema: unclaimTaskDeprecatedSchema,
115
+ cliExamples: [
116
+ "slock task unclaim --channel '#channel' --number 123"
117
+ ]
118
+ },
119
+ {
120
+ toolName: "update_task_status",
121
+ description: `${DEPRECATED_MCP_SHIM_HEADER}
122
+ Use \`slock task update ...\`.`,
123
+ schema: updateTaskStatusDeprecatedSchema,
124
+ cliExamples: [
125
+ "slock task update --channel '#channel' --number 123 --status in_review"
126
+ ]
127
+ }
128
+ ];
129
+ function buildDeprecatedMcpToolErrorText(tool) {
130
+ return [
131
+ DEPRECATED_MCP_SHIM_HEADER,
132
+ "",
133
+ ...tool.cliExamples
134
+ ].join("\n");
135
+ }
11
136
 
12
137
  // src/historyFormatting.ts
13
138
  function toLocalHistoryTime(iso) {
@@ -210,10 +335,14 @@ var args = process.argv.slice(2);
210
335
  var agentId = "";
211
336
  var serverUrl = "http://localhost:3001";
212
337
  var authToken = "";
338
+ var runtime = "unknown";
339
+ var deprecatedShimMode = false;
213
340
  for (let i = 0; i < args.length; i++) {
214
341
  if (args[i] === "--agent-id" && args[i + 1]) agentId = args[++i];
215
342
  if (args[i] === "--server-url" && args[i + 1]) serverUrl = args[++i];
216
343
  if (args[i] === "--auth-token" && args[i + 1]) authToken = args[++i];
344
+ if (args[i] === "--runtime" && args[i + 1]) runtime = args[++i];
345
+ if (args[i] === "--deprecated-shim") deprecatedShimMode = true;
217
346
  }
218
347
  if (!agentId) {
219
348
  console.error("Missing --agent-id");
@@ -371,904 +500,932 @@ var server = new McpServer({
371
500
  name: "chat",
372
501
  version: "1.0.0"
373
502
  });
374
- server.tool(
375
- "send_message",
376
- "Send a message to a channel, DM, or thread. Use the target value from received messages to reply. Format: '#channel' for channels, 'dm:@peer' for DMs, '#channel:shortid' for threads in channels, 'dm:@peer:shortid' for threads in DMs. To start a NEW DM, use 'dm:@person-name'.",
377
- {
378
- target: z.string().describe(
379
- "Where to send. Reuse the identifier from received messages. Format: '#channel' for channels, 'dm:@name' for DMs, '#channel:id' for channel threads, 'dm:@name:id' for DM threads. Examples: '#general', 'dm:@richard', '#general:abcd1234', 'dm:@richard:abcd1234'."
380
- ),
381
- content: z.string().describe("The message content"),
382
- attachment_ids: z.array(z.string()).optional().describe("Optional attachment IDs from upload_file to include with the message")
383
- },
384
- async ({ target, content, attachment_ids }) => {
385
- try {
386
- const { response: res, data } = await executeRetrySafeSendRequest(
387
- `${serverUrl}/internal/agent/${agentId}/send`,
388
- (idempotencyKey) => ({
389
- method: "POST",
390
- headers: commonHeaders,
391
- body: JSON.stringify({ target, content, attachmentIds: attachment_ids, idempotencyKey })
392
- }),
393
- {
394
- target,
395
- fetchImpl: bridgeFetch
396
- }
397
- );
398
- if (!res.ok) {
399
- return {
400
- content: [
401
- { type: "text", text: `Error: ${data.error}` }
402
- ]
403
- };
404
- }
405
- const shortId = data.messageId ? data.messageId.slice(0, 8) : null;
406
- const replyHint = shortId ? ` (to reply in this message's thread, use target "${target.includes(":") ? target : target + ":" + shortId}")` : "";
407
- let unreadSection = "";
408
- if (data.recentUnread && data.recentUnread.length > 0) {
409
- await acknowledgeReceivedMessages(data.recentUnread);
410
- const unreadToShow = rememberDeliveredMessages(data.recentUnread);
411
- if (unreadToShow.length > 0) {
412
- unreadSection = `
413
-
414
- --- New messages you may have missed ---
415
- ${formatMessages(unreadToShow)}`;
416
- }
417
- }
418
- return {
419
- content: [
420
- {
421
- type: "text",
422
- text: `Message sent to ${target}. Message ID: ${data.messageId}${replyHint}${unreadSection}`
423
- }
424
- ]
425
- };
426
- } catch (err) {
503
+ function logDeprecatedShimInvocation(tool) {
504
+ logger.warn(
505
+ `[ChatBridgeDeprecatedShim] tool=${tool.toolName} runtime=${runtime} agent_id=${agentId} outcome=deprecated`
506
+ );
507
+ }
508
+ function registerDeprecatedTool(tool) {
509
+ server.tool(
510
+ tool.toolName,
511
+ tool.description,
512
+ tool.schema,
513
+ async () => {
514
+ logDeprecatedShimInvocation(tool);
427
515
  return {
428
516
  isError: true,
429
- content: [{ type: "text", text: `Error: ${err.message}` }]
517
+ content: [{ type: "text", text: buildDeprecatedMcpToolErrorText(tool) }]
430
518
  };
431
519
  }
520
+ );
521
+ }
522
+ if (deprecatedShimMode) {
523
+ for (const tool of DEPRECATED_MCP_TOOL_DEFINITIONS) {
524
+ registerDeprecatedTool(tool);
432
525
  }
433
- );
434
- server.tool(
435
- "upload_file",
436
- "Upload a file to attach to a message. Returns an attachment ID that you can pass to send_message's attachment_ids parameter. Images keep preview behavior; other files are sent as downloadable attachments. Max size: 10MB.",
437
- {
438
- file_path: z.string().describe("Absolute path to the file on your local filesystem"),
439
- channel: z.string().describe("The channel target where this file will be used (e.g. '#general', 'dm:@richard')")
440
- },
441
- async ({ file_path, channel }) => {
442
- try {
443
- const fs = await import("fs");
444
- const path = await import("path");
445
- if (!fs.existsSync(file_path)) {
526
+ }
527
+ if (!deprecatedShimMode) {
528
+ let formatMessages = function(messages) {
529
+ return messages.map((m) => {
530
+ const target = formatTarget(m);
531
+ const msgId = m.message_id ? m.message_id.slice(0, 8) : "-";
532
+ const time = m.timestamp ? toLocalTime(m.timestamp) : "-";
533
+ const senderType = ` type=${m.sender_type}`;
534
+ const renderedContent = m.content;
535
+ const attachSuffix = formatAttachmentSuffix(m.attachments);
536
+ const taskSuffix = m.task_status ? ` [task #${m.task_number} status=${m.task_status}${m.task_assignee_id ? ` assignee=${m.task_assignee_type}:${m.task_assignee_id}` : ""}]` : "";
537
+ return `[target=${target} msg=${msgId} time=${time}${senderType}] ${formatSenderHandle(m)}: ${renderedContent}${attachSuffix}${taskSuffix}`;
538
+ }).join("\n");
539
+ }, formatReminder = function(r) {
540
+ const fireLocal = toLocalTime(r.fireAt);
541
+ const ref = r.msgRef ? ` ref=${r.msgRef}` : "";
542
+ const repeat = r.recurrence ? ` repeat=${r.recurrence.description}` : "";
543
+ return `#${r.reminderId.slice(0, 8)} [${r.status}] fires=${fireLocal} "${r.title}"${ref}${repeat}`;
544
+ };
545
+ formatMessages2 = formatMessages, formatReminder2 = formatReminder;
546
+ server.tool(
547
+ "send_message",
548
+ "Send a message to a channel, DM, or thread. Use the target value from received messages to reply. Format: '#channel' for channels, 'dm:@peer' for DMs, '#channel:shortid' for threads in channels, 'dm:@peer:shortid' for threads in DMs. To start a NEW DM, use 'dm:@person-name'.",
549
+ {
550
+ target: z2.string().describe(
551
+ "Where to send. Reuse the identifier from received messages. Format: '#channel' for channels, 'dm:@name' for DMs, '#channel:id' for channel threads, 'dm:@name:id' for DM threads. Examples: '#general', 'dm:@richard', '#general:abcd1234', 'dm:@richard:abcd1234'."
552
+ ),
553
+ content: z2.string().describe("The message content"),
554
+ attachment_ids: z2.array(z2.string()).optional().describe("Optional attachment IDs from upload_file to include with the message")
555
+ },
556
+ async ({ target, content, attachment_ids }) => {
557
+ try {
558
+ const { response: res, data } = await executeRetrySafeSendRequest(
559
+ `${serverUrl}/internal/agent/${agentId}/send`,
560
+ (idempotencyKey) => ({
561
+ method: "POST",
562
+ headers: commonHeaders,
563
+ body: JSON.stringify({ target, content, attachmentIds: attachment_ids, idempotencyKey })
564
+ }),
565
+ {
566
+ target,
567
+ fetchImpl: bridgeFetch
568
+ }
569
+ );
570
+ if (!res.ok) {
571
+ return {
572
+ content: [
573
+ { type: "text", text: `Error: ${data.error}` }
574
+ ]
575
+ };
576
+ }
577
+ const shortId = data.messageId ? data.messageId.slice(0, 8) : null;
578
+ const replyHint = shortId ? ` (to reply in this message's thread, use target "${target.includes(":") ? target : target + ":" + shortId}")` : "";
579
+ let unreadSection = "";
580
+ if (data.recentUnread && data.recentUnread.length > 0) {
581
+ await acknowledgeReceivedMessages(data.recentUnread);
582
+ const unreadToShow = rememberDeliveredMessages(data.recentUnread);
583
+ if (unreadToShow.length > 0) {
584
+ unreadSection = `
585
+
586
+ --- New messages you may have missed ---
587
+ ${formatMessages(unreadToShow)}`;
588
+ }
589
+ }
446
590
  return {
447
- isError: true,
448
- content: [{ type: "text", text: `Error: File not found: ${file_path}` }]
591
+ content: [
592
+ {
593
+ type: "text",
594
+ text: `Message sent to ${target}. Message ID: ${data.messageId}${replyHint}${unreadSection}`
595
+ }
596
+ ]
449
597
  };
450
- }
451
- const stat = fs.statSync(file_path);
452
- if (stat.size > 10 * 1024 * 1024) {
598
+ } catch (err) {
453
599
  return {
454
600
  isError: true,
455
- content: [{ type: "text", text: `Error: File too large (${(stat.size / 1024 / 1024).toFixed(1)}MB). Max 10MB per file.` }]
601
+ content: [{ type: "text", text: `Error: ${err.message}` }]
456
602
  };
457
603
  }
458
- const { response: listRes, data: listData } = await executeJsonRequest(
459
- `${serverUrl}/internal/agent/${agentId}/resolve-channel`,
460
- {
461
- method: "POST",
462
- headers: commonHeaders,
463
- body: JSON.stringify({ target: channel })
464
- },
465
- {
466
- toolName: "upload_file.resolve_channel",
467
- target: channel,
468
- fetchImpl: bridgeFetch
604
+ }
605
+ );
606
+ server.tool(
607
+ "upload_file",
608
+ "Upload a file to attach to a message. Returns an attachment ID that you can pass to send_message's attachment_ids parameter. Images keep preview behavior; other files are sent as downloadable attachments. Max size: 10MB.",
609
+ {
610
+ file_path: z2.string().describe("Absolute path to the file on your local filesystem"),
611
+ channel: z2.string().describe("The channel target where this file will be used (e.g. '#general', 'dm:@richard')")
612
+ },
613
+ async ({ file_path, channel }) => {
614
+ try {
615
+ const fs = await import("fs");
616
+ const path = await import("path");
617
+ if (!fs.existsSync(file_path)) {
618
+ return {
619
+ isError: true,
620
+ content: [{ type: "text", text: `Error: File not found: ${file_path}` }]
621
+ };
622
+ }
623
+ const stat = fs.statSync(file_path);
624
+ if (stat.size > 10 * 1024 * 1024) {
625
+ return {
626
+ isError: true,
627
+ content: [{ type: "text", text: `Error: File too large (${(stat.size / 1024 / 1024).toFixed(1)}MB). Max 10MB per file.` }]
628
+ };
629
+ }
630
+ const { response: listRes, data: listData } = await executeJsonRequest(
631
+ `${serverUrl}/internal/agent/${agentId}/resolve-channel`,
632
+ {
633
+ method: "POST",
634
+ headers: commonHeaders,
635
+ body: JSON.stringify({ target: channel })
636
+ },
637
+ {
638
+ toolName: "upload_file.resolve_channel",
639
+ target: channel,
640
+ fetchImpl: bridgeFetch
641
+ }
642
+ );
643
+ if (!listRes.ok || !listData.channelId) {
644
+ return {
645
+ isError: true,
646
+ content: [{ type: "text", text: `Error: ${listData.error || `Could not resolve channel: ${channel}`}` }]
647
+ };
648
+ }
649
+ const channelId = listData.channelId;
650
+ const fileBuffer = fs.readFileSync(file_path);
651
+ const filename = path.basename(file_path);
652
+ const mimeType = guessMimeTypeFromFilename(filename);
653
+ const blob = new Blob([fileBuffer], { type: mimeType });
654
+ const formData = new FormData();
655
+ formData.append("file", blob, filename);
656
+ formData.append("channelId", channelId);
657
+ const uploadHeaders = buildChatBridgeCommonHeaders(authToken, { includeContentType: false });
658
+ const { response: res, data } = await executeJsonRequest(
659
+ `${serverUrl}/internal/agent/${agentId}/upload`,
660
+ {
661
+ method: "POST",
662
+ headers: uploadHeaders,
663
+ body: formData
664
+ },
665
+ {
666
+ toolName: "upload_file",
667
+ target: channel,
668
+ fetchImpl: bridgeFetch
669
+ }
670
+ );
671
+ if (!res.ok) {
672
+ return {
673
+ isError: true,
674
+ content: [{ type: "text", text: `Error: ${data.error}` }]
675
+ };
469
676
  }
470
- );
471
- if (!listRes.ok || !listData.channelId) {
472
677
  return {
473
- isError: true,
474
- content: [{ type: "text", text: `Error: ${listData.error || `Could not resolve channel: ${channel}`}` }]
678
+ content: [
679
+ {
680
+ type: "text",
681
+ text: `File uploaded: ${data.filename} (${(data.sizeBytes / 1024).toFixed(1)}KB)
682
+ Attachment ID: ${data.id}
683
+
684
+ Use this ID in send_message's attachment_ids parameter to include it in a message.`
685
+ }
686
+ ]
475
687
  };
476
- }
477
- const channelId = listData.channelId;
478
- const fileBuffer = fs.readFileSync(file_path);
479
- const filename = path.basename(file_path);
480
- const mimeType = guessMimeTypeFromFilename(filename);
481
- const blob = new Blob([fileBuffer], { type: mimeType });
482
- const formData = new FormData();
483
- formData.append("file", blob, filename);
484
- formData.append("channelId", channelId);
485
- const uploadHeaders = buildChatBridgeCommonHeaders(authToken, { includeContentType: false });
486
- const { response: res, data } = await executeJsonRequest(
487
- `${serverUrl}/internal/agent/${agentId}/upload`,
488
- {
489
- method: "POST",
490
- headers: uploadHeaders,
491
- body: formData
492
- },
493
- {
494
- toolName: "upload_file",
495
- target: channel,
496
- fetchImpl: bridgeFetch
497
- }
498
- );
499
- if (!res.ok) {
688
+ } catch (err) {
500
689
  return {
501
690
  isError: true,
502
- content: [{ type: "text", text: `Error: ${data.error}` }]
691
+ content: [{ type: "text", text: `Error: ${err.message}` }]
503
692
  };
504
693
  }
505
- return {
506
- content: [
694
+ }
695
+ );
696
+ server.tool(
697
+ "view_file",
698
+ "Download an attached file by its attachment ID and save it locally so you can inspect it. Returns the local file path.",
699
+ {
700
+ attachment_id: z2.string().describe("The attachment UUID (from the 'id:...' shown in the message)")
701
+ },
702
+ async ({ attachment_id }) => {
703
+ try {
704
+ const fs = await import("fs");
705
+ const path = await import("path");
706
+ const os = await import("os");
707
+ const cacheDir = path.join(os.homedir(), ".slock", "attachments");
708
+ fs.mkdirSync(cacheDir, { recursive: true });
709
+ const existing = fs.readdirSync(cacheDir).find((f) => f.startsWith(attachment_id));
710
+ if (existing) {
711
+ const cachedPath = path.join(cacheDir, existing);
712
+ return {
713
+ content: [{ type: "text", text: `File already cached at: ${cachedPath}` }]
714
+ };
715
+ }
716
+ const downloadHeaders = buildChatBridgeCommonHeaders(authToken, { includeContentType: false });
717
+ const { response: res } = await executeResponseRequest(
718
+ `${serverUrl}/api/attachments/${attachment_id}`,
507
719
  {
508
- type: "text",
509
- text: `File uploaded: ${data.filename} (${(data.sizeBytes / 1024).toFixed(1)}KB)
510
- Attachment ID: ${data.id}
511
-
512
- Use this ID in send_message's attachment_ids parameter to include it in a message.`
720
+ headers: downloadHeaders,
721
+ redirect: "follow"
722
+ },
723
+ {
724
+ toolName: "view_file",
725
+ target: attachment_id,
726
+ fetchImpl: bridgeFetch
513
727
  }
514
- ]
515
- };
516
- } catch (err) {
517
- return {
518
- isError: true,
519
- content: [{ type: "text", text: `Error: ${err.message}` }]
520
- };
521
- }
522
- }
523
- );
524
- server.tool(
525
- "view_file",
526
- "Download an attached file by its attachment ID and save it locally so you can inspect it. Returns the local file path.",
527
- {
528
- attachment_id: z.string().describe("The attachment UUID (from the 'id:...' shown in the message)")
529
- },
530
- async ({ attachment_id }) => {
531
- try {
532
- const fs = await import("fs");
533
- const path = await import("path");
534
- const os = await import("os");
535
- const cacheDir = path.join(os.homedir(), ".slock", "attachments");
536
- fs.mkdirSync(cacheDir, { recursive: true });
537
- const existing = fs.readdirSync(cacheDir).find((f) => f.startsWith(attachment_id));
538
- if (existing) {
539
- const cachedPath = path.join(cacheDir, existing);
728
+ );
729
+ if (!res.ok) {
730
+ return {
731
+ isError: true,
732
+ content: [{ type: "text", text: `Error: Failed to download attachment (${res.status})` }]
733
+ };
734
+ }
735
+ const contentType = res.headers.get("content-type") || "application/octet-stream";
736
+ const filename = parseFilenameFromContentDisposition(res.headers.get("content-disposition"));
737
+ const ext = filename ? path.extname(filename) || extensionForContentType(contentType) : extensionForContentType(contentType);
738
+ const filePath = path.join(cacheDir, `${attachment_id}${ext}`);
739
+ const buffer = Buffer.from(await res.arrayBuffer());
740
+ fs.writeFileSync(filePath, buffer);
540
741
  return {
541
- content: [{ type: "text", text: `File already cached at: ${cachedPath}` }]
742
+ content: [{ type: "text", text: `Downloaded to: ${filePath}` }]
542
743
  };
543
- }
544
- const downloadHeaders = buildChatBridgeCommonHeaders(authToken, { includeContentType: false });
545
- const { response: res } = await executeResponseRequest(
546
- `${serverUrl}/api/attachments/${attachment_id}`,
547
- {
548
- headers: downloadHeaders,
549
- redirect: "follow"
550
- },
551
- {
552
- toolName: "view_file",
553
- target: attachment_id,
554
- fetchImpl: bridgeFetch
555
- }
556
- );
557
- if (!res.ok) {
744
+ } catch (err) {
558
745
  return {
559
746
  isError: true,
560
- content: [{ type: "text", text: `Error: Failed to download attachment (${res.status})` }]
747
+ content: [{ type: "text", text: `Error: ${err.message}` }]
561
748
  };
562
749
  }
563
- const contentType = res.headers.get("content-type") || "application/octet-stream";
564
- const filename = parseFilenameFromContentDisposition(res.headers.get("content-disposition"));
565
- const ext = filename ? path.extname(filename) || extensionForContentType(contentType) : extensionForContentType(contentType);
566
- const filePath = path.join(cacheDir, `${attachment_id}${ext}`);
567
- const buffer = Buffer.from(await res.arrayBuffer());
568
- fs.writeFileSync(filePath, buffer);
569
- return {
570
- content: [{ type: "text", text: `Downloaded to: ${filePath}` }]
571
- };
572
- } catch (err) {
573
- return {
574
- isError: true,
575
- content: [{ type: "text", text: `Error: ${err.message}` }]
576
- };
577
750
  }
578
- }
579
- );
580
- server.tool(
581
- "check_messages",
582
- "Check for new messages without waiting. Returns immediately with any pending messages, or 'No new messages' if none. Use this freely during work \u2014 at natural breakpoints, after notifications, or whenever you want to see if anything new came in.",
583
- {},
584
- async () => {
585
- try {
586
- const { response: res, data } = await executeJsonRequest(
587
- `${serverUrl}/internal/agent/${agentId}/receive`,
588
- { method: "GET", headers: commonHeaders },
589
- {
590
- toolName: "check_messages",
591
- timeoutMs: 1e4,
592
- fetchImpl: bridgeFetch
751
+ );
752
+ server.tool(
753
+ "check_messages",
754
+ "Check for new messages without waiting. Returns immediately with any pending messages, or 'No new messages' if none. Use this freely during work \u2014 at natural breakpoints, after notifications, or whenever you want to see if anything new came in.",
755
+ {},
756
+ async () => {
757
+ try {
758
+ const { response: res, data } = await executeJsonRequest(
759
+ `${serverUrl}/internal/agent/${agentId}/receive`,
760
+ { method: "GET", headers: commonHeaders },
761
+ {
762
+ toolName: "check_messages",
763
+ timeoutMs: 1e4,
764
+ fetchImpl: bridgeFetch
765
+ }
766
+ );
767
+ if (!res.ok) {
768
+ return { isError: true, content: [{ type: "text", text: `Error: ${data.error || res.statusText}` }] };
593
769
  }
594
- );
595
- if (!res.ok) {
596
- return { isError: true, content: [{ type: "text", text: `Error: ${data.error || res.statusText}` }] };
597
- }
598
- const messages = data.messages ?? [];
599
- if (messages.length > 0) {
600
- await acknowledgeReceivedMessages(messages);
601
- const messagesToShow = rememberDeliveredMessages(messages);
602
- if (messagesToShow.length > 0) {
603
- return { content: [{ type: "text", text: formatMessages(messagesToShow) }] };
770
+ const messages = data.messages ?? [];
771
+ if (messages.length > 0) {
772
+ await acknowledgeReceivedMessages(messages);
773
+ const messagesToShow = rememberDeliveredMessages(messages);
774
+ if (messagesToShow.length > 0) {
775
+ return { content: [{ type: "text", text: formatMessages(messagesToShow) }] };
776
+ }
604
777
  }
778
+ return {
779
+ content: [{ type: "text", text: "No new messages." }]
780
+ };
781
+ } catch (err) {
782
+ return {
783
+ isError: true,
784
+ content: [{ type: "text", text: `Error: ${err.message}` }]
785
+ };
605
786
  }
606
- return {
607
- content: [{ type: "text", text: "No new messages." }]
608
- };
609
- } catch (err) {
610
- return {
611
- isError: true,
612
- content: [{ type: "text", text: `Error: ${err.message}` }]
613
- };
614
787
  }
615
- }
616
- );
617
- function formatMessages(messages) {
618
- return messages.map((m) => {
619
- const target = formatTarget(m);
620
- const msgId = m.message_id ? m.message_id.slice(0, 8) : "-";
621
- const time = m.timestamp ? toLocalTime(m.timestamp) : "-";
622
- const senderType = ` type=${m.sender_type}`;
623
- const renderedContent = m.content;
624
- const attachSuffix = formatAttachmentSuffix(m.attachments);
625
- const taskSuffix = m.task_status ? ` [task #${m.task_number} status=${m.task_status}${m.task_assignee_id ? ` assignee=${m.task_assignee_type}:${m.task_assignee_id}` : ""}]` : "";
626
- return `[target=${target} msg=${msgId} time=${time}${senderType}] ${formatSenderHandle(m)}: ${renderedContent}${attachSuffix}${taskSuffix}`;
627
- }).join("\n");
628
- }
629
- server.tool(
630
- "list_server",
631
- "List all channels in this server, including which ones you have joined, plus all agents and humans. Use this to discover who and where you can message.",
632
- {},
633
- async () => {
634
- try {
635
- const { response: res, data } = await executeJsonRequest(
636
- `${serverUrl}/internal/agent/${agentId}/server`,
637
- { method: "GET", headers: commonHeaders },
638
- {
639
- toolName: "list_server",
640
- fetchImpl: bridgeFetch
641
- }
642
- );
643
- let text = "## Server\n\n";
644
- const channels = data.channels ?? [];
645
- const agents = data.agents ?? [];
646
- const humans = data.humans ?? [];
647
- text += "### Channels\n";
648
- text += 'Visible public channels may appear even when `joined=false`. Use `read_history(channel="#name")` to inspect them. When a channel is not joined, you cannot send messages there or receive ordinary channel delivery until a human adds you to the channel.\n';
649
- if (channels.length > 0) {
650
- for (const t of channels) {
651
- const status = t.joined ? "joined" : "not joined";
652
- text += t.description ? ` - #${t.name} [${status}] \u2014 ${t.description}
788
+ );
789
+ server.tool(
790
+ "list_server",
791
+ "List all channels in this server, including which ones you have joined, plus all agents and humans. Use this to discover who and where you can message.",
792
+ {},
793
+ async () => {
794
+ try {
795
+ const { response: res, data } = await executeJsonRequest(
796
+ `${serverUrl}/internal/agent/${agentId}/server`,
797
+ { method: "GET", headers: commonHeaders },
798
+ {
799
+ toolName: "list_server",
800
+ fetchImpl: bridgeFetch
801
+ }
802
+ );
803
+ let text = "## Server\n\n";
804
+ const channels = data.channels ?? [];
805
+ const agents = data.agents ?? [];
806
+ const humans = data.humans ?? [];
807
+ text += "### Channels\n";
808
+ text += 'Visible public channels may appear even when `joined=false`. Use `read_history(channel="#name")` to inspect them. When a channel is not joined, you cannot send messages there or receive ordinary channel delivery until a human adds you to the channel.\n';
809
+ if (channels.length > 0) {
810
+ for (const t of channels) {
811
+ const status = t.joined ? "joined" : "not joined";
812
+ text += t.description ? ` - #${t.name} [${status}] \u2014 ${t.description}
653
813
  ` : ` - #${t.name} [${status}]
654
814
  `;
815
+ }
816
+ } else {
817
+ text += " (none)\n";
655
818
  }
656
- } else {
657
- text += " (none)\n";
658
- }
659
- text += "\n### Agents\n";
660
- text += "Other AI agents in this server.\n";
661
- if (agents.length > 0) {
662
- for (const a of agents) {
663
- text += a.description ? ` - @${a.name} (${a.status}) \u2014 ${a.description}
819
+ text += "\n### Agents\n";
820
+ text += "Other AI agents in this server.\n";
821
+ if (agents.length > 0) {
822
+ for (const a of agents) {
823
+ text += a.description ? ` - @${a.name} (${a.status}) \u2014 ${a.description}
664
824
  ` : ` - @${a.name} (${a.status})
665
825
  `;
826
+ }
827
+ } else {
828
+ text += " (none)\n";
666
829
  }
667
- } else {
668
- text += " (none)\n";
669
- }
670
- text += "\n### Humans\n";
671
- text += 'To start a new DM: send_message(target="dm:@name"). To reply in an existing DM: reuse the target from received messages.\n';
672
- if (humans.length > 0) {
673
- for (const u of humans) {
674
- text += u.description ? ` - @${u.name} \u2014 ${u.description}
830
+ text += "\n### Humans\n";
831
+ text += 'To start a new DM: send_message(target="dm:@name"). To reply in an existing DM: reuse the target from received messages.\n';
832
+ if (humans.length > 0) {
833
+ for (const u of humans) {
834
+ text += u.description ? ` - @${u.name} \u2014 ${u.description}
675
835
  ` : ` - @${u.name}
676
836
  `;
837
+ }
838
+ } else {
839
+ text += " (none)\n";
677
840
  }
678
- } else {
679
- text += " (none)\n";
680
- }
681
- return {
682
- content: [{ type: "text", text }]
683
- };
684
- } catch (err) {
685
- return {
686
- isError: true,
687
- content: [{ type: "text", text: `Error: ${err.message}` }]
688
- };
689
- }
690
- }
691
- );
692
- server.tool(
693
- "search_messages",
694
- "Search messages visible to the agent. Use this to find relevant conversations, then inspect a hit with read_history(channel=..., around=messageId).",
695
- {
696
- query: z.string().describe("Search query"),
697
- channel: z.string().optional().describe("Optional target to scope the search, e.g. '#general', 'dm:@richard', '#general:abcd1234'"),
698
- sender_id: z.string().optional().describe("Optional exact sender id filter."),
699
- after: z.string().optional().describe("Optional inclusive ISO datetime lower bound for message created_at."),
700
- before: z.string().optional().describe("Optional inclusive ISO datetime upper bound for message created_at."),
701
- limit: z.number().default(10).describe("Max number of search results to return (default 10, max 20)")
702
- },
703
- async ({ query, channel, sender_id, after, before, limit }) => {
704
- try {
705
- const trimmed = query.trim();
706
- if (!trimmed) {
707
- return {
708
- content: [{ type: "text", text: "Search query cannot be empty." }]
709
- };
710
- }
711
- const params = new URLSearchParams();
712
- params.set("q", trimmed);
713
- params.set("limit", String(Math.min(limit, 20)));
714
- if (channel) params.set("channel", channel);
715
- if (sender_id) params.set("senderId", sender_id);
716
- if (after) params.set("after", after);
717
- if (before) params.set("before", before);
718
- const { response: res, data } = await executeJsonRequest(
719
- `${serverUrl}/internal/agent/${agentId}/search?${params}`,
720
- { method: "GET", headers: commonHeaders },
721
- {
722
- toolName: "search_messages",
723
- target: channel ?? null,
724
- fetchImpl: bridgeFetch
725
- }
726
- );
727
- if (!res.ok) {
728
841
  return {
729
- content: [{ type: "text", text: `Error: ${data.error}` }]
842
+ content: [{ type: "text", text }]
730
843
  };
731
- }
732
- if (!data.results || data.results.length === 0) {
844
+ } catch (err) {
733
845
  return {
734
- content: [{ type: "text", text: "No search results." }]
846
+ isError: true,
847
+ content: [{ type: "text", text: `Error: ${err.message}` }]
735
848
  };
736
849
  }
737
- const formatted = data.results.map((result, index) => {
738
- const target = formatSearchTarget(result);
739
- const threadInfo = result.channelType === "thread" ? `
740
- thread: ${result.parentChannelName} -> ${target}` : "";
741
- return [
742
- `[${index + 1}] msg=${result.id} seq=${result.seq} time=${toLocalTime(result.createdAt)}`,
743
- `target: ${target}${threadInfo}`,
744
- `sender: @${result.senderName} (${result.senderType})`,
745
- `content: ${result.content}`,
746
- `match: ${result.snippet}`,
747
- `next: read_history(channel="${target}", around="${result.id}", limit=20)`
748
- ].join("\n");
749
- }).join("\n\n");
750
- return {
751
- content: [{
752
- type: "text",
753
- text: `## Search Results for "${trimmed}" (${data.results.length} results)
754
-
755
- ${formatted}`
756
- }]
757
- };
758
- } catch (err) {
759
- return {
760
- isError: true,
761
- content: [{ type: "text", text: `Error: ${err.message}` }]
762
- };
763
850
  }
764
- }
765
- );
766
- server.tool(
767
- "read_history",
768
- "Read message history for a channel, DM, or thread. Use the same target format: '#channel', 'dm:@name', '#channel:id' for threads, 'dm:@name:id' for DM threads. Supports pagination via 'before' / 'after', and context jumps via 'around' (messageId or seq).",
769
- {
770
- channel: z.string().describe("The target to read history from \u2014 e.g. '#general', 'dm:@richard', '#general:abcd1234', 'dm:@richard:abcd1234'"),
771
- limit: z.number().default(50).describe("Max number of messages to return (default 50, max 100)"),
772
- around: z.union([z.string(), z.number()]).optional().describe("Center the result window around a messageId or seq in this channel/thread."),
773
- before: z.number().optional().describe("Return messages before this seq number (for backward pagination). Omit for latest messages."),
774
- after: z.number().optional().describe("Return messages after this seq number (for catching up on unread). Returns oldest-first.")
775
- },
776
- async ({ channel, limit, around, before, after }) => {
777
- try {
778
- const params = new URLSearchParams();
779
- params.set("channel", channel);
780
- params.set("limit", String(Math.min(limit, 100)));
781
- if (around !== void 0) params.set("around", String(around));
782
- if (before) params.set("before", String(before));
783
- if (after) params.set("after", String(after));
784
- const { response: res, data } = await executeJsonRequest(
785
- `${serverUrl}/internal/agent/${agentId}/history?${params}`,
786
- { method: "GET", headers: commonHeaders },
787
- {
788
- toolName: "read_history",
789
- target: channel,
790
- fetchImpl: bridgeFetch
851
+ );
852
+ server.tool(
853
+ "search_messages",
854
+ "Search messages visible to the agent. Use this to find relevant conversations, then inspect a hit with read_history(channel=..., around=messageId).",
855
+ {
856
+ query: z2.string().describe("Search query"),
857
+ channel: z2.string().optional().describe("Optional target to scope the search, e.g. '#general', 'dm:@richard', '#general:abcd1234'"),
858
+ sender_id: z2.string().optional().describe("Optional exact sender id filter."),
859
+ after: z2.string().optional().describe("Optional inclusive ISO datetime lower bound for message created_at."),
860
+ before: z2.string().optional().describe("Optional inclusive ISO datetime upper bound for message created_at."),
861
+ limit: z2.number().default(10).describe("Max number of search results to return (default 10, max 20)")
862
+ },
863
+ async ({ query, channel, sender_id, after, before, limit }) => {
864
+ try {
865
+ const trimmed = query.trim();
866
+ if (!trimmed) {
867
+ return {
868
+ content: [{ type: "text", text: "Search query cannot be empty." }]
869
+ };
791
870
  }
792
- );
793
- if (!res.ok) {
871
+ const params = new URLSearchParams();
872
+ params.set("q", trimmed);
873
+ params.set("limit", String(Math.min(limit, 20)));
874
+ if (channel) params.set("channel", channel);
875
+ if (sender_id) params.set("senderId", sender_id);
876
+ if (after) params.set("after", after);
877
+ if (before) params.set("before", before);
878
+ const { response: res, data } = await executeJsonRequest(
879
+ `${serverUrl}/internal/agent/${agentId}/search?${params}`,
880
+ { method: "GET", headers: commonHeaders },
881
+ {
882
+ toolName: "search_messages",
883
+ target: channel ?? null,
884
+ fetchImpl: bridgeFetch
885
+ }
886
+ );
887
+ if (!res.ok) {
888
+ return {
889
+ content: [{ type: "text", text: `Error: ${data.error}` }]
890
+ };
891
+ }
892
+ if (!data.results || data.results.length === 0) {
893
+ return {
894
+ content: [{ type: "text", text: "No search results." }]
895
+ };
896
+ }
897
+ const formatted = data.results.map((result, index) => {
898
+ const target = formatSearchTarget(result);
899
+ const threadInfo = result.channelType === "thread" ? `
900
+ thread: ${result.parentChannelName} -> ${target}` : "";
901
+ return [
902
+ `[${index + 1}] msg=${result.id} seq=${result.seq} time=${toLocalTime(result.createdAt)}`,
903
+ `target: ${target}${threadInfo}`,
904
+ `sender: @${result.senderName} (${result.senderType})`,
905
+ `content: ${result.content}`,
906
+ `match: ${result.snippet}`,
907
+ `next: read_history(channel="${target}", around="${result.id}", limit=20)`
908
+ ].join("\n");
909
+ }).join("\n\n");
794
910
  return {
795
- content: [
796
- { type: "text", text: `Error: ${data.error}` }
797
- ]
911
+ content: [{
912
+ type: "text",
913
+ text: `## Search Results for "${trimmed}" (${data.results.length} results)
914
+
915
+ ${formatted}`
916
+ }]
798
917
  };
799
- }
800
- if (!data.messages || data.messages.length === 0) {
918
+ } catch (err) {
801
919
  return {
802
- content: [
803
- { type: "text", text: "No messages in this channel." }
804
- ]
920
+ isError: true,
921
+ content: [{ type: "text", text: `Error: ${err.message}` }]
805
922
  };
806
923
  }
807
- const formatted = data.messages.map((m) => formatHistoryMessageLine({
808
- ...m,
809
- senderName: m.senderName ?? m.sender_name ?? "unknown",
810
- senderDescription: m.senderDescription ?? m.sender_description ?? null
811
- })).join("\n");
812
- let footer = "";
813
- if (data.historyLimited) {
814
- footer = `
924
+ }
925
+ );
926
+ server.tool(
927
+ "read_history",
928
+ "Read message history for a channel, DM, or thread. Use the same target format: '#channel', 'dm:@name', '#channel:id' for threads, 'dm:@name:id' for DM threads. Supports pagination via 'before' / 'after', and context jumps via 'around' (messageId or seq).",
929
+ {
930
+ channel: z2.string().describe("The target to read history from \u2014 e.g. '#general', 'dm:@richard', '#general:abcd1234', 'dm:@richard:abcd1234'"),
931
+ limit: z2.number().default(50).describe("Max number of messages to return (default 50, max 100)"),
932
+ around: z2.union([z2.string(), z2.number()]).optional().describe("Center the result window around a messageId or seq in this channel/thread."),
933
+ before: z2.number().optional().describe("Return messages before this seq number (for backward pagination). Omit for latest messages."),
934
+ after: z2.number().optional().describe("Return messages after this seq number (for catching up on unread). Returns oldest-first.")
935
+ },
936
+ async ({ channel, limit, around, before, after }) => {
937
+ try {
938
+ const params = new URLSearchParams();
939
+ params.set("channel", channel);
940
+ params.set("limit", String(Math.min(limit, 100)));
941
+ if (around !== void 0) params.set("around", String(around));
942
+ if (before) params.set("before", String(before));
943
+ if (after) params.set("after", String(after));
944
+ const { response: res, data } = await executeJsonRequest(
945
+ `${serverUrl}/internal/agent/${agentId}/history?${params}`,
946
+ { method: "GET", headers: commonHeaders },
947
+ {
948
+ toolName: "read_history",
949
+ target: channel,
950
+ fetchImpl: bridgeFetch
951
+ }
952
+ );
953
+ if (!res.ok) {
954
+ return {
955
+ content: [
956
+ { type: "text", text: `Error: ${data.error}` }
957
+ ]
958
+ };
959
+ }
960
+ if (!data.messages || data.messages.length === 0) {
961
+ return {
962
+ content: [
963
+ { type: "text", text: "No messages in this channel." }
964
+ ]
965
+ };
966
+ }
967
+ const formatted = data.messages.map((m) => formatHistoryMessageLine({
968
+ ...m,
969
+ senderName: m.senderName ?? m.sender_name ?? "unknown",
970
+ senderDescription: m.senderDescription ?? m.sender_description ?? null
971
+ })).join("\n");
972
+ let footer = "";
973
+ if (data.historyLimited) {
974
+ footer = `
815
975
 
816
976
  --- ${data.historyLimitMessage || "Message history is limited on this plan."} ---`;
817
- } else if (around && data.messages.length > 0 && (data.has_older || data.has_newer)) {
818
- const minSeq = data.messages[0].seq;
819
- const maxSeq = data.messages[data.messages.length - 1].seq;
820
- footer = `
821
-
822
- --- Context window shown. Use before=${minSeq} to load older messages or after=${maxSeq} to load newer messages. ---`;
823
- } else if (data.has_more && data.messages.length > 0) {
824
- if (after) {
977
+ } else if (around && data.messages.length > 0 && (data.has_older || data.has_newer)) {
978
+ const minSeq = data.messages[0].seq;
825
979
  const maxSeq = data.messages[data.messages.length - 1].seq;
826
980
  footer = `
827
981
 
982
+ --- Context window shown. Use before=${minSeq} to load older messages or after=${maxSeq} to load newer messages. ---`;
983
+ } else if (data.has_more && data.messages.length > 0) {
984
+ if (after) {
985
+ const maxSeq = data.messages[data.messages.length - 1].seq;
986
+ footer = `
987
+
828
988
  --- ${data.messages.length} messages shown. Use after=${maxSeq} to load more recent messages. ---`;
829
- } else {
830
- const minSeq = data.messages[0].seq;
831
- footer = `
989
+ } else {
990
+ const minSeq = data.messages[0].seq;
991
+ footer = `
832
992
 
833
993
  --- ${data.messages.length} messages shown. Use before=${minSeq} to load older messages. ---`;
994
+ }
834
995
  }
835
- }
836
- let header = `## Message History for ${channel}${around ? ` around ${around}` : ""} (${data.messages.length} messages)`;
837
- if ((data.last_read_seq ?? 0) > 0 && !after && !before && !around) {
838
- header += `
996
+ let header = `## Message History for ${channel}${around ? ` around ${around}` : ""} (${data.messages.length} messages)`;
997
+ if ((data.last_read_seq ?? 0) > 0 && !after && !before && !around) {
998
+ header += `
839
999
  Your last read position: seq ${data.last_read_seq}. Use read_history(channel="${channel}", after=${data.last_read_seq}) to see only unread messages.`;
840
- }
841
- return {
842
- content: [
843
- {
844
- type: "text",
845
- text: `${header}
846
-
847
- ${formatted}${footer}`
848
- }
849
- ]
850
- };
851
- } catch (err) {
852
- return {
853
- isError: true,
854
- content: [{ type: "text", text: `Error: ${err.message}` }]
855
- };
856
- }
857
- }
858
- );
859
- server.tool(
860
- "list_tasks",
861
- "List all tasks in a channel. Returns each task's number, title, status, assignee, and message ID. Use this to see what work exists before claiming. Tasks marked as legacy are from an older system and cannot be claimed or modified.",
862
- {
863
- channel: z.string().describe("The channel whose task board to view \u2014 e.g. '#engineering', '#proj-slock'"),
864
- status: z.enum(["all", "todo", "in_progress", "in_review", "done"]).default("all").describe("Filter by status (default: all)")
865
- },
866
- async ({ channel, status }) => {
867
- try {
868
- const params = new URLSearchParams();
869
- params.set("channel", channel);
870
- if (status !== "all") params.set("status", status);
871
- const { response: res, data } = await executeJsonRequest(
872
- `${serverUrl}/internal/agent/${agentId}/tasks?${params}`,
873
- { method: "GET", headers: commonHeaders },
874
- {
875
- toolName: "list_tasks",
876
- target: channel,
877
- fetchImpl: bridgeFetch
878
1000
  }
879
- );
880
- if (!res.ok) {
881
1001
  return {
882
- isError: true,
883
- content: [{ type: "text", text: `Error: ${data.error}` }]
1002
+ content: [
1003
+ {
1004
+ type: "text",
1005
+ text: `${header}
1006
+
1007
+ ${formatted}${footer}`
1008
+ }
1009
+ ]
884
1010
  };
885
- }
886
- if (!data.tasks || data.tasks.length === 0) {
1011
+ } catch (err) {
887
1012
  return {
888
- content: [{ type: "text", text: `No${status !== "all" ? ` ${status}` : ""} tasks in ${channel}.` }]
1013
+ isError: true,
1014
+ content: [{ type: "text", text: `Error: ${err.message}` }]
889
1015
  };
890
1016
  }
891
- const formatted = data.tasks.map((t) => {
892
- const assignee = t.claimedByName ? ` \u2192 @${t.claimedByName}` : "";
893
- const creator = t.createdByName ? ` (by @${t.createdByName})` : "";
894
- const msgId = t.messageId ? ` msg=${t.messageId.slice(0, 8)}` : "";
895
- const legacy = t.isLegacy ? " [LEGACY \u2014 read-only]" : "";
896
- return `#${t.taskNumber} [${t.status}] ${t.title}${assignee}${creator}${msgId}${legacy}`;
897
- }).join("\n");
898
- return {
899
- content: [
1017
+ }
1018
+ );
1019
+ server.tool(
1020
+ "list_tasks",
1021
+ "List all tasks in a channel. Returns each task's number, title, status, assignee, and message ID. Use this to see what work exists before claiming. Tasks marked as legacy are from an older system and cannot be claimed or modified.",
1022
+ {
1023
+ channel: z2.string().describe("The channel whose task board to view \u2014 e.g. '#engineering', '#proj-slock'"),
1024
+ status: z2.enum(["all", "todo", "in_progress", "in_review", "done"]).default("all").describe("Filter by status (default: all)")
1025
+ },
1026
+ async ({ channel, status }) => {
1027
+ try {
1028
+ const params = new URLSearchParams();
1029
+ params.set("channel", channel);
1030
+ if (status !== "all") params.set("status", status);
1031
+ const { response: res, data } = await executeJsonRequest(
1032
+ `${serverUrl}/internal/agent/${agentId}/tasks?${params}`,
1033
+ { method: "GET", headers: commonHeaders },
900
1034
  {
901
- type: "text",
902
- text: `## Task Board for ${channel} (${data.tasks.length} tasks)
903
-
904
- ${formatted}`
1035
+ toolName: "list_tasks",
1036
+ target: channel,
1037
+ fetchImpl: bridgeFetch
905
1038
  }
906
- ]
907
- };
908
- } catch (err) {
909
- return {
910
- isError: true,
911
- content: [{ type: "text", text: `Error: ${err.message}` }]
912
- };
913
- }
914
- }
915
- );
916
- server.tool(
917
- "create_tasks",
918
- "Create one or more new task-messages in a top-level channel or DM. This is a convenience helper for creating a brand-new message and publishing it as a task-message in the chat flow. Thread messages cannot become tasks. It does not claim the task for you; if you want to own it, still call claim_tasks afterward. It is not a separate task board outside the chat flow. Typical uses are breaking down a larger task into parallel subtasks or batch-creating new work for others to claim. Do not use this to convert an existing message \u2014 use claim_tasks with message_ids instead. If the work already exists as a task, either claim that task or leave it alone; do not create a second task/message for the same work.",
919
- {
920
- channel: z.string().describe("The channel to create tasks in \u2014 e.g. '#engineering'"),
921
- tasks: z.array(
922
- z.object({
923
- title: z.string().describe("Task title")
924
- })
925
- ).describe("Array of tasks to create")
926
- },
927
- async ({ channel, tasks }) => {
928
- try {
929
- const { response: res, data } = await executeJsonRequest(
930
- `${serverUrl}/internal/agent/${agentId}/tasks`,
931
- {
932
- method: "POST",
933
- headers: commonHeaders,
934
- body: JSON.stringify({ channel, tasks })
935
- },
936
- {
937
- toolName: "create_tasks",
938
- target: channel,
939
- fetchImpl: bridgeFetch
1039
+ );
1040
+ if (!res.ok) {
1041
+ return {
1042
+ isError: true,
1043
+ content: [{ type: "text", text: `Error: ${data.error}` }]
1044
+ };
940
1045
  }
941
- );
942
- if (!res.ok) {
1046
+ if (!data.tasks || data.tasks.length === 0) {
1047
+ return {
1048
+ content: [{ type: "text", text: `No${status !== "all" ? ` ${status}` : ""} tasks in ${channel}.` }]
1049
+ };
1050
+ }
1051
+ const formatted = data.tasks.map((t) => {
1052
+ const assignee = t.claimedByName ? ` \u2192 @${t.claimedByName}` : "";
1053
+ const creator = t.createdByName ? ` (by @${t.createdByName})` : "";
1054
+ const msgId = t.messageId ? ` msg=${t.messageId.slice(0, 8)}` : "";
1055
+ const legacy = t.isLegacy ? " [LEGACY \u2014 read-only]" : "";
1056
+ return `#${t.taskNumber} [${t.status}] ${t.title}${assignee}${creator}${msgId}${legacy}`;
1057
+ }).join("\n");
1058
+ return {
1059
+ content: [
1060
+ {
1061
+ type: "text",
1062
+ text: `## Task Board for ${channel} (${data.tasks.length} tasks)
1063
+
1064
+ ${formatted}`
1065
+ }
1066
+ ]
1067
+ };
1068
+ } catch (err) {
943
1069
  return {
944
1070
  isError: true,
945
- content: [{ type: "text", text: `Error: ${data.error}` }]
1071
+ content: [{ type: "text", text: `Error: ${err.message}` }]
946
1072
  };
947
1073
  }
948
- const created = data.tasks.map((t) => `#${t.taskNumber} msg=${t.messageId.slice(0, 8)} "${t.title}"`).join("\n");
949
- const threadHints = data.tasks.map((t) => `#${t.taskNumber} \u2192 send_message to "${channel}:${t.messageId.slice(0, 8)}"`).join("\n");
950
- return {
951
- content: [
1074
+ }
1075
+ );
1076
+ server.tool(
1077
+ "create_tasks",
1078
+ "Create one or more new task-messages in a top-level channel or DM. This is a convenience helper for creating a brand-new message and publishing it as a task-message in the chat flow. Thread messages cannot become tasks. It does not claim the task for you; if you want to own it, still call claim_tasks afterward. It is not a separate task board outside the chat flow. Typical uses are breaking down a larger task into parallel subtasks or batch-creating new work for others to claim. Do not use this to convert an existing message \u2014 use claim_tasks with message_ids instead. If the work already exists as a task, either claim that task or leave it alone; do not create a second task/message for the same work.",
1079
+ {
1080
+ channel: z2.string().describe("The channel to create tasks in \u2014 e.g. '#engineering'"),
1081
+ tasks: z2.array(
1082
+ z2.object({
1083
+ title: z2.string().describe("Task title")
1084
+ })
1085
+ ).describe("Array of tasks to create")
1086
+ },
1087
+ async ({ channel, tasks }) => {
1088
+ try {
1089
+ const { response: res, data } = await executeJsonRequest(
1090
+ `${serverUrl}/internal/agent/${agentId}/tasks`,
952
1091
  {
953
- type: "text",
954
- text: `Created ${data.tasks.length} task(s) in ${channel}:
1092
+ method: "POST",
1093
+ headers: commonHeaders,
1094
+ body: JSON.stringify({ channel, tasks })
1095
+ },
1096
+ {
1097
+ toolName: "create_tasks",
1098
+ target: channel,
1099
+ fetchImpl: bridgeFetch
1100
+ }
1101
+ );
1102
+ if (!res.ok) {
1103
+ return {
1104
+ isError: true,
1105
+ content: [{ type: "text", text: `Error: ${data.error}` }]
1106
+ };
1107
+ }
1108
+ const created = data.tasks.map((t) => `#${t.taskNumber} msg=${t.messageId.slice(0, 8)} "${t.title}"`).join("\n");
1109
+ const threadHints = data.tasks.map((t) => `#${t.taskNumber} \u2192 send_message to "${channel}:${t.messageId.slice(0, 8)}"`).join("\n");
1110
+ return {
1111
+ content: [
1112
+ {
1113
+ type: "text",
1114
+ text: `Created ${data.tasks.length} task(s) in ${channel}:
955
1115
  ${created}
956
1116
 
957
1117
  To follow up in each task's thread:
958
1118
  ${threadHints}`
959
- }
960
- ]
961
- };
962
- } catch (err) {
963
- return {
964
- isError: true,
965
- content: [{ type: "text", text: `Error: ${err.message}` }]
966
- };
1119
+ }
1120
+ ]
1121
+ };
1122
+ } catch (err) {
1123
+ return {
1124
+ isError: true,
1125
+ content: [{ type: "text", text: `Error: ${err.message}` }]
1126
+ };
1127
+ }
967
1128
  }
968
- }
969
- );
970
- server.tool(
971
- "claim_tasks",
972
- `Claim tasks so you are assigned to work on them. Two modes:
1129
+ );
1130
+ server.tool(
1131
+ "claim_tasks",
1132
+ `Claim tasks so you are assigned to work on them. Two modes:
973
1133
  1. By task number: claim existing tasks shown in list_tasks. Use task_numbers=[1, 3].
974
1134
  2. By message ID: convert a regular top-level message into a task and claim it. Use message_ids=["a1b2c3d4"]. The message ID is the 8-character msg= value from received messages or read_history.
975
1135
 
976
1136
  Thread messages and system messages (e.g. task-claim / task-status announcements) cannot be claimed or converted into tasks \u2014 if a system message describes an action you should take, just do it; otherwise ignore it. If a task is in "todo" status, claiming auto-advances it to "in_progress". If another agent already claimed it, the claim fails \u2014 do not work on that task, move on. Always claim before starting any work to prevent duplicate effort.`,
977
- {
978
- channel: z.string().describe("The channel \u2014 e.g. '#engineering'"),
979
- task_numbers: z.array(z.number()).optional().describe("Task numbers to claim (from list_tasks output, e.g. [1, 3])"),
980
- message_ids: z.array(z.string()).optional().describe("Message IDs or short ID prefixes (the 8-char msg= value, e.g. ['a1b2c3d4']). Converts a regular top-level message to a task and claims it. Thread messages are not allowed.")
981
- },
982
- async ({ channel, task_numbers, message_ids }) => {
983
- try {
984
- if ((!task_numbers || task_numbers.length === 0) && (!message_ids || message_ids.length === 0)) {
1137
+ {
1138
+ channel: z2.string().describe("The channel \u2014 e.g. '#engineering'"),
1139
+ task_numbers: z2.array(z2.number()).optional().describe("Task numbers to claim (from list_tasks output, e.g. [1, 3])"),
1140
+ message_ids: z2.array(z2.string()).optional().describe("Message IDs or short ID prefixes (the 8-char msg= value, e.g. ['a1b2c3d4']). Converts a regular top-level message to a task and claims it. Thread messages are not allowed.")
1141
+ },
1142
+ async ({ channel, task_numbers, message_ids }) => {
1143
+ try {
1144
+ if ((!task_numbers || task_numbers.length === 0) && (!message_ids || message_ids.length === 0)) {
1145
+ return {
1146
+ content: [{ type: "text", text: "Error: provide at least one of task_numbers or message_ids" }]
1147
+ };
1148
+ }
1149
+ const body = { channel };
1150
+ if (task_numbers && task_numbers.length > 0) body.task_numbers = task_numbers;
1151
+ if (message_ids && message_ids.length > 0) body.message_ids = message_ids;
1152
+ const { response: res, data } = await executeJsonRequest(
1153
+ `${serverUrl}/internal/agent/${agentId}/tasks/claim`,
1154
+ {
1155
+ method: "POST",
1156
+ headers: commonHeaders,
1157
+ body: JSON.stringify(body)
1158
+ },
1159
+ {
1160
+ toolName: "claim_tasks",
1161
+ target: channel,
1162
+ fetchImpl: bridgeFetch
1163
+ }
1164
+ );
1165
+ if (!res.ok) {
1166
+ return {
1167
+ isError: true,
1168
+ content: [{ type: "text", text: `Error: ${data.error}` }]
1169
+ };
1170
+ }
1171
+ const lines = data.results.map((r) => {
1172
+ const label = r.taskNumber ? `#${r.taskNumber}` : `msg:${r.messageId}`;
1173
+ if (r.success) {
1174
+ const msgShort = r.messageId ? r.messageId.slice(0, 8) : "";
1175
+ return `${label} (msg:${msgShort}): claimed`;
1176
+ }
1177
+ return `${label}: FAILED \u2014 ${r.reason || "already claimed"}`;
1178
+ });
1179
+ const succeeded = data.results.filter((r) => r.success).length;
1180
+ const failed = data.results.length - succeeded;
1181
+ let summary = `${succeeded} claimed`;
1182
+ if (failed > 0) summary += `, ${failed} failed`;
1183
+ const claimedMsgs = data.results.filter((r) => r.success && r.messageId).map((r) => `#${r.taskNumber} \u2192 send_message to "${channel}:${r.messageId.slice(0, 8)}"`).join("\n");
1184
+ const threadHint = claimedMsgs ? `
1185
+
1186
+ Follow up in each task's thread:
1187
+ ${claimedMsgs}` : "";
985
1188
  return {
986
- content: [{ type: "text", text: "Error: provide at least one of task_numbers or message_ids" }]
1189
+ content: [
1190
+ {
1191
+ type: "text",
1192
+ text: `Claim results (${summary}):
1193
+ ${lines.join("\n")}${threadHint}`
1194
+ }
1195
+ ]
987
1196
  };
988
- }
989
- const body = { channel };
990
- if (task_numbers && task_numbers.length > 0) body.task_numbers = task_numbers;
991
- if (message_ids && message_ids.length > 0) body.message_ids = message_ids;
992
- const { response: res, data } = await executeJsonRequest(
993
- `${serverUrl}/internal/agent/${agentId}/tasks/claim`,
994
- {
995
- method: "POST",
996
- headers: commonHeaders,
997
- body: JSON.stringify(body)
998
- },
999
- {
1000
- toolName: "claim_tasks",
1001
- target: channel,
1002
- fetchImpl: bridgeFetch
1003
- }
1004
- );
1005
- if (!res.ok) {
1197
+ } catch (err) {
1006
1198
  return {
1007
1199
  isError: true,
1008
- content: [{ type: "text", text: `Error: ${data.error}` }]
1200
+ content: [{ type: "text", text: `Error: ${err.message}` }]
1009
1201
  };
1010
1202
  }
1011
- const lines = data.results.map((r) => {
1012
- const label = r.taskNumber ? `#${r.taskNumber}` : `msg:${r.messageId}`;
1013
- if (r.success) {
1014
- const msgShort = r.messageId ? r.messageId.slice(0, 8) : "";
1015
- return `${label} (msg:${msgShort}): claimed`;
1016
- }
1017
- return `${label}: FAILED \u2014 ${r.reason || "already claimed"}`;
1018
- });
1019
- const succeeded = data.results.filter((r) => r.success).length;
1020
- const failed = data.results.length - succeeded;
1021
- let summary = `${succeeded} claimed`;
1022
- if (failed > 0) summary += `, ${failed} failed`;
1023
- const claimedMsgs = data.results.filter((r) => r.success && r.messageId).map((r) => `#${r.taskNumber} \u2192 send_message to "${channel}:${r.messageId.slice(0, 8)}"`).join("\n");
1024
- const threadHint = claimedMsgs ? `
1025
-
1026
- Follow up in each task's thread:
1027
- ${claimedMsgs}` : "";
1028
- return {
1029
- content: [
1203
+ }
1204
+ );
1205
+ server.tool(
1206
+ "unclaim_task",
1207
+ "Release your claim on a task so someone else can pick it up. Only use this if you can no longer work on the task \u2014 not as a way to mark it done. Use update_task_status to change status instead.",
1208
+ {
1209
+ channel: z2.string().describe("The channel \u2014 e.g. '#engineering'"),
1210
+ task_number: z2.number().describe("The task number to unclaim (e.g. 3)")
1211
+ },
1212
+ async ({ channel, task_number }) => {
1213
+ try {
1214
+ const { response: res, data } = await executeJsonRequest(
1215
+ `${serverUrl}/internal/agent/${agentId}/tasks/unclaim`,
1030
1216
  {
1031
- type: "text",
1032
- text: `Claim results (${summary}):
1033
- ${lines.join("\n")}${threadHint}`
1217
+ method: "POST",
1218
+ headers: commonHeaders,
1219
+ body: JSON.stringify({ channel, task_number })
1220
+ },
1221
+ {
1222
+ toolName: "unclaim_task",
1223
+ target: channel,
1224
+ fetchImpl: bridgeFetch
1034
1225
  }
1035
- ]
1036
- };
1037
- } catch (err) {
1038
- return {
1039
- isError: true,
1040
- content: [{ type: "text", text: `Error: ${err.message}` }]
1041
- };
1042
- }
1043
- }
1044
- );
1045
- server.tool(
1046
- "unclaim_task",
1047
- "Release your claim on a task so someone else can pick it up. Only use this if you can no longer work on the task \u2014 not as a way to mark it done. Use update_task_status to change status instead.",
1048
- {
1049
- channel: z.string().describe("The channel \u2014 e.g. '#engineering'"),
1050
- task_number: z.number().describe("The task number to unclaim (e.g. 3)")
1051
- },
1052
- async ({ channel, task_number }) => {
1053
- try {
1054
- const { response: res, data } = await executeJsonRequest(
1055
- `${serverUrl}/internal/agent/${agentId}/tasks/unclaim`,
1056
- {
1057
- method: "POST",
1058
- headers: commonHeaders,
1059
- body: JSON.stringify({ channel, task_number })
1060
- },
1061
- {
1062
- toolName: "unclaim_task",
1063
- target: channel,
1064
- fetchImpl: bridgeFetch
1226
+ );
1227
+ if (!res.ok) {
1228
+ return {
1229
+ isError: true,
1230
+ content: [{ type: "text", text: `Error: ${data.error}` }]
1231
+ };
1065
1232
  }
1066
- );
1067
- if (!res.ok) {
1233
+ return {
1234
+ content: [
1235
+ { type: "text", text: `#${task_number} unclaimed \u2014 now open.` }
1236
+ ]
1237
+ };
1238
+ } catch (err) {
1068
1239
  return {
1069
1240
  isError: true,
1070
- content: [{ type: "text", text: `Error: ${data.error}` }]
1241
+ content: [{ type: "text", text: `Error: ${err.message}` }]
1071
1242
  };
1072
1243
  }
1073
- return {
1074
- content: [
1075
- { type: "text", text: `#${task_number} unclaimed \u2014 now open.` }
1076
- ]
1077
- };
1078
- } catch (err) {
1079
- return {
1080
- isError: true,
1081
- content: [{ type: "text", text: `Error: ${err.message}` }]
1082
- };
1083
1244
  }
1084
- }
1085
- );
1086
- server.tool(
1087
- "update_task_status",
1088
- "Update a task's progress status. You must be the task's assignee to update it. Use in_review when your work is ready for human validation. Only set done for trivial tasks or after explicit approval. Valid transitions: todo\u2192in_progress, in_progress\u2192in_review or done, in_review\u2192done or back to in_progress.",
1089
- {
1090
- channel: z.string().describe("The channel \u2014 e.g. '#engineering'"),
1091
- task_number: z.number().describe("The task number to update (e.g. 3)"),
1092
- status: z.enum(["todo", "in_progress", "in_review", "done"]).describe("The new status")
1093
- },
1094
- async ({ channel, task_number, status }) => {
1095
- try {
1096
- const { response: res, data } = await executeJsonRequest(
1097
- `${serverUrl}/internal/agent/${agentId}/tasks/update-status`,
1098
- {
1099
- method: "POST",
1100
- headers: commonHeaders,
1101
- body: JSON.stringify({ channel, task_number, status })
1102
- },
1103
- {
1104
- toolName: "update_task_status",
1105
- target: channel,
1106
- fetchImpl: bridgeFetch
1245
+ );
1246
+ server.tool(
1247
+ "update_task_status",
1248
+ "Update a task's progress status. You must be the task's assignee to update it. Use in_review when your work is ready for human validation. Only set done for trivial tasks or after explicit approval. Valid transitions: todo\u2192in_progress, in_progress\u2192in_review or done, in_review\u2192done or back to in_progress.",
1249
+ {
1250
+ channel: z2.string().describe("The channel \u2014 e.g. '#engineering'"),
1251
+ task_number: z2.number().describe("The task number to update (e.g. 3)"),
1252
+ status: z2.enum(["todo", "in_progress", "in_review", "done"]).describe("The new status")
1253
+ },
1254
+ async ({ channel, task_number, status }) => {
1255
+ try {
1256
+ const { response: res, data } = await executeJsonRequest(
1257
+ `${serverUrl}/internal/agent/${agentId}/tasks/update-status`,
1258
+ {
1259
+ method: "POST",
1260
+ headers: commonHeaders,
1261
+ body: JSON.stringify({ channel, task_number, status })
1262
+ },
1263
+ {
1264
+ toolName: "update_task_status",
1265
+ target: channel,
1266
+ fetchImpl: bridgeFetch
1267
+ }
1268
+ );
1269
+ if (!res.ok) {
1270
+ return {
1271
+ isError: true,
1272
+ content: [{ type: "text", text: `Error: ${data.error}` }]
1273
+ };
1107
1274
  }
1108
- );
1109
- if (!res.ok) {
1275
+ return {
1276
+ content: [
1277
+ { type: "text", text: `#${task_number} moved to ${status}.` }
1278
+ ]
1279
+ };
1280
+ } catch (err) {
1110
1281
  return {
1111
1282
  isError: true,
1112
- content: [{ type: "text", text: `Error: ${data.error}` }]
1283
+ content: [{ type: "text", text: `Error: ${err.message}` }]
1113
1284
  };
1114
1285
  }
1115
- return {
1116
- content: [
1117
- { type: "text", text: `#${task_number} moved to ${status}.` }
1118
- ]
1119
- };
1120
- } catch (err) {
1121
- return {
1122
- isError: true,
1123
- content: [{ type: "text", text: `Error: ${err.message}` }]
1124
- };
1125
1286
  }
1126
- }
1127
- );
1128
- function formatReminder(r) {
1129
- const fireLocal = toLocalTime(r.fireAt);
1130
- const ref = r.msgRef ? ` ref=${r.msgRef}` : "";
1131
- const repeat = r.recurrence ? ` repeat=${r.recurrence.description}` : "";
1132
- return `#${r.reminderId.slice(0, 8)} [${r.status}] fires=${fireLocal} "${r.title}"${ref}${repeat}`;
1133
- }
1134
- server.tool(
1135
- "schedule_reminder",
1136
- "Schedule a reminder that will fire at a future time and wake you up with a DM. Use this when you need to follow up on something after a delay, at a specific time, or on a schedule. The reminder persists across daemon restarts. For one-shot reminders, provide delay_seconds (preferred) OR fire_at. For recurring reminders, provide repeat; you may also combine repeat with delay_seconds or fire_at to pin the first fire.",
1137
- {
1138
- title: z.string().describe("Short description of what the reminder is about. This is what you'll see when it fires."),
1139
- delay_seconds: z.number().int().positive().optional().describe("Preferred for relative times. Fires this many seconds from now (server-computed, timezone-safe). Use this for any 'in N seconds/minutes/hours' request."),
1140
- fire_at: z.string().optional().describe("ISO-8601 UTC timestamp, e.g. '2026-04-21T09:00:00Z'. Use only for absolute calendar times ('tomorrow 9am UTC'). Your local clock is NOT trusted as UTC \u2014 if you mean a relative delay, use delay_seconds instead."),
1141
- repeat: z.string().optional().describe("Recurrence rule. Supported forms: 'every:15m' | 'every:2h' | 'every:1d' (fixed interval) | 'daily@09:00' (in your local tz, snapshotted at creation) | 'weekly:mon,fri@09:00' (specific weekdays). The reminder auto-reschedules after each fire until you cancel it."),
1142
- channel: z.string().optional().describe("Optional explicit channel to post a receipt system message in (format: '#channel', 'dm:@name', or thread ref). Use this only if you want the receipt somewhere other than the anchor message's channel."),
1143
- msg_id: z.string().describe("Required anchor message id (from a received message). Resolve it explicitly and pass it in; if you cannot resolve one, do not create the reminder.")
1144
- },
1145
- async ({ title, delay_seconds, fire_at, repeat, channel, msg_id }) => {
1146
- try {
1147
- const body = { title, msgId: msg_id };
1148
- if (delay_seconds !== void 0) body.delaySeconds = delay_seconds;
1149
- if (fire_at !== void 0) body.fireAt = fire_at;
1150
- if (repeat !== void 0) {
1151
- body.repeat = repeat;
1152
- body.tz = Intl.DateTimeFormat().resolvedOptions().timeZone;
1153
- }
1154
- if (channel !== void 0) body.channel = channel;
1155
- const { response: res, data } = await executeJsonRequest(
1156
- `${serverUrl}/internal/agent/${agentId}/reminders`,
1157
- {
1158
- method: "POST",
1159
- headers: commonHeaders,
1160
- body: JSON.stringify(body)
1161
- },
1162
- {
1163
- toolName: "schedule_reminder",
1164
- fetchImpl: bridgeFetch
1287
+ );
1288
+ server.tool(
1289
+ "schedule_reminder",
1290
+ "Schedule a reminder that will fire at a future time and wake you up with a DM. Use this when you need to follow up on something after a delay, at a specific time, or on a schedule. The reminder persists across daemon restarts. For one-shot reminders, provide delay_seconds (preferred) OR fire_at. For recurring reminders, provide repeat; you may also combine repeat with delay_seconds or fire_at to pin the first fire.",
1291
+ {
1292
+ title: z2.string().describe("Short description of what the reminder is about. This is what you'll see when it fires."),
1293
+ delay_seconds: z2.number().int().positive().optional().describe("Preferred for relative times. Fires this many seconds from now (server-computed, timezone-safe). Use this for any 'in N seconds/minutes/hours' request."),
1294
+ fire_at: z2.string().optional().describe("ISO-8601 UTC timestamp, e.g. '2026-04-21T09:00:00Z'. Use only for absolute calendar times ('tomorrow 9am UTC'). Your local clock is NOT trusted as UTC \u2014 if you mean a relative delay, use delay_seconds instead."),
1295
+ repeat: z2.string().optional().describe("Recurrence rule. Supported forms: 'every:15m' | 'every:2h' | 'every:1d' (fixed interval) | 'daily@09:00' (in your local tz, snapshotted at creation) | 'weekly:mon,fri@09:00' (specific weekdays). The reminder auto-reschedules after each fire until you cancel it."),
1296
+ channel: z2.string().optional().describe("Optional explicit channel to post a receipt system message in (format: '#channel', 'dm:@name', or thread ref). Use this only if you want the receipt somewhere other than the anchor message's channel."),
1297
+ msg_id: z2.string().describe("Required anchor message id (from a received message). Resolve it explicitly and pass it in; if you cannot resolve one, do not create the reminder.")
1298
+ },
1299
+ async ({ title, delay_seconds, fire_at, repeat, channel, msg_id }) => {
1300
+ try {
1301
+ const body = { title, msgId: msg_id };
1302
+ if (delay_seconds !== void 0) body.delaySeconds = delay_seconds;
1303
+ if (fire_at !== void 0) body.fireAt = fire_at;
1304
+ if (repeat !== void 0) {
1305
+ body.repeat = repeat;
1306
+ body.tz = Intl.DateTimeFormat().resolvedOptions().timeZone;
1165
1307
  }
1166
- );
1167
- if (!res.ok || !data.reminder) {
1168
- return { isError: true, content: [{ type: "text", text: `Error: ${data.error || res.statusText}` }] };
1169
- }
1170
- const lines = [`Reminder scheduled: ${formatReminder(data.reminder)}`];
1171
- if (data.warning) lines.push(`Warning: ${data.warning}`);
1172
- return { content: [{ type: "text", text: lines.join("\n") }] };
1173
- } catch (err) {
1174
- return {
1175
- isError: true,
1176
- content: [{ type: "text", text: `Error: ${err.message}` }]
1177
- };
1178
- }
1179
- }
1180
- );
1181
- server.tool(
1182
- "list_reminders",
1183
- "List your own reminders. Defaults to scheduled (pending) ones; pass status to include fired or canceled.",
1184
- {
1185
- status: z.string().optional().describe("Comma-separated statuses to include (scheduled,fired,canceled). Defaults to 'scheduled'.")
1186
- },
1187
- async ({ status }) => {
1188
- try {
1189
- const qs = new URLSearchParams();
1190
- qs.set("status", status && status.trim().length > 0 ? status.trim() : "scheduled");
1191
- const { response: res, data } = await executeJsonRequest(
1192
- `${serverUrl}/internal/agent/${agentId}/reminders?${qs.toString()}`,
1193
- { method: "GET", headers: commonHeaders },
1194
- {
1195
- toolName: "list_reminders",
1196
- fetchImpl: bridgeFetch
1308
+ if (channel !== void 0) body.channel = channel;
1309
+ const { response: res, data } = await executeJsonRequest(
1310
+ `${serverUrl}/internal/agent/${agentId}/reminders`,
1311
+ {
1312
+ method: "POST",
1313
+ headers: commonHeaders,
1314
+ body: JSON.stringify(body)
1315
+ },
1316
+ {
1317
+ toolName: "schedule_reminder",
1318
+ fetchImpl: bridgeFetch
1319
+ }
1320
+ );
1321
+ if (!res.ok || !data.reminder) {
1322
+ return { isError: true, content: [{ type: "text", text: `Error: ${data.error || res.statusText}` }] };
1197
1323
  }
1198
- );
1199
- if (!res.ok) {
1200
- return { isError: true, content: [{ type: "text", text: `Error: ${data.error || res.statusText}` }] };
1201
- }
1202
- const list = data.reminders ?? [];
1203
- if (list.length === 0) {
1204
- return { content: [{ type: "text", text: "No reminders." }] };
1324
+ const lines = [`Reminder scheduled: ${formatReminder(data.reminder)}`];
1325
+ if (data.warning) lines.push(`Warning: ${data.warning}`);
1326
+ return { content: [{ type: "text", text: lines.join("\n") }] };
1327
+ } catch (err) {
1328
+ return {
1329
+ isError: true,
1330
+ content: [{ type: "text", text: `Error: ${err.message}` }]
1331
+ };
1205
1332
  }
1206
- return {
1207
- content: [
1208
- { type: "text", text: list.map(formatReminder).join("\n") }
1209
- ]
1210
- };
1211
- } catch (err) {
1212
- return {
1213
- isError: true,
1214
- content: [{ type: "text", text: `Error: ${err.message}` }]
1215
- };
1216
1333
  }
1217
- }
1218
- );
1219
- server.tool(
1220
- "cancel_reminder",
1221
- "Cancel one of your own scheduled reminders by id. Only reminders in 'scheduled' status can be canceled.",
1222
- {
1223
- reminder_id: z.string().describe("The reminder id (full uuid, or the short 8-char prefix shown by schedule_reminder / list_reminders).")
1224
- },
1225
- async ({ reminder_id }) => {
1226
- try {
1227
- let fullId = reminder_id;
1228
- if (reminder_id.length < 32) {
1229
- const { response: listRes, data: listData } = await executeJsonRequest(
1230
- `${serverUrl}/internal/agent/${agentId}/reminders?status=scheduled`,
1334
+ );
1335
+ server.tool(
1336
+ "list_reminders",
1337
+ "List your own reminders. Defaults to scheduled (pending) ones; pass status to include fired or canceled.",
1338
+ {
1339
+ status: z2.string().optional().describe("Comma-separated statuses to include (scheduled,fired,canceled). Defaults to 'scheduled'.")
1340
+ },
1341
+ async ({ status }) => {
1342
+ try {
1343
+ const qs = new URLSearchParams();
1344
+ qs.set("status", status && status.trim().length > 0 ? status.trim() : "scheduled");
1345
+ const { response: res, data } = await executeJsonRequest(
1346
+ `${serverUrl}/internal/agent/${agentId}/reminders?${qs.toString()}`,
1231
1347
  { method: "GET", headers: commonHeaders },
1232
1348
  {
1233
- toolName: "cancel_reminder.resolve",
1349
+ toolName: "list_reminders",
1234
1350
  fetchImpl: bridgeFetch
1235
1351
  }
1236
1352
  );
1237
- if (!listRes.ok) {
1238
- return { isError: true, content: [{ type: "text", text: `Error: ${listData.error || listRes.statusText}` }] };
1353
+ if (!res.ok) {
1354
+ return { isError: true, content: [{ type: "text", text: `Error: ${data.error || res.statusText}` }] };
1239
1355
  }
1240
- const matches = (listData.reminders ?? []).filter((r) => r.reminderId.startsWith(reminder_id));
1241
- if (matches.length === 0) {
1242
- return { isError: true, content: [{ type: "text", text: `No scheduled reminder matches id prefix '${reminder_id}'.` }] };
1356
+ const list = data.reminders ?? [];
1357
+ if (list.length === 0) {
1358
+ return { content: [{ type: "text", text: "No reminders." }] };
1243
1359
  }
1244
- if (matches.length > 1) {
1245
- return { isError: true, content: [{ type: "text", text: `Ambiguous id prefix '${reminder_id}' matches ${matches.length} reminders; pass a longer id.` }] };
1246
- }
1247
- fullId = matches[0].reminderId;
1360
+ return {
1361
+ content: [
1362
+ { type: "text", text: list.map(formatReminder).join("\n") }
1363
+ ]
1364
+ };
1365
+ } catch (err) {
1366
+ return {
1367
+ isError: true,
1368
+ content: [{ type: "text", text: `Error: ${err.message}` }]
1369
+ };
1248
1370
  }
1249
- const { response: res, data } = await executeJsonRequest(
1250
- `${serverUrl}/internal/agent/${agentId}/reminders/${fullId}`,
1251
- { method: "DELETE", headers: commonHeaders },
1252
- {
1253
- toolName: "cancel_reminder",
1254
- fetchImpl: bridgeFetch
1371
+ }
1372
+ );
1373
+ server.tool(
1374
+ "cancel_reminder",
1375
+ "Cancel one of your own scheduled reminders by id. Only reminders in 'scheduled' status can be canceled.",
1376
+ {
1377
+ reminder_id: z2.string().describe("The reminder id (full uuid, or the short 8-char prefix shown by schedule_reminder / list_reminders).")
1378
+ },
1379
+ async ({ reminder_id }) => {
1380
+ try {
1381
+ let fullId = reminder_id;
1382
+ if (reminder_id.length < 32) {
1383
+ const { response: listRes, data: listData } = await executeJsonRequest(
1384
+ `${serverUrl}/internal/agent/${agentId}/reminders?status=scheduled`,
1385
+ { method: "GET", headers: commonHeaders },
1386
+ {
1387
+ toolName: "cancel_reminder.resolve",
1388
+ fetchImpl: bridgeFetch
1389
+ }
1390
+ );
1391
+ if (!listRes.ok) {
1392
+ return { isError: true, content: [{ type: "text", text: `Error: ${listData.error || listRes.statusText}` }] };
1393
+ }
1394
+ const matches = (listData.reminders ?? []).filter((r) => r.reminderId.startsWith(reminder_id));
1395
+ if (matches.length === 0) {
1396
+ return { isError: true, content: [{ type: "text", text: `No scheduled reminder matches id prefix '${reminder_id}'.` }] };
1397
+ }
1398
+ if (matches.length > 1) {
1399
+ return { isError: true, content: [{ type: "text", text: `Ambiguous id prefix '${reminder_id}' matches ${matches.length} reminders; pass a longer id.` }] };
1400
+ }
1401
+ fullId = matches[0].reminderId;
1255
1402
  }
1256
- );
1257
- if (!res.ok || !data.reminder) {
1258
- return { isError: true, content: [{ type: "text", text: `Error: ${data.error || res.statusText}` }] };
1403
+ const { response: res, data } = await executeJsonRequest(
1404
+ `${serverUrl}/internal/agent/${agentId}/reminders/${fullId}`,
1405
+ { method: "DELETE", headers: commonHeaders },
1406
+ {
1407
+ toolName: "cancel_reminder",
1408
+ fetchImpl: bridgeFetch
1409
+ }
1410
+ );
1411
+ if (!res.ok || !data.reminder) {
1412
+ return { isError: true, content: [{ type: "text", text: `Error: ${data.error || res.statusText}` }] };
1413
+ }
1414
+ return {
1415
+ content: [
1416
+ { type: "text", text: `Reminder canceled: ${formatReminder(data.reminder)}` }
1417
+ ]
1418
+ };
1419
+ } catch (err) {
1420
+ return {
1421
+ isError: true,
1422
+ content: [{ type: "text", text: `Error: ${err.message}` }]
1423
+ };
1259
1424
  }
1260
- return {
1261
- content: [
1262
- { type: "text", text: `Reminder canceled: ${formatReminder(data.reminder)}` }
1263
- ]
1264
- };
1265
- } catch (err) {
1266
- return {
1267
- isError: true,
1268
- content: [{ type: "text", text: `Error: ${err.message}` }]
1269
- };
1270
1425
  }
1271
- }
1272
- );
1426
+ );
1427
+ }
1428
+ var formatMessages2;
1429
+ var formatReminder2;
1273
1430
  var transport = new StdioServerTransport();
1274
1431
  await server.connect(transport);