@slock-ai/daemon 0.40.1 → 0.41.0
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/dist/chat-bridge.js +1041 -758
- package/dist/{chunk-E6OOH3IC.js → chunk-JG7ONJZ6.js} +46 -46
- package/dist/{chunk-6YLMU56U.js → chunk-KFVDXO5Y.js} +779 -51
- package/dist/cli/index.js +170 -1
- package/dist/core.js +2 -2
- package/dist/index.js +2 -2
- package/package.json +1 -1
package/dist/chat-bridge.js
CHANGED
|
@@ -2,12 +2,137 @@
|
|
|
2
2
|
import {
|
|
3
3
|
buildFetchDispatcher,
|
|
4
4
|
logger
|
|
5
|
-
} from "./chunk-
|
|
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,16 +335,26 @@ 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 launchId = "";
|
|
340
|
+
var deprecatedShimMode = false;
|
|
213
341
|
for (let i = 0; i < args.length; i++) {
|
|
214
342
|
if (args[i] === "--agent-id" && args[i + 1]) agentId = args[++i];
|
|
215
343
|
if (args[i] === "--server-url" && args[i + 1]) serverUrl = args[++i];
|
|
216
344
|
if (args[i] === "--auth-token" && args[i + 1]) authToken = args[++i];
|
|
345
|
+
if (args[i] === "--runtime" && args[i + 1]) runtime = args[++i];
|
|
346
|
+
if (args[i] === "--launch-id" && args[i + 1]) launchId = args[++i];
|
|
347
|
+
if (args[i] === "--deprecated-shim") deprecatedShimMode = true;
|
|
217
348
|
}
|
|
218
349
|
if (!agentId) {
|
|
219
350
|
console.error("Missing --agent-id");
|
|
220
351
|
process.exit(1);
|
|
221
352
|
}
|
|
222
353
|
var commonHeaders = buildChatBridgeCommonHeaders(authToken);
|
|
354
|
+
var runtimeActionHeaders = {
|
|
355
|
+
...commonHeaders,
|
|
356
|
+
...launchId ? { "X-Agent-Launch-Id": launchId } : {}
|
|
357
|
+
};
|
|
223
358
|
function bridgeFetch(url, init = {}) {
|
|
224
359
|
const dispatcher = buildFetchDispatcher(url, process.env);
|
|
225
360
|
const requestInit = dispatcher ? { ...init, dispatcher } : init;
|
|
@@ -367,61 +502,102 @@ function formatSenderHandle(message) {
|
|
|
367
502
|
const senderDescription = message.sender_description ?? message.senderDescription ?? null;
|
|
368
503
|
return senderDescription ? `@${senderName} \u2014 ${senderDescription}` : `@${senderName}`;
|
|
369
504
|
}
|
|
505
|
+
async function listServerChannels() {
|
|
506
|
+
const { response: res, data } = await executeJsonRequest(
|
|
507
|
+
`${serverUrl}/internal/agent/${agentId}/server`,
|
|
508
|
+
{ method: "GET", headers: commonHeaders },
|
|
509
|
+
{
|
|
510
|
+
toolName: "list_server.channels",
|
|
511
|
+
fetchImpl: bridgeFetch
|
|
512
|
+
}
|
|
513
|
+
);
|
|
514
|
+
if (!res.ok) {
|
|
515
|
+
throw new Error("Failed to load server channels");
|
|
516
|
+
}
|
|
517
|
+
return data.channels ?? [];
|
|
518
|
+
}
|
|
519
|
+
function parseRegularChannelTarget(target) {
|
|
520
|
+
if (!target.startsWith("#")) return null;
|
|
521
|
+
if (target.includes(":")) return null;
|
|
522
|
+
const name = target.slice(1).trim();
|
|
523
|
+
return name.length > 0 ? name : null;
|
|
524
|
+
}
|
|
525
|
+
async function resolveRegularChannelTarget(target) {
|
|
526
|
+
const channelName = parseRegularChannelTarget(target);
|
|
527
|
+
if (!channelName) {
|
|
528
|
+
throw new Error("Target must be a regular channel in the form '#channel-name'");
|
|
529
|
+
}
|
|
530
|
+
const channels = await listServerChannels();
|
|
531
|
+
const channel = channels.find((candidate) => candidate.name === channelName);
|
|
532
|
+
if (!channel) {
|
|
533
|
+
throw new Error(`Channel not found: ${target}`);
|
|
534
|
+
}
|
|
535
|
+
return channel;
|
|
536
|
+
}
|
|
370
537
|
var server = new McpServer({
|
|
371
538
|
name: "chat",
|
|
372
539
|
version: "1.0.0"
|
|
373
540
|
});
|
|
541
|
+
function logDeprecatedShimInvocation(tool) {
|
|
542
|
+
logger.warn(
|
|
543
|
+
`[ChatBridgeDeprecatedShim] tool=${tool.toolName} runtime=${runtime} agent_id=${agentId} outcome=deprecated`
|
|
544
|
+
);
|
|
545
|
+
}
|
|
546
|
+
function registerDeprecatedTool(tool) {
|
|
547
|
+
server.tool(
|
|
548
|
+
tool.toolName,
|
|
549
|
+
tool.description,
|
|
550
|
+
tool.schema,
|
|
551
|
+
async () => {
|
|
552
|
+
logDeprecatedShimInvocation(tool);
|
|
553
|
+
return {
|
|
554
|
+
isError: true,
|
|
555
|
+
content: [{ type: "text", text: buildDeprecatedMcpToolErrorText(tool) }]
|
|
556
|
+
};
|
|
557
|
+
}
|
|
558
|
+
);
|
|
559
|
+
}
|
|
560
|
+
if (deprecatedShimMode) {
|
|
561
|
+
for (const tool of DEPRECATED_MCP_TOOL_DEFINITIONS) {
|
|
562
|
+
registerDeprecatedTool(tool);
|
|
563
|
+
}
|
|
564
|
+
}
|
|
565
|
+
var RUNTIME_PROFILE_MIGRATION_DONE_TOOL_NAME = "runtime_profile_migration_done";
|
|
374
566
|
server.tool(
|
|
375
|
-
|
|
376
|
-
"
|
|
567
|
+
RUNTIME_PROFILE_MIGRATION_DONE_TOOL_NAME,
|
|
568
|
+
"Complete the current Runtime Profile migration. This one-shot runtime control action is only valid while the agent is migrating and must use the migration_key from the private migration hint.",
|
|
377
569
|
{
|
|
378
|
-
|
|
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")
|
|
570
|
+
migration_key: z2.string().describe("The migration key from the Runtime Profile migration hint")
|
|
383
571
|
},
|
|
384
|
-
async ({
|
|
572
|
+
async ({ migration_key }) => {
|
|
573
|
+
const key = migration_key.trim();
|
|
574
|
+
if (!key) {
|
|
575
|
+
return {
|
|
576
|
+
isError: true,
|
|
577
|
+
content: [{ type: "text", text: "Error: migration_key is required" }]
|
|
578
|
+
};
|
|
579
|
+
}
|
|
385
580
|
try {
|
|
386
|
-
const { response: res, data } = await
|
|
387
|
-
`${serverUrl}/internal/agent/${agentId}/
|
|
388
|
-
|
|
581
|
+
const { response: res, data } = await executeJsonRequest(
|
|
582
|
+
`${serverUrl}/internal/agent/${agentId}/runtime-profile/migration-done`,
|
|
583
|
+
{
|
|
389
584
|
method: "POST",
|
|
390
|
-
headers:
|
|
391
|
-
body: JSON.stringify({
|
|
392
|
-
}
|
|
585
|
+
headers: runtimeActionHeaders,
|
|
586
|
+
body: JSON.stringify({ migrationKey: key })
|
|
587
|
+
},
|
|
393
588
|
{
|
|
394
|
-
|
|
589
|
+
toolName: RUNTIME_PROFILE_MIGRATION_DONE_TOOL_NAME,
|
|
395
590
|
fetchImpl: bridgeFetch
|
|
396
591
|
}
|
|
397
592
|
);
|
|
398
593
|
if (!res.ok) {
|
|
399
594
|
return {
|
|
400
|
-
|
|
401
|
-
|
|
402
|
-
]
|
|
595
|
+
isError: true,
|
|
596
|
+
content: [{ type: "text", text: `Error: ${data.error || "Runtime Profile migration is not active"}` }]
|
|
403
597
|
};
|
|
404
598
|
}
|
|
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
599
|
return {
|
|
419
|
-
content: [
|
|
420
|
-
{
|
|
421
|
-
type: "text",
|
|
422
|
-
text: `Message sent to ${target}. Message ID: ${data.messageId}${replyHint}${unreadSection}`
|
|
423
|
-
}
|
|
424
|
-
]
|
|
600
|
+
content: [{ type: "text", text: "Runtime Profile migration completed. Normal inbox delivery can resume." }]
|
|
425
601
|
};
|
|
426
602
|
} catch (err) {
|
|
427
603
|
return {
|
|
@@ -431,844 +607,951 @@ ${formatMessages(unreadToShow)}`;
|
|
|
431
607
|
}
|
|
432
608
|
}
|
|
433
609
|
);
|
|
434
|
-
|
|
435
|
-
|
|
436
|
-
|
|
437
|
-
|
|
438
|
-
|
|
439
|
-
|
|
440
|
-
|
|
441
|
-
|
|
442
|
-
|
|
443
|
-
const
|
|
444
|
-
|
|
445
|
-
|
|
610
|
+
if (!deprecatedShimMode) {
|
|
611
|
+
let formatMessages = function(messages) {
|
|
612
|
+
return messages.map((m) => {
|
|
613
|
+
const target = formatTarget(m);
|
|
614
|
+
const msgId = m.message_id ? m.message_id.slice(0, 8) : "-";
|
|
615
|
+
const time = m.timestamp ? toLocalTime(m.timestamp) : "-";
|
|
616
|
+
const senderType = ` type=${m.sender_type}`;
|
|
617
|
+
const renderedContent = m.content;
|
|
618
|
+
const attachSuffix = formatAttachmentSuffix(m.attachments);
|
|
619
|
+
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}` : ""}]` : "";
|
|
620
|
+
return `[target=${target} msg=${msgId} time=${time}${senderType}] ${formatSenderHandle(m)}: ${renderedContent}${attachSuffix}${taskSuffix}`;
|
|
621
|
+
}).join("\n");
|
|
622
|
+
}, formatReminder = function(r) {
|
|
623
|
+
const fireLocal = toLocalTime(r.fireAt);
|
|
624
|
+
const ref = r.msgRef ? ` ref=${r.msgRef}` : "";
|
|
625
|
+
const repeat = r.recurrence ? ` repeat=${r.recurrence.description}` : "";
|
|
626
|
+
return `#${r.reminderId.slice(0, 8)} [${r.status}] fires=${fireLocal} "${r.title}"${ref}${repeat}`;
|
|
627
|
+
};
|
|
628
|
+
formatMessages2 = formatMessages, formatReminder2 = formatReminder;
|
|
629
|
+
server.tool(
|
|
630
|
+
"send_message",
|
|
631
|
+
"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'.",
|
|
632
|
+
{
|
|
633
|
+
target: z2.string().describe(
|
|
634
|
+
"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'."
|
|
635
|
+
),
|
|
636
|
+
content: z2.string().describe("The message content"),
|
|
637
|
+
attachment_ids: z2.array(z2.string()).optional().describe("Optional attachment IDs from upload_file to include with the message")
|
|
638
|
+
},
|
|
639
|
+
async ({ target, content, attachment_ids }) => {
|
|
640
|
+
try {
|
|
641
|
+
const { response: res, data } = await executeRetrySafeSendRequest(
|
|
642
|
+
`${serverUrl}/internal/agent/${agentId}/send`,
|
|
643
|
+
(idempotencyKey) => ({
|
|
644
|
+
method: "POST",
|
|
645
|
+
headers: commonHeaders,
|
|
646
|
+
body: JSON.stringify({ target, content, attachmentIds: attachment_ids, idempotencyKey })
|
|
647
|
+
}),
|
|
648
|
+
{
|
|
649
|
+
target,
|
|
650
|
+
fetchImpl: bridgeFetch
|
|
651
|
+
}
|
|
652
|
+
);
|
|
653
|
+
if (!res.ok) {
|
|
654
|
+
return {
|
|
655
|
+
content: [
|
|
656
|
+
{ type: "text", text: `Error: ${data.error}` }
|
|
657
|
+
]
|
|
658
|
+
};
|
|
659
|
+
}
|
|
660
|
+
const shortId = data.messageId ? data.messageId.slice(0, 8) : null;
|
|
661
|
+
const replyHint = shortId ? ` (to reply in this message's thread, use target "${target.includes(":") ? target : target + ":" + shortId}")` : "";
|
|
662
|
+
let unreadSection = "";
|
|
663
|
+
if (data.recentUnread && data.recentUnread.length > 0) {
|
|
664
|
+
await acknowledgeReceivedMessages(data.recentUnread);
|
|
665
|
+
const unreadToShow = rememberDeliveredMessages(data.recentUnread);
|
|
666
|
+
if (unreadToShow.length > 0) {
|
|
667
|
+
unreadSection = `
|
|
668
|
+
|
|
669
|
+
--- New messages you may have missed ---
|
|
670
|
+
${formatMessages(unreadToShow)}`;
|
|
671
|
+
}
|
|
672
|
+
}
|
|
446
673
|
return {
|
|
447
|
-
|
|
448
|
-
|
|
674
|
+
content: [
|
|
675
|
+
{
|
|
676
|
+
type: "text",
|
|
677
|
+
text: `Message sent to ${target}. Message ID: ${data.messageId}${replyHint}${unreadSection}`
|
|
678
|
+
}
|
|
679
|
+
]
|
|
449
680
|
};
|
|
450
|
-
}
|
|
451
|
-
const stat = fs.statSync(file_path);
|
|
452
|
-
if (stat.size > 10 * 1024 * 1024) {
|
|
681
|
+
} catch (err) {
|
|
453
682
|
return {
|
|
454
683
|
isError: true,
|
|
455
|
-
content: [{ type: "text", text: `Error:
|
|
684
|
+
content: [{ type: "text", text: `Error: ${err.message}` }]
|
|
456
685
|
};
|
|
457
686
|
}
|
|
458
|
-
|
|
459
|
-
|
|
460
|
-
|
|
461
|
-
|
|
462
|
-
|
|
463
|
-
|
|
464
|
-
|
|
465
|
-
|
|
466
|
-
|
|
467
|
-
|
|
468
|
-
|
|
687
|
+
}
|
|
688
|
+
);
|
|
689
|
+
server.tool(
|
|
690
|
+
"upload_file",
|
|
691
|
+
"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.",
|
|
692
|
+
{
|
|
693
|
+
file_path: z2.string().describe("Absolute path to the file on your local filesystem"),
|
|
694
|
+
channel: z2.string().describe("The channel target where this file will be used (e.g. '#general', 'dm:@richard')")
|
|
695
|
+
},
|
|
696
|
+
async ({ file_path, channel }) => {
|
|
697
|
+
try {
|
|
698
|
+
const fs = await import("fs");
|
|
699
|
+
const path = await import("path");
|
|
700
|
+
if (!fs.existsSync(file_path)) {
|
|
701
|
+
return {
|
|
702
|
+
isError: true,
|
|
703
|
+
content: [{ type: "text", text: `Error: File not found: ${file_path}` }]
|
|
704
|
+
};
|
|
705
|
+
}
|
|
706
|
+
const stat = fs.statSync(file_path);
|
|
707
|
+
if (stat.size > 10 * 1024 * 1024) {
|
|
708
|
+
return {
|
|
709
|
+
isError: true,
|
|
710
|
+
content: [{ type: "text", text: `Error: File too large (${(stat.size / 1024 / 1024).toFixed(1)}MB). Max 10MB per file.` }]
|
|
711
|
+
};
|
|
712
|
+
}
|
|
713
|
+
const { response: listRes, data: listData } = await executeJsonRequest(
|
|
714
|
+
`${serverUrl}/internal/agent/${agentId}/resolve-channel`,
|
|
715
|
+
{
|
|
716
|
+
method: "POST",
|
|
717
|
+
headers: commonHeaders,
|
|
718
|
+
body: JSON.stringify({ target: channel })
|
|
719
|
+
},
|
|
720
|
+
{
|
|
721
|
+
toolName: "upload_file.resolve_channel",
|
|
722
|
+
target: channel,
|
|
723
|
+
fetchImpl: bridgeFetch
|
|
724
|
+
}
|
|
725
|
+
);
|
|
726
|
+
if (!listRes.ok || !listData.channelId) {
|
|
727
|
+
return {
|
|
728
|
+
isError: true,
|
|
729
|
+
content: [{ type: "text", text: `Error: ${listData.error || `Could not resolve channel: ${channel}`}` }]
|
|
730
|
+
};
|
|
731
|
+
}
|
|
732
|
+
const channelId = listData.channelId;
|
|
733
|
+
const fileBuffer = fs.readFileSync(file_path);
|
|
734
|
+
const filename = path.basename(file_path);
|
|
735
|
+
const mimeType = guessMimeTypeFromFilename(filename);
|
|
736
|
+
const blob = new Blob([fileBuffer], { type: mimeType });
|
|
737
|
+
const formData = new FormData();
|
|
738
|
+
formData.append("file", blob, filename);
|
|
739
|
+
formData.append("channelId", channelId);
|
|
740
|
+
const uploadHeaders = buildChatBridgeCommonHeaders(authToken, { includeContentType: false });
|
|
741
|
+
const { response: res, data } = await executeJsonRequest(
|
|
742
|
+
`${serverUrl}/internal/agent/${agentId}/upload`,
|
|
743
|
+
{
|
|
744
|
+
method: "POST",
|
|
745
|
+
headers: uploadHeaders,
|
|
746
|
+
body: formData
|
|
747
|
+
},
|
|
748
|
+
{
|
|
749
|
+
toolName: "upload_file",
|
|
750
|
+
target: channel,
|
|
751
|
+
fetchImpl: bridgeFetch
|
|
752
|
+
}
|
|
753
|
+
);
|
|
754
|
+
if (!res.ok) {
|
|
755
|
+
return {
|
|
756
|
+
isError: true,
|
|
757
|
+
content: [{ type: "text", text: `Error: ${data.error}` }]
|
|
758
|
+
};
|
|
469
759
|
}
|
|
470
|
-
);
|
|
471
|
-
if (!listRes.ok || !listData.channelId) {
|
|
472
760
|
return {
|
|
473
|
-
|
|
474
|
-
|
|
761
|
+
content: [
|
|
762
|
+
{
|
|
763
|
+
type: "text",
|
|
764
|
+
text: `File uploaded: ${data.filename} (${(data.sizeBytes / 1024).toFixed(1)}KB)
|
|
765
|
+
Attachment ID: ${data.id}
|
|
766
|
+
|
|
767
|
+
Use this ID in send_message's attachment_ids parameter to include it in a message.`
|
|
768
|
+
}
|
|
769
|
+
]
|
|
475
770
|
};
|
|
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) {
|
|
771
|
+
} catch (err) {
|
|
500
772
|
return {
|
|
501
773
|
isError: true,
|
|
502
|
-
content: [{ type: "text", text: `Error: ${
|
|
774
|
+
content: [{ type: "text", text: `Error: ${err.message}` }]
|
|
503
775
|
};
|
|
504
776
|
}
|
|
505
|
-
|
|
506
|
-
|
|
777
|
+
}
|
|
778
|
+
);
|
|
779
|
+
server.tool(
|
|
780
|
+
"view_file",
|
|
781
|
+
"Download an attached file by its attachment ID and save it locally so you can inspect it. Returns the local file path.",
|
|
782
|
+
{
|
|
783
|
+
attachment_id: z2.string().describe("The attachment UUID (from the 'id:...' shown in the message)")
|
|
784
|
+
},
|
|
785
|
+
async ({ attachment_id }) => {
|
|
786
|
+
try {
|
|
787
|
+
const fs = await import("fs");
|
|
788
|
+
const path = await import("path");
|
|
789
|
+
const os = await import("os");
|
|
790
|
+
const cacheDir = path.join(os.homedir(), ".slock", "attachments");
|
|
791
|
+
fs.mkdirSync(cacheDir, { recursive: true });
|
|
792
|
+
const existing = fs.readdirSync(cacheDir).find((f) => f.startsWith(attachment_id));
|
|
793
|
+
if (existing) {
|
|
794
|
+
const cachedPath = path.join(cacheDir, existing);
|
|
795
|
+
return {
|
|
796
|
+
content: [{ type: "text", text: `File already cached at: ${cachedPath}` }]
|
|
797
|
+
};
|
|
798
|
+
}
|
|
799
|
+
const downloadHeaders = buildChatBridgeCommonHeaders(authToken, { includeContentType: false });
|
|
800
|
+
const { response: res } = await executeResponseRequest(
|
|
801
|
+
`${serverUrl}/api/attachments/${attachment_id}`,
|
|
507
802
|
{
|
|
508
|
-
|
|
509
|
-
|
|
510
|
-
|
|
511
|
-
|
|
512
|
-
|
|
803
|
+
headers: downloadHeaders,
|
|
804
|
+
redirect: "follow"
|
|
805
|
+
},
|
|
806
|
+
{
|
|
807
|
+
toolName: "view_file",
|
|
808
|
+
target: attachment_id,
|
|
809
|
+
fetchImpl: bridgeFetch
|
|
513
810
|
}
|
|
514
|
-
|
|
515
|
-
|
|
516
|
-
|
|
517
|
-
|
|
518
|
-
|
|
519
|
-
|
|
520
|
-
|
|
521
|
-
|
|
522
|
-
|
|
523
|
-
);
|
|
524
|
-
|
|
525
|
-
|
|
526
|
-
|
|
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);
|
|
811
|
+
);
|
|
812
|
+
if (!res.ok) {
|
|
813
|
+
return {
|
|
814
|
+
isError: true,
|
|
815
|
+
content: [{ type: "text", text: `Error: Failed to download attachment (${res.status})` }]
|
|
816
|
+
};
|
|
817
|
+
}
|
|
818
|
+
const contentType = res.headers.get("content-type") || "application/octet-stream";
|
|
819
|
+
const filename = parseFilenameFromContentDisposition(res.headers.get("content-disposition"));
|
|
820
|
+
const ext = filename ? path.extname(filename) || extensionForContentType(contentType) : extensionForContentType(contentType);
|
|
821
|
+
const filePath = path.join(cacheDir, `${attachment_id}${ext}`);
|
|
822
|
+
const buffer = Buffer.from(await res.arrayBuffer());
|
|
823
|
+
fs.writeFileSync(filePath, buffer);
|
|
540
824
|
return {
|
|
541
|
-
content: [{ type: "text", text: `
|
|
825
|
+
content: [{ type: "text", text: `Downloaded to: ${filePath}` }]
|
|
542
826
|
};
|
|
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) {
|
|
827
|
+
} catch (err) {
|
|
558
828
|
return {
|
|
559
829
|
isError: true,
|
|
560
|
-
content: [{ type: "text", text: `Error:
|
|
830
|
+
content: [{ type: "text", text: `Error: ${err.message}` }]
|
|
561
831
|
};
|
|
562
832
|
}
|
|
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
833
|
}
|
|
578
|
-
|
|
579
|
-
|
|
580
|
-
|
|
581
|
-
|
|
582
|
-
|
|
583
|
-
|
|
584
|
-
|
|
585
|
-
|
|
586
|
-
|
|
587
|
-
|
|
588
|
-
|
|
589
|
-
|
|
590
|
-
|
|
591
|
-
|
|
592
|
-
|
|
834
|
+
);
|
|
835
|
+
server.tool(
|
|
836
|
+
"check_messages",
|
|
837
|
+
"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.",
|
|
838
|
+
{},
|
|
839
|
+
async () => {
|
|
840
|
+
try {
|
|
841
|
+
const { response: res, data } = await executeJsonRequest(
|
|
842
|
+
`${serverUrl}/internal/agent/${agentId}/receive`,
|
|
843
|
+
{ method: "GET", headers: commonHeaders },
|
|
844
|
+
{
|
|
845
|
+
toolName: "check_messages",
|
|
846
|
+
timeoutMs: 1e4,
|
|
847
|
+
fetchImpl: bridgeFetch
|
|
848
|
+
}
|
|
849
|
+
);
|
|
850
|
+
if (!res.ok) {
|
|
851
|
+
return { isError: true, content: [{ type: "text", text: `Error: ${data.error || res.statusText}` }] };
|
|
593
852
|
}
|
|
594
|
-
|
|
595
|
-
|
|
596
|
-
|
|
597
|
-
|
|
598
|
-
|
|
599
|
-
|
|
600
|
-
|
|
601
|
-
const messagesToShow = rememberDeliveredMessages(messages);
|
|
602
|
-
if (messagesToShow.length > 0) {
|
|
603
|
-
return { content: [{ type: "text", text: formatMessages(messagesToShow) }] };
|
|
853
|
+
const messages = data.messages ?? [];
|
|
854
|
+
if (messages.length > 0) {
|
|
855
|
+
await acknowledgeReceivedMessages(messages);
|
|
856
|
+
const messagesToShow = rememberDeliveredMessages(messages);
|
|
857
|
+
if (messagesToShow.length > 0) {
|
|
858
|
+
return { content: [{ type: "text", text: formatMessages(messagesToShow) }] };
|
|
859
|
+
}
|
|
604
860
|
}
|
|
861
|
+
return {
|
|
862
|
+
content: [{ type: "text", text: "No new messages." }]
|
|
863
|
+
};
|
|
864
|
+
} catch (err) {
|
|
865
|
+
return {
|
|
866
|
+
isError: true,
|
|
867
|
+
content: [{ type: "text", text: `Error: ${err.message}` }]
|
|
868
|
+
};
|
|
605
869
|
}
|
|
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
870
|
}
|
|
615
|
-
|
|
616
|
-
|
|
617
|
-
|
|
618
|
-
|
|
619
|
-
|
|
620
|
-
|
|
621
|
-
|
|
622
|
-
|
|
623
|
-
|
|
624
|
-
|
|
625
|
-
|
|
626
|
-
|
|
627
|
-
|
|
628
|
-
}
|
|
629
|
-
|
|
630
|
-
|
|
631
|
-
|
|
632
|
-
|
|
633
|
-
|
|
634
|
-
|
|
635
|
-
|
|
636
|
-
|
|
637
|
-
|
|
638
|
-
|
|
639
|
-
|
|
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}
|
|
871
|
+
);
|
|
872
|
+
server.tool(
|
|
873
|
+
"list_server",
|
|
874
|
+
"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.",
|
|
875
|
+
{},
|
|
876
|
+
async () => {
|
|
877
|
+
try {
|
|
878
|
+
const { response: res, data } = await executeJsonRequest(
|
|
879
|
+
`${serverUrl}/internal/agent/${agentId}/server`,
|
|
880
|
+
{ method: "GET", headers: commonHeaders },
|
|
881
|
+
{
|
|
882
|
+
toolName: "list_server",
|
|
883
|
+
fetchImpl: bridgeFetch
|
|
884
|
+
}
|
|
885
|
+
);
|
|
886
|
+
let text = "## Server\n\n";
|
|
887
|
+
const channels = data.channels ?? [];
|
|
888
|
+
const agents = data.agents ?? [];
|
|
889
|
+
const humans = data.humans ?? [];
|
|
890
|
+
text += "### Channels\n";
|
|
891
|
+
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. To leave a regular channel you have joined, use `leave_channel(target="#name")`.\n';
|
|
892
|
+
if (channels.length > 0) {
|
|
893
|
+
for (const t of channels) {
|
|
894
|
+
const status = t.joined ? "joined" : "not joined";
|
|
895
|
+
text += t.description ? ` - #${t.name} [${status}] \u2014 ${t.description}
|
|
653
896
|
` : ` - #${t.name} [${status}]
|
|
654
897
|
`;
|
|
898
|
+
}
|
|
899
|
+
} else {
|
|
900
|
+
text += " (none)\n";
|
|
655
901
|
}
|
|
656
|
-
|
|
657
|
-
text += "
|
|
658
|
-
|
|
659
|
-
|
|
660
|
-
|
|
661
|
-
if (agents.length > 0) {
|
|
662
|
-
for (const a of agents) {
|
|
663
|
-
text += a.description ? ` - @${a.name} (${a.status}) \u2014 ${a.description}
|
|
902
|
+
text += "\n### Agents\n";
|
|
903
|
+
text += "Other AI agents in this server.\n";
|
|
904
|
+
if (agents.length > 0) {
|
|
905
|
+
for (const a of agents) {
|
|
906
|
+
text += a.description ? ` - @${a.name} (${a.status}) \u2014 ${a.description}
|
|
664
907
|
` : ` - @${a.name} (${a.status})
|
|
665
908
|
`;
|
|
909
|
+
}
|
|
910
|
+
} else {
|
|
911
|
+
text += " (none)\n";
|
|
666
912
|
}
|
|
667
|
-
|
|
668
|
-
text += "
|
|
669
|
-
|
|
670
|
-
|
|
671
|
-
|
|
672
|
-
if (humans.length > 0) {
|
|
673
|
-
for (const u of humans) {
|
|
674
|
-
text += u.description ? ` - @${u.name} \u2014 ${u.description}
|
|
913
|
+
text += "\n### Humans\n";
|
|
914
|
+
text += 'To start a new DM: send_message(target="dm:@name"). To reply in an existing DM: reuse the target from received messages.\n';
|
|
915
|
+
if (humans.length > 0) {
|
|
916
|
+
for (const u of humans) {
|
|
917
|
+
text += u.description ? ` - @${u.name} \u2014 ${u.description}
|
|
675
918
|
` : ` - @${u.name}
|
|
676
919
|
`;
|
|
920
|
+
}
|
|
921
|
+
} else {
|
|
922
|
+
text += " (none)\n";
|
|
677
923
|
}
|
|
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
924
|
return {
|
|
708
|
-
content: [{ type: "text", text
|
|
925
|
+
content: [{ type: "text", text }]
|
|
926
|
+
};
|
|
927
|
+
} catch (err) {
|
|
928
|
+
return {
|
|
929
|
+
isError: true,
|
|
930
|
+
content: [{ type: "text", text: `Error: ${err.message}` }]
|
|
709
931
|
};
|
|
710
932
|
}
|
|
711
|
-
|
|
712
|
-
|
|
713
|
-
|
|
714
|
-
|
|
715
|
-
|
|
716
|
-
|
|
717
|
-
|
|
718
|
-
|
|
719
|
-
|
|
720
|
-
|
|
721
|
-
|
|
722
|
-
|
|
723
|
-
|
|
724
|
-
|
|
933
|
+
}
|
|
934
|
+
);
|
|
935
|
+
server.tool(
|
|
936
|
+
"leave_channel",
|
|
937
|
+
"Leave a regular channel you have joined. This only affects your own agent membership; it does not require admin privileges. After leaving, you can still inspect visible public channel history, but you will stop receiving ordinary channel delivery and cannot send until a human adds you again.",
|
|
938
|
+
{
|
|
939
|
+
target: z2.string().describe("Regular channel to leave, in the form '#channel-name'. DMs and thread targets are not supported.")
|
|
940
|
+
},
|
|
941
|
+
async ({ target }) => {
|
|
942
|
+
try {
|
|
943
|
+
const channel = await resolveRegularChannelTarget(target);
|
|
944
|
+
if (!channel.joined) {
|
|
945
|
+
return {
|
|
946
|
+
content: [{ type: "text", text: `Already not joined in ${target}.` }]
|
|
947
|
+
};
|
|
948
|
+
}
|
|
949
|
+
const { response: res, data } = await executeJsonRequest(
|
|
950
|
+
`${serverUrl}/internal/agent/${agentId}/channels/${channel.id}/leave`,
|
|
951
|
+
{
|
|
952
|
+
method: "POST",
|
|
953
|
+
headers: commonHeaders
|
|
954
|
+
},
|
|
955
|
+
{
|
|
956
|
+
toolName: "leave_channel",
|
|
957
|
+
target,
|
|
958
|
+
fetchImpl: bridgeFetch
|
|
959
|
+
}
|
|
960
|
+
);
|
|
961
|
+
if (!res.ok) {
|
|
962
|
+
return {
|
|
963
|
+
isError: true,
|
|
964
|
+
content: [{ type: "text", text: `Error: ${data.error || `Failed to leave ${target}`}` }]
|
|
965
|
+
};
|
|
725
966
|
}
|
|
726
|
-
);
|
|
727
|
-
if (!res.ok) {
|
|
728
967
|
return {
|
|
729
|
-
content: [{ type: "text", text: `
|
|
968
|
+
content: [{ type: "text", text: `Left ${target}. You can still inspect visible public channel history there, but you can no longer send or receive ordinary channel delivery until a human adds you again.` }]
|
|
730
969
|
};
|
|
731
|
-
}
|
|
732
|
-
if (!data.results || data.results.length === 0) {
|
|
970
|
+
} catch (err) {
|
|
733
971
|
return {
|
|
734
|
-
|
|
972
|
+
isError: true,
|
|
973
|
+
content: [{ type: "text", text: `Error: ${err.message}` }]
|
|
735
974
|
};
|
|
736
975
|
}
|
|
737
|
-
|
|
738
|
-
|
|
739
|
-
|
|
976
|
+
}
|
|
977
|
+
);
|
|
978
|
+
server.tool(
|
|
979
|
+
"search_messages",
|
|
980
|
+
"Search messages visible to the agent. Use this to find relevant conversations, then inspect a hit with read_history(channel=..., around=messageId).",
|
|
981
|
+
{
|
|
982
|
+
query: z2.string().describe("Search query"),
|
|
983
|
+
channel: z2.string().optional().describe("Optional target to scope the search, e.g. '#general', 'dm:@richard', '#general:abcd1234'"),
|
|
984
|
+
sender_id: z2.string().optional().describe("Optional exact sender id filter."),
|
|
985
|
+
after: z2.string().optional().describe("Optional inclusive ISO datetime lower bound for message created_at."),
|
|
986
|
+
before: z2.string().optional().describe("Optional inclusive ISO datetime upper bound for message created_at."),
|
|
987
|
+
limit: z2.number().default(10).describe("Max number of search results to return (default 10, max 20)")
|
|
988
|
+
},
|
|
989
|
+
async ({ query, channel, sender_id, after, before, limit }) => {
|
|
990
|
+
try {
|
|
991
|
+
const trimmed = query.trim();
|
|
992
|
+
if (!trimmed) {
|
|
993
|
+
return {
|
|
994
|
+
content: [{ type: "text", text: "Search query cannot be empty." }]
|
|
995
|
+
};
|
|
996
|
+
}
|
|
997
|
+
const params = new URLSearchParams();
|
|
998
|
+
params.set("q", trimmed);
|
|
999
|
+
params.set("limit", String(Math.min(limit, 20)));
|
|
1000
|
+
if (channel) params.set("channel", channel);
|
|
1001
|
+
if (sender_id) params.set("senderId", sender_id);
|
|
1002
|
+
if (after) params.set("after", after);
|
|
1003
|
+
if (before) params.set("before", before);
|
|
1004
|
+
const { response: res, data } = await executeJsonRequest(
|
|
1005
|
+
`${serverUrl}/internal/agent/${agentId}/search?${params}`,
|
|
1006
|
+
{ method: "GET", headers: commonHeaders },
|
|
1007
|
+
{
|
|
1008
|
+
toolName: "search_messages",
|
|
1009
|
+
target: channel ?? null,
|
|
1010
|
+
fetchImpl: bridgeFetch
|
|
1011
|
+
}
|
|
1012
|
+
);
|
|
1013
|
+
if (!res.ok) {
|
|
1014
|
+
return {
|
|
1015
|
+
content: [{ type: "text", text: `Error: ${data.error}` }]
|
|
1016
|
+
};
|
|
1017
|
+
}
|
|
1018
|
+
if (!data.results || data.results.length === 0) {
|
|
1019
|
+
return {
|
|
1020
|
+
content: [{ type: "text", text: "No search results." }]
|
|
1021
|
+
};
|
|
1022
|
+
}
|
|
1023
|
+
const formatted = data.results.map((result, index) => {
|
|
1024
|
+
const target = formatSearchTarget(result);
|
|
1025
|
+
const threadInfo = result.channelType === "thread" ? `
|
|
740
1026
|
thread: ${result.parentChannelName} -> ${target}` : "";
|
|
741
|
-
|
|
742
|
-
|
|
743
|
-
|
|
744
|
-
|
|
745
|
-
|
|
746
|
-
|
|
747
|
-
|
|
748
|
-
|
|
749
|
-
|
|
750
|
-
|
|
751
|
-
|
|
752
|
-
|
|
753
|
-
|
|
1027
|
+
return [
|
|
1028
|
+
`[${index + 1}] msg=${result.id} seq=${result.seq} time=${toLocalTime(result.createdAt)}`,
|
|
1029
|
+
`target: ${target}${threadInfo}`,
|
|
1030
|
+
`sender: @${result.senderName} (${result.senderType})`,
|
|
1031
|
+
`content: ${result.content}`,
|
|
1032
|
+
`match: ${result.snippet}`,
|
|
1033
|
+
`next: read_history(channel="${target}", around="${result.id}", limit=20)`
|
|
1034
|
+
].join("\n");
|
|
1035
|
+
}).join("\n\n");
|
|
1036
|
+
return {
|
|
1037
|
+
content: [{
|
|
1038
|
+
type: "text",
|
|
1039
|
+
text: `## Search Results for "${trimmed}" (${data.results.length} results)
|
|
754
1040
|
|
|
755
1041
|
${formatted}`
|
|
756
|
-
|
|
757
|
-
};
|
|
758
|
-
} catch (err) {
|
|
759
|
-
return {
|
|
760
|
-
isError: true,
|
|
761
|
-
content: [{ type: "text", text: `Error: ${err.message}` }]
|
|
762
|
-
};
|
|
763
|
-
}
|
|
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
|
|
791
|
-
}
|
|
792
|
-
);
|
|
793
|
-
if (!res.ok) {
|
|
794
|
-
return {
|
|
795
|
-
content: [
|
|
796
|
-
{ type: "text", text: `Error: ${data.error}` }
|
|
797
|
-
]
|
|
1042
|
+
}]
|
|
798
1043
|
};
|
|
799
|
-
}
|
|
800
|
-
if (!data.messages || data.messages.length === 0) {
|
|
1044
|
+
} catch (err) {
|
|
801
1045
|
return {
|
|
802
|
-
|
|
803
|
-
|
|
804
|
-
]
|
|
1046
|
+
isError: true,
|
|
1047
|
+
content: [{ type: "text", text: `Error: ${err.message}` }]
|
|
805
1048
|
};
|
|
806
1049
|
}
|
|
807
|
-
|
|
808
|
-
|
|
809
|
-
|
|
810
|
-
|
|
811
|
-
|
|
812
|
-
|
|
813
|
-
|
|
814
|
-
|
|
1050
|
+
}
|
|
1051
|
+
);
|
|
1052
|
+
server.tool(
|
|
1053
|
+
"read_history",
|
|
1054
|
+
"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).",
|
|
1055
|
+
{
|
|
1056
|
+
channel: z2.string().describe("The target to read history from \u2014 e.g. '#general', 'dm:@richard', '#general:abcd1234', 'dm:@richard:abcd1234'"),
|
|
1057
|
+
limit: z2.number().default(50).describe("Max number of messages to return (default 50, max 100)"),
|
|
1058
|
+
around: z2.union([z2.string(), z2.number()]).optional().describe("Center the result window around a messageId or seq in this channel/thread."),
|
|
1059
|
+
before: z2.number().optional().describe("Return messages before this seq number (for backward pagination). Omit for latest messages."),
|
|
1060
|
+
after: z2.number().optional().describe("Return messages after this seq number (for catching up on unread). Returns oldest-first.")
|
|
1061
|
+
},
|
|
1062
|
+
async ({ channel, limit, around, before, after }) => {
|
|
1063
|
+
try {
|
|
1064
|
+
const params = new URLSearchParams();
|
|
1065
|
+
params.set("channel", channel);
|
|
1066
|
+
params.set("limit", String(Math.min(limit, 100)));
|
|
1067
|
+
if (around !== void 0) params.set("around", String(around));
|
|
1068
|
+
if (before) params.set("before", String(before));
|
|
1069
|
+
if (after) params.set("after", String(after));
|
|
1070
|
+
const { response: res, data } = await executeJsonRequest(
|
|
1071
|
+
`${serverUrl}/internal/agent/${agentId}/history?${params}`,
|
|
1072
|
+
{ method: "GET", headers: commonHeaders },
|
|
1073
|
+
{
|
|
1074
|
+
toolName: "read_history",
|
|
1075
|
+
target: channel,
|
|
1076
|
+
fetchImpl: bridgeFetch
|
|
1077
|
+
}
|
|
1078
|
+
);
|
|
1079
|
+
if (!res.ok) {
|
|
1080
|
+
return {
|
|
1081
|
+
content: [
|
|
1082
|
+
{ type: "text", text: `Error: ${data.error}` }
|
|
1083
|
+
]
|
|
1084
|
+
};
|
|
1085
|
+
}
|
|
1086
|
+
if (!data.messages || data.messages.length === 0) {
|
|
1087
|
+
return {
|
|
1088
|
+
content: [
|
|
1089
|
+
{ type: "text", text: "No messages in this channel." }
|
|
1090
|
+
]
|
|
1091
|
+
};
|
|
1092
|
+
}
|
|
1093
|
+
const formatted = data.messages.map((m) => formatHistoryMessageLine({
|
|
1094
|
+
...m,
|
|
1095
|
+
senderName: m.senderName ?? m.sender_name ?? "unknown",
|
|
1096
|
+
senderDescription: m.senderDescription ?? m.sender_description ?? null
|
|
1097
|
+
})).join("\n");
|
|
1098
|
+
let footer = "";
|
|
1099
|
+
if (data.historyLimited) {
|
|
1100
|
+
footer = `
|
|
815
1101
|
|
|
816
1102
|
--- ${data.historyLimitMessage || "Message history is limited on this plan."} ---`;
|
|
817
|
-
|
|
818
|
-
|
|
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) {
|
|
1103
|
+
} else if (around && data.messages.length > 0 && (data.has_older || data.has_newer)) {
|
|
1104
|
+
const minSeq = data.messages[0].seq;
|
|
825
1105
|
const maxSeq = data.messages[data.messages.length - 1].seq;
|
|
826
1106
|
footer = `
|
|
827
1107
|
|
|
1108
|
+
--- Context window shown. Use before=${minSeq} to load older messages or after=${maxSeq} to load newer messages. ---`;
|
|
1109
|
+
} else if (data.has_more && data.messages.length > 0) {
|
|
1110
|
+
if (after) {
|
|
1111
|
+
const maxSeq = data.messages[data.messages.length - 1].seq;
|
|
1112
|
+
footer = `
|
|
1113
|
+
|
|
828
1114
|
--- ${data.messages.length} messages shown. Use after=${maxSeq} to load more recent messages. ---`;
|
|
829
|
-
|
|
830
|
-
|
|
831
|
-
|
|
1115
|
+
} else {
|
|
1116
|
+
const minSeq = data.messages[0].seq;
|
|
1117
|
+
footer = `
|
|
832
1118
|
|
|
833
1119
|
--- ${data.messages.length} messages shown. Use before=${minSeq} to load older messages. ---`;
|
|
1120
|
+
}
|
|
834
1121
|
}
|
|
835
|
-
|
|
836
|
-
|
|
837
|
-
|
|
838
|
-
header += `
|
|
1122
|
+
let header = `## Message History for ${channel}${around ? ` around ${around}` : ""} (${data.messages.length} messages)`;
|
|
1123
|
+
if ((data.last_read_seq ?? 0) > 0 && !after && !before && !around) {
|
|
1124
|
+
header += `
|
|
839
1125
|
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
1126
|
}
|
|
879
|
-
);
|
|
880
|
-
if (!res.ok) {
|
|
881
1127
|
return {
|
|
882
|
-
|
|
883
|
-
|
|
1128
|
+
content: [
|
|
1129
|
+
{
|
|
1130
|
+
type: "text",
|
|
1131
|
+
text: `${header}
|
|
1132
|
+
|
|
1133
|
+
${formatted}${footer}`
|
|
1134
|
+
}
|
|
1135
|
+
]
|
|
884
1136
|
};
|
|
885
|
-
}
|
|
886
|
-
if (!data.tasks || data.tasks.length === 0) {
|
|
1137
|
+
} catch (err) {
|
|
887
1138
|
return {
|
|
888
|
-
|
|
1139
|
+
isError: true,
|
|
1140
|
+
content: [{ type: "text", text: `Error: ${err.message}` }]
|
|
889
1141
|
};
|
|
890
1142
|
}
|
|
891
|
-
|
|
892
|
-
|
|
893
|
-
|
|
894
|
-
|
|
895
|
-
|
|
896
|
-
|
|
897
|
-
|
|
898
|
-
|
|
899
|
-
|
|
1143
|
+
}
|
|
1144
|
+
);
|
|
1145
|
+
server.tool(
|
|
1146
|
+
"list_tasks",
|
|
1147
|
+
"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.",
|
|
1148
|
+
{
|
|
1149
|
+
channel: z2.string().describe("The channel whose task board to view \u2014 e.g. '#engineering', '#proj-slock'"),
|
|
1150
|
+
status: z2.enum(["all", "todo", "in_progress", "in_review", "done"]).default("all").describe("Filter by status (default: all)")
|
|
1151
|
+
},
|
|
1152
|
+
async ({ channel, status }) => {
|
|
1153
|
+
try {
|
|
1154
|
+
const params = new URLSearchParams();
|
|
1155
|
+
params.set("channel", channel);
|
|
1156
|
+
if (status !== "all") params.set("status", status);
|
|
1157
|
+
const { response: res, data } = await executeJsonRequest(
|
|
1158
|
+
`${serverUrl}/internal/agent/${agentId}/tasks?${params}`,
|
|
1159
|
+
{ method: "GET", headers: commonHeaders },
|
|
900
1160
|
{
|
|
901
|
-
|
|
902
|
-
|
|
903
|
-
|
|
904
|
-
${formatted}`
|
|
1161
|
+
toolName: "list_tasks",
|
|
1162
|
+
target: channel,
|
|
1163
|
+
fetchImpl: bridgeFetch
|
|
905
1164
|
}
|
|
906
|
-
|
|
907
|
-
|
|
908
|
-
|
|
909
|
-
|
|
910
|
-
|
|
911
|
-
|
|
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
|
|
1165
|
+
);
|
|
1166
|
+
if (!res.ok) {
|
|
1167
|
+
return {
|
|
1168
|
+
isError: true,
|
|
1169
|
+
content: [{ type: "text", text: `Error: ${data.error}` }]
|
|
1170
|
+
};
|
|
940
1171
|
}
|
|
941
|
-
|
|
942
|
-
|
|
1172
|
+
if (!data.tasks || data.tasks.length === 0) {
|
|
1173
|
+
return {
|
|
1174
|
+
content: [{ type: "text", text: `No${status !== "all" ? ` ${status}` : ""} tasks in ${channel}.` }]
|
|
1175
|
+
};
|
|
1176
|
+
}
|
|
1177
|
+
const formatted = data.tasks.map((t) => {
|
|
1178
|
+
const assignee = t.claimedByName ? ` \u2192 @${t.claimedByName}` : "";
|
|
1179
|
+
const creator = t.createdByName ? ` (by @${t.createdByName})` : "";
|
|
1180
|
+
const msgId = t.messageId ? ` msg=${t.messageId.slice(0, 8)}` : "";
|
|
1181
|
+
const legacy = t.isLegacy ? " [LEGACY \u2014 read-only]" : "";
|
|
1182
|
+
return `#${t.taskNumber} [${t.status}] ${t.title}${assignee}${creator}${msgId}${legacy}`;
|
|
1183
|
+
}).join("\n");
|
|
1184
|
+
return {
|
|
1185
|
+
content: [
|
|
1186
|
+
{
|
|
1187
|
+
type: "text",
|
|
1188
|
+
text: `## Task Board for ${channel} (${data.tasks.length} tasks)
|
|
1189
|
+
|
|
1190
|
+
${formatted}`
|
|
1191
|
+
}
|
|
1192
|
+
]
|
|
1193
|
+
};
|
|
1194
|
+
} catch (err) {
|
|
943
1195
|
return {
|
|
944
1196
|
isError: true,
|
|
945
|
-
content: [{ type: "text", text: `Error: ${
|
|
1197
|
+
content: [{ type: "text", text: `Error: ${err.message}` }]
|
|
946
1198
|
};
|
|
947
1199
|
}
|
|
948
|
-
|
|
949
|
-
|
|
950
|
-
|
|
951
|
-
|
|
1200
|
+
}
|
|
1201
|
+
);
|
|
1202
|
+
server.tool(
|
|
1203
|
+
"create_tasks",
|
|
1204
|
+
"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.",
|
|
1205
|
+
{
|
|
1206
|
+
channel: z2.string().describe("The channel to create tasks in \u2014 e.g. '#engineering'"),
|
|
1207
|
+
tasks: z2.array(
|
|
1208
|
+
z2.object({
|
|
1209
|
+
title: z2.string().describe("Task title")
|
|
1210
|
+
})
|
|
1211
|
+
).describe("Array of tasks to create")
|
|
1212
|
+
},
|
|
1213
|
+
async ({ channel, tasks }) => {
|
|
1214
|
+
try {
|
|
1215
|
+
const { response: res, data } = await executeJsonRequest(
|
|
1216
|
+
`${serverUrl}/internal/agent/${agentId}/tasks`,
|
|
952
1217
|
{
|
|
953
|
-
|
|
954
|
-
|
|
1218
|
+
method: "POST",
|
|
1219
|
+
headers: commonHeaders,
|
|
1220
|
+
body: JSON.stringify({ channel, tasks })
|
|
1221
|
+
},
|
|
1222
|
+
{
|
|
1223
|
+
toolName: "create_tasks",
|
|
1224
|
+
target: channel,
|
|
1225
|
+
fetchImpl: bridgeFetch
|
|
1226
|
+
}
|
|
1227
|
+
);
|
|
1228
|
+
if (!res.ok) {
|
|
1229
|
+
return {
|
|
1230
|
+
isError: true,
|
|
1231
|
+
content: [{ type: "text", text: `Error: ${data.error}` }]
|
|
1232
|
+
};
|
|
1233
|
+
}
|
|
1234
|
+
const created = data.tasks.map((t) => `#${t.taskNumber} msg=${t.messageId.slice(0, 8)} "${t.title}"`).join("\n");
|
|
1235
|
+
const threadHints = data.tasks.map((t) => `#${t.taskNumber} \u2192 send_message to "${channel}:${t.messageId.slice(0, 8)}"`).join("\n");
|
|
1236
|
+
return {
|
|
1237
|
+
content: [
|
|
1238
|
+
{
|
|
1239
|
+
type: "text",
|
|
1240
|
+
text: `Created ${data.tasks.length} task(s) in ${channel}:
|
|
955
1241
|
${created}
|
|
956
1242
|
|
|
957
1243
|
To follow up in each task's thread:
|
|
958
1244
|
${threadHints}`
|
|
959
|
-
|
|
960
|
-
|
|
961
|
-
|
|
962
|
-
|
|
963
|
-
|
|
964
|
-
|
|
965
|
-
|
|
966
|
-
|
|
1245
|
+
}
|
|
1246
|
+
]
|
|
1247
|
+
};
|
|
1248
|
+
} catch (err) {
|
|
1249
|
+
return {
|
|
1250
|
+
isError: true,
|
|
1251
|
+
content: [{ type: "text", text: `Error: ${err.message}` }]
|
|
1252
|
+
};
|
|
1253
|
+
}
|
|
967
1254
|
}
|
|
968
|
-
|
|
969
|
-
|
|
970
|
-
|
|
971
|
-
|
|
972
|
-
`Claim tasks so you are assigned to work on them. Two modes:
|
|
1255
|
+
);
|
|
1256
|
+
server.tool(
|
|
1257
|
+
"claim_tasks",
|
|
1258
|
+
`Claim tasks so you are assigned to work on them. Two modes:
|
|
973
1259
|
1. By task number: claim existing tasks shown in list_tasks. Use task_numbers=[1, 3].
|
|
974
1260
|
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
1261
|
|
|
976
1262
|
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
|
-
|
|
979
|
-
|
|
980
|
-
|
|
981
|
-
|
|
982
|
-
|
|
983
|
-
|
|
984
|
-
|
|
1263
|
+
{
|
|
1264
|
+
channel: z2.string().describe("The channel \u2014 e.g. '#engineering'"),
|
|
1265
|
+
task_numbers: z2.array(z2.number()).optional().describe("Task numbers to claim (from list_tasks output, e.g. [1, 3])"),
|
|
1266
|
+
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.")
|
|
1267
|
+
},
|
|
1268
|
+
async ({ channel, task_numbers, message_ids }) => {
|
|
1269
|
+
try {
|
|
1270
|
+
if ((!task_numbers || task_numbers.length === 0) && (!message_ids || message_ids.length === 0)) {
|
|
1271
|
+
return {
|
|
1272
|
+
content: [{ type: "text", text: "Error: provide at least one of task_numbers or message_ids" }]
|
|
1273
|
+
};
|
|
1274
|
+
}
|
|
1275
|
+
const body = { channel };
|
|
1276
|
+
if (task_numbers && task_numbers.length > 0) body.task_numbers = task_numbers;
|
|
1277
|
+
if (message_ids && message_ids.length > 0) body.message_ids = message_ids;
|
|
1278
|
+
const { response: res, data } = await executeJsonRequest(
|
|
1279
|
+
`${serverUrl}/internal/agent/${agentId}/tasks/claim`,
|
|
1280
|
+
{
|
|
1281
|
+
method: "POST",
|
|
1282
|
+
headers: commonHeaders,
|
|
1283
|
+
body: JSON.stringify(body)
|
|
1284
|
+
},
|
|
1285
|
+
{
|
|
1286
|
+
toolName: "claim_tasks",
|
|
1287
|
+
target: channel,
|
|
1288
|
+
fetchImpl: bridgeFetch
|
|
1289
|
+
}
|
|
1290
|
+
);
|
|
1291
|
+
if (!res.ok) {
|
|
1292
|
+
return {
|
|
1293
|
+
isError: true,
|
|
1294
|
+
content: [{ type: "text", text: `Error: ${data.error}` }]
|
|
1295
|
+
};
|
|
1296
|
+
}
|
|
1297
|
+
const lines = data.results.map((r) => {
|
|
1298
|
+
const label = r.taskNumber ? `#${r.taskNumber}` : `msg:${r.messageId}`;
|
|
1299
|
+
if (r.success) {
|
|
1300
|
+
const msgShort = r.messageId ? r.messageId.slice(0, 8) : "";
|
|
1301
|
+
return `${label} (msg:${msgShort}): claimed`;
|
|
1302
|
+
}
|
|
1303
|
+
return `${label}: FAILED \u2014 ${r.reason || "already claimed"}`;
|
|
1304
|
+
});
|
|
1305
|
+
const succeeded = data.results.filter((r) => r.success).length;
|
|
1306
|
+
const failed = data.results.length - succeeded;
|
|
1307
|
+
let summary = `${succeeded} claimed`;
|
|
1308
|
+
if (failed > 0) summary += `, ${failed} failed`;
|
|
1309
|
+
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");
|
|
1310
|
+
const threadHint = claimedMsgs ? `
|
|
1311
|
+
|
|
1312
|
+
Follow up in each task's thread:
|
|
1313
|
+
${claimedMsgs}` : "";
|
|
985
1314
|
return {
|
|
986
|
-
content: [
|
|
1315
|
+
content: [
|
|
1316
|
+
{
|
|
1317
|
+
type: "text",
|
|
1318
|
+
text: `Claim results (${summary}):
|
|
1319
|
+
${lines.join("\n")}${threadHint}`
|
|
1320
|
+
}
|
|
1321
|
+
]
|
|
987
1322
|
};
|
|
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) {
|
|
1323
|
+
} catch (err) {
|
|
1006
1324
|
return {
|
|
1007
1325
|
isError: true,
|
|
1008
|
-
content: [{ type: "text", text: `Error: ${
|
|
1326
|
+
content: [{ type: "text", text: `Error: ${err.message}` }]
|
|
1009
1327
|
};
|
|
1010
1328
|
}
|
|
1011
|
-
|
|
1012
|
-
|
|
1013
|
-
|
|
1014
|
-
|
|
1015
|
-
|
|
1016
|
-
|
|
1017
|
-
|
|
1018
|
-
|
|
1019
|
-
|
|
1020
|
-
|
|
1021
|
-
|
|
1022
|
-
|
|
1023
|
-
|
|
1024
|
-
const threadHint = claimedMsgs ? `
|
|
1025
|
-
|
|
1026
|
-
Follow up in each task's thread:
|
|
1027
|
-
${claimedMsgs}` : "";
|
|
1028
|
-
return {
|
|
1029
|
-
content: [
|
|
1329
|
+
}
|
|
1330
|
+
);
|
|
1331
|
+
server.tool(
|
|
1332
|
+
"unclaim_task",
|
|
1333
|
+
"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.",
|
|
1334
|
+
{
|
|
1335
|
+
channel: z2.string().describe("The channel \u2014 e.g. '#engineering'"),
|
|
1336
|
+
task_number: z2.number().describe("The task number to unclaim (e.g. 3)")
|
|
1337
|
+
},
|
|
1338
|
+
async ({ channel, task_number }) => {
|
|
1339
|
+
try {
|
|
1340
|
+
const { response: res, data } = await executeJsonRequest(
|
|
1341
|
+
`${serverUrl}/internal/agent/${agentId}/tasks/unclaim`,
|
|
1030
1342
|
{
|
|
1031
|
-
|
|
1032
|
-
|
|
1033
|
-
|
|
1343
|
+
method: "POST",
|
|
1344
|
+
headers: commonHeaders,
|
|
1345
|
+
body: JSON.stringify({ channel, task_number })
|
|
1346
|
+
},
|
|
1347
|
+
{
|
|
1348
|
+
toolName: "unclaim_task",
|
|
1349
|
+
target: channel,
|
|
1350
|
+
fetchImpl: bridgeFetch
|
|
1034
1351
|
}
|
|
1035
|
-
|
|
1036
|
-
|
|
1037
|
-
|
|
1038
|
-
|
|
1039
|
-
|
|
1040
|
-
|
|
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
|
|
1352
|
+
);
|
|
1353
|
+
if (!res.ok) {
|
|
1354
|
+
return {
|
|
1355
|
+
isError: true,
|
|
1356
|
+
content: [{ type: "text", text: `Error: ${data.error}` }]
|
|
1357
|
+
};
|
|
1065
1358
|
}
|
|
1066
|
-
|
|
1067
|
-
|
|
1359
|
+
return {
|
|
1360
|
+
content: [
|
|
1361
|
+
{ type: "text", text: `#${task_number} unclaimed \u2014 now open.` }
|
|
1362
|
+
]
|
|
1363
|
+
};
|
|
1364
|
+
} catch (err) {
|
|
1068
1365
|
return {
|
|
1069
1366
|
isError: true,
|
|
1070
|
-
content: [{ type: "text", text: `Error: ${
|
|
1367
|
+
content: [{ type: "text", text: `Error: ${err.message}` }]
|
|
1071
1368
|
};
|
|
1072
1369
|
}
|
|
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
1370
|
}
|
|
1084
|
-
|
|
1085
|
-
|
|
1086
|
-
|
|
1087
|
-
|
|
1088
|
-
|
|
1089
|
-
|
|
1090
|
-
|
|
1091
|
-
|
|
1092
|
-
|
|
1093
|
-
|
|
1094
|
-
|
|
1095
|
-
|
|
1096
|
-
|
|
1097
|
-
|
|
1098
|
-
|
|
1099
|
-
|
|
1100
|
-
|
|
1101
|
-
|
|
1102
|
-
|
|
1103
|
-
|
|
1104
|
-
|
|
1105
|
-
|
|
1106
|
-
|
|
1371
|
+
);
|
|
1372
|
+
server.tool(
|
|
1373
|
+
"update_task_status",
|
|
1374
|
+
"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.",
|
|
1375
|
+
{
|
|
1376
|
+
channel: z2.string().describe("The channel \u2014 e.g. '#engineering'"),
|
|
1377
|
+
task_number: z2.number().describe("The task number to update (e.g. 3)"),
|
|
1378
|
+
status: z2.enum(["todo", "in_progress", "in_review", "done"]).describe("The new status")
|
|
1379
|
+
},
|
|
1380
|
+
async ({ channel, task_number, status }) => {
|
|
1381
|
+
try {
|
|
1382
|
+
const { response: res, data } = await executeJsonRequest(
|
|
1383
|
+
`${serverUrl}/internal/agent/${agentId}/tasks/update-status`,
|
|
1384
|
+
{
|
|
1385
|
+
method: "POST",
|
|
1386
|
+
headers: commonHeaders,
|
|
1387
|
+
body: JSON.stringify({ channel, task_number, status })
|
|
1388
|
+
},
|
|
1389
|
+
{
|
|
1390
|
+
toolName: "update_task_status",
|
|
1391
|
+
target: channel,
|
|
1392
|
+
fetchImpl: bridgeFetch
|
|
1393
|
+
}
|
|
1394
|
+
);
|
|
1395
|
+
if (!res.ok) {
|
|
1396
|
+
return {
|
|
1397
|
+
isError: true,
|
|
1398
|
+
content: [{ type: "text", text: `Error: ${data.error}` }]
|
|
1399
|
+
};
|
|
1107
1400
|
}
|
|
1108
|
-
|
|
1109
|
-
|
|
1401
|
+
return {
|
|
1402
|
+
content: [
|
|
1403
|
+
{ type: "text", text: `#${task_number} moved to ${status}.` }
|
|
1404
|
+
]
|
|
1405
|
+
};
|
|
1406
|
+
} catch (err) {
|
|
1110
1407
|
return {
|
|
1111
1408
|
isError: true,
|
|
1112
|
-
content: [{ type: "text", text: `Error: ${
|
|
1409
|
+
content: [{ type: "text", text: `Error: ${err.message}` }]
|
|
1113
1410
|
};
|
|
1114
1411
|
}
|
|
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
1412
|
}
|
|
1126
|
-
|
|
1127
|
-
|
|
1128
|
-
|
|
1129
|
-
|
|
1130
|
-
|
|
1131
|
-
|
|
1132
|
-
|
|
1133
|
-
|
|
1134
|
-
|
|
1135
|
-
|
|
1136
|
-
|
|
1137
|
-
|
|
1138
|
-
|
|
1139
|
-
|
|
1140
|
-
|
|
1141
|
-
|
|
1142
|
-
|
|
1143
|
-
|
|
1144
|
-
|
|
1145
|
-
|
|
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
|
|
1413
|
+
);
|
|
1414
|
+
server.tool(
|
|
1415
|
+
"schedule_reminder",
|
|
1416
|
+
"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.",
|
|
1417
|
+
{
|
|
1418
|
+
title: z2.string().describe("Short description of what the reminder is about. This is what you'll see when it fires."),
|
|
1419
|
+
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."),
|
|
1420
|
+
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."),
|
|
1421
|
+
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."),
|
|
1422
|
+
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."),
|
|
1423
|
+
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.")
|
|
1424
|
+
},
|
|
1425
|
+
async ({ title, delay_seconds, fire_at, repeat, channel, msg_id }) => {
|
|
1426
|
+
try {
|
|
1427
|
+
const body = { title, msgId: msg_id };
|
|
1428
|
+
if (delay_seconds !== void 0) body.delaySeconds = delay_seconds;
|
|
1429
|
+
if (fire_at !== void 0) body.fireAt = fire_at;
|
|
1430
|
+
if (repeat !== void 0) {
|
|
1431
|
+
body.repeat = repeat;
|
|
1432
|
+
body.tz = Intl.DateTimeFormat().resolvedOptions().timeZone;
|
|
1165
1433
|
}
|
|
1166
|
-
|
|
1167
|
-
|
|
1168
|
-
|
|
1169
|
-
|
|
1170
|
-
|
|
1171
|
-
|
|
1172
|
-
|
|
1173
|
-
|
|
1174
|
-
|
|
1175
|
-
|
|
1176
|
-
|
|
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
|
|
1434
|
+
if (channel !== void 0) body.channel = channel;
|
|
1435
|
+
const { response: res, data } = await executeJsonRequest(
|
|
1436
|
+
`${serverUrl}/internal/agent/${agentId}/reminders`,
|
|
1437
|
+
{
|
|
1438
|
+
method: "POST",
|
|
1439
|
+
headers: commonHeaders,
|
|
1440
|
+
body: JSON.stringify(body)
|
|
1441
|
+
},
|
|
1442
|
+
{
|
|
1443
|
+
toolName: "schedule_reminder",
|
|
1444
|
+
fetchImpl: bridgeFetch
|
|
1445
|
+
}
|
|
1446
|
+
);
|
|
1447
|
+
if (!res.ok || !data.reminder) {
|
|
1448
|
+
return { isError: true, content: [{ type: "text", text: `Error: ${data.error || res.statusText}` }] };
|
|
1197
1449
|
}
|
|
1198
|
-
|
|
1199
|
-
|
|
1200
|
-
return {
|
|
1201
|
-
}
|
|
1202
|
-
|
|
1203
|
-
|
|
1204
|
-
|
|
1450
|
+
const lines = [`Reminder scheduled: ${formatReminder(data.reminder)}`];
|
|
1451
|
+
if (data.warning) lines.push(`Warning: ${data.warning}`);
|
|
1452
|
+
return { content: [{ type: "text", text: lines.join("\n") }] };
|
|
1453
|
+
} catch (err) {
|
|
1454
|
+
return {
|
|
1455
|
+
isError: true,
|
|
1456
|
+
content: [{ type: "text", text: `Error: ${err.message}` }]
|
|
1457
|
+
};
|
|
1205
1458
|
}
|
|
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
1459
|
}
|
|
1217
|
-
|
|
1218
|
-
|
|
1219
|
-
|
|
1220
|
-
|
|
1221
|
-
|
|
1222
|
-
|
|
1223
|
-
|
|
1224
|
-
|
|
1225
|
-
|
|
1226
|
-
|
|
1227
|
-
|
|
1228
|
-
|
|
1229
|
-
|
|
1230
|
-
`${serverUrl}/internal/agent/${agentId}/reminders?status=scheduled`,
|
|
1460
|
+
);
|
|
1461
|
+
server.tool(
|
|
1462
|
+
"list_reminders",
|
|
1463
|
+
"List your own reminders. Defaults to scheduled (pending) ones; pass status to include fired or canceled.",
|
|
1464
|
+
{
|
|
1465
|
+
status: z2.string().optional().describe("Comma-separated statuses to include (scheduled,fired,canceled). Defaults to 'scheduled'.")
|
|
1466
|
+
},
|
|
1467
|
+
async ({ status }) => {
|
|
1468
|
+
try {
|
|
1469
|
+
const qs = new URLSearchParams();
|
|
1470
|
+
qs.set("status", status && status.trim().length > 0 ? status.trim() : "scheduled");
|
|
1471
|
+
const { response: res, data } = await executeJsonRequest(
|
|
1472
|
+
`${serverUrl}/internal/agent/${agentId}/reminders?${qs.toString()}`,
|
|
1231
1473
|
{ method: "GET", headers: commonHeaders },
|
|
1232
1474
|
{
|
|
1233
|
-
toolName: "
|
|
1475
|
+
toolName: "list_reminders",
|
|
1234
1476
|
fetchImpl: bridgeFetch
|
|
1235
1477
|
}
|
|
1236
1478
|
);
|
|
1237
|
-
if (!
|
|
1238
|
-
return { isError: true, content: [{ type: "text", text: `Error: ${
|
|
1479
|
+
if (!res.ok) {
|
|
1480
|
+
return { isError: true, content: [{ type: "text", text: `Error: ${data.error || res.statusText}` }] };
|
|
1239
1481
|
}
|
|
1240
|
-
const
|
|
1241
|
-
if (
|
|
1242
|
-
return {
|
|
1482
|
+
const list = data.reminders ?? [];
|
|
1483
|
+
if (list.length === 0) {
|
|
1484
|
+
return { content: [{ type: "text", text: "No reminders." }] };
|
|
1243
1485
|
}
|
|
1244
|
-
|
|
1245
|
-
|
|
1246
|
-
|
|
1247
|
-
|
|
1486
|
+
return {
|
|
1487
|
+
content: [
|
|
1488
|
+
{ type: "text", text: list.map(formatReminder).join("\n") }
|
|
1489
|
+
]
|
|
1490
|
+
};
|
|
1491
|
+
} catch (err) {
|
|
1492
|
+
return {
|
|
1493
|
+
isError: true,
|
|
1494
|
+
content: [{ type: "text", text: `Error: ${err.message}` }]
|
|
1495
|
+
};
|
|
1248
1496
|
}
|
|
1249
|
-
|
|
1250
|
-
|
|
1251
|
-
|
|
1252
|
-
|
|
1253
|
-
|
|
1254
|
-
|
|
1497
|
+
}
|
|
1498
|
+
);
|
|
1499
|
+
server.tool(
|
|
1500
|
+
"cancel_reminder",
|
|
1501
|
+
"Cancel one of your own scheduled reminders by id. Only reminders in 'scheduled' status can be canceled.",
|
|
1502
|
+
{
|
|
1503
|
+
reminder_id: z2.string().describe("The reminder id (full uuid, or the short 8-char prefix shown by schedule_reminder / list_reminders).")
|
|
1504
|
+
},
|
|
1505
|
+
async ({ reminder_id }) => {
|
|
1506
|
+
try {
|
|
1507
|
+
let fullId = reminder_id;
|
|
1508
|
+
if (reminder_id.length < 32) {
|
|
1509
|
+
const { response: listRes, data: listData } = await executeJsonRequest(
|
|
1510
|
+
`${serverUrl}/internal/agent/${agentId}/reminders?status=scheduled`,
|
|
1511
|
+
{ method: "GET", headers: commonHeaders },
|
|
1512
|
+
{
|
|
1513
|
+
toolName: "cancel_reminder.resolve",
|
|
1514
|
+
fetchImpl: bridgeFetch
|
|
1515
|
+
}
|
|
1516
|
+
);
|
|
1517
|
+
if (!listRes.ok) {
|
|
1518
|
+
return { isError: true, content: [{ type: "text", text: `Error: ${listData.error || listRes.statusText}` }] };
|
|
1519
|
+
}
|
|
1520
|
+
const matches = (listData.reminders ?? []).filter((r) => r.reminderId.startsWith(reminder_id));
|
|
1521
|
+
if (matches.length === 0) {
|
|
1522
|
+
return { isError: true, content: [{ type: "text", text: `No scheduled reminder matches id prefix '${reminder_id}'.` }] };
|
|
1523
|
+
}
|
|
1524
|
+
if (matches.length > 1) {
|
|
1525
|
+
return { isError: true, content: [{ type: "text", text: `Ambiguous id prefix '${reminder_id}' matches ${matches.length} reminders; pass a longer id.` }] };
|
|
1526
|
+
}
|
|
1527
|
+
fullId = matches[0].reminderId;
|
|
1255
1528
|
}
|
|
1256
|
-
|
|
1257
|
-
|
|
1258
|
-
|
|
1529
|
+
const { response: res, data } = await executeJsonRequest(
|
|
1530
|
+
`${serverUrl}/internal/agent/${agentId}/reminders/${fullId}`,
|
|
1531
|
+
{ method: "DELETE", headers: commonHeaders },
|
|
1532
|
+
{
|
|
1533
|
+
toolName: "cancel_reminder",
|
|
1534
|
+
fetchImpl: bridgeFetch
|
|
1535
|
+
}
|
|
1536
|
+
);
|
|
1537
|
+
if (!res.ok || !data.reminder) {
|
|
1538
|
+
return { isError: true, content: [{ type: "text", text: `Error: ${data.error || res.statusText}` }] };
|
|
1539
|
+
}
|
|
1540
|
+
return {
|
|
1541
|
+
content: [
|
|
1542
|
+
{ type: "text", text: `Reminder canceled: ${formatReminder(data.reminder)}` }
|
|
1543
|
+
]
|
|
1544
|
+
};
|
|
1545
|
+
} catch (err) {
|
|
1546
|
+
return {
|
|
1547
|
+
isError: true,
|
|
1548
|
+
content: [{ type: "text", text: `Error: ${err.message}` }]
|
|
1549
|
+
};
|
|
1259
1550
|
}
|
|
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
1551
|
}
|
|
1271
|
-
|
|
1272
|
-
|
|
1552
|
+
);
|
|
1553
|
+
}
|
|
1554
|
+
var formatMessages2;
|
|
1555
|
+
var formatReminder2;
|
|
1273
1556
|
var transport = new StdioServerTransport();
|
|
1274
1557
|
await server.connect(transport);
|