@slock-ai/daemon 0.48.0 → 0.49.0-play.20260515203416

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.
@@ -1,219 +1,13 @@
1
1
  #!/usr/bin/env node
2
2
  import {
3
- DEFAULT_CHAT_BRIDGE_TOOL_TIMEOUT_MS,
4
3
  buildFetchDispatcher,
5
- executeJsonRequest,
6
- executeResponseRequest,
7
- logger,
8
- resolveSlockHomePath
9
- } from "./chunk-B7XIMLOT.js";
4
+ executeJsonRequest
5
+ } from "./chunk-KNMCE6WB.js";
10
6
 
11
7
  // src/chat-bridge.ts
12
8
  import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
13
9
  import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
14
- import { z as z2 } from "zod";
15
-
16
- // src/deprecatedMcpShim.ts
17
10
  import { z } from "zod";
18
- var DEPRECATED_MCP_SHIM_HEADER = "This MCP tool is deprecated. Use slock CLI instead.";
19
- var sendMessageDeprecatedSchema = {
20
- target: z.string().describe("Deprecated target argument."),
21
- content: z.string().optional().describe("Deprecated content argument."),
22
- attachment_ids: z.array(z.string()).optional().describe("Deprecated attachment ids.")
23
- };
24
- var checkMessagesDeprecatedSchema = {};
25
- var readHistoryDeprecatedSchema = {
26
- channel: z.string().describe("Deprecated channel argument."),
27
- limit: z.number().optional().describe("Deprecated limit argument."),
28
- around: z.union([z.string(), z.number()]).optional().describe("Deprecated around argument."),
29
- before: z.number().optional().describe("Deprecated before argument."),
30
- after: z.number().optional().describe("Deprecated after argument.")
31
- };
32
- var searchMessagesDeprecatedSchema = {
33
- query: z.string().describe("Deprecated query argument."),
34
- limit: z.number().optional().describe("Deprecated limit argument.")
35
- };
36
- var listTasksDeprecatedSchema = {
37
- channel: z.string().describe("Deprecated channel argument."),
38
- status: z.enum(["all", "todo", "in_progress", "in_review", "done", "closed"]).optional().describe("Deprecated status argument.")
39
- };
40
- var claimTasksDeprecatedSchema = {
41
- channel: z.string().describe("Deprecated channel argument."),
42
- task_numbers: z.array(z.number()).optional().describe("Deprecated task numbers."),
43
- message_ids: z.array(z.string()).optional().describe("Deprecated message ids.")
44
- };
45
- var unclaimTaskDeprecatedSchema = {
46
- channel: z.string().describe("Deprecated channel argument."),
47
- task_number: z.number().describe("Deprecated task number.")
48
- };
49
- var updateTaskStatusDeprecatedSchema = {
50
- channel: z.string().describe("Deprecated channel argument."),
51
- task_number: z.number().describe("Deprecated task number."),
52
- status: z.enum(["todo", "in_progress", "in_review", "done", "closed"]).describe("Deprecated status argument.")
53
- };
54
- var DEPRECATED_MCP_TOOL_DEFINITIONS = [
55
- {
56
- toolName: "send_message",
57
- description: `${DEPRECATED_MCP_SHIM_HEADER}
58
- Use \`slock message send\` and pass the message body on stdin.`,
59
- schema: sendMessageDeprecatedSchema,
60
- cliExamples: [
61
- "slock message send --target '#channel-or-dm' <<'EOF2'",
62
- "message body",
63
- "EOF2"
64
- ]
65
- },
66
- {
67
- toolName: "check_messages",
68
- description: `${DEPRECATED_MCP_SHIM_HEADER}
69
- Use \`slock message check\`.`,
70
- schema: checkMessagesDeprecatedSchema,
71
- cliExamples: [
72
- "slock message check"
73
- ]
74
- },
75
- {
76
- toolName: "read_history",
77
- description: `${DEPRECATED_MCP_SHIM_HEADER}
78
- Use \`slock message read --channel ...\`.`,
79
- schema: readHistoryDeprecatedSchema,
80
- cliExamples: [
81
- "slock message read --channel '#channel'",
82
- "slock message read --channel 'dm:@peer'",
83
- "slock message read --channel '#channel:threadId'"
84
- ]
85
- },
86
- {
87
- toolName: "search_messages",
88
- description: `${DEPRECATED_MCP_SHIM_HEADER}
89
- Use \`slock message search --query ...\`.`,
90
- schema: searchMessagesDeprecatedSchema,
91
- cliExamples: [
92
- "slock message search --query 'keyword'"
93
- ]
94
- },
95
- {
96
- toolName: "list_tasks",
97
- description: `${DEPRECATED_MCP_SHIM_HEADER}
98
- Use \`slock task list --channel ...\`.`,
99
- schema: listTasksDeprecatedSchema,
100
- cliExamples: [
101
- "slock task list --channel '#channel'"
102
- ]
103
- },
104
- {
105
- toolName: "claim_tasks",
106
- description: `${DEPRECATED_MCP_SHIM_HEADER}
107
- Use \`slock task claim ...\`.`,
108
- schema: claimTasksDeprecatedSchema,
109
- cliExamples: [
110
- "slock task claim --channel '#channel' --number 123",
111
- "slock task claim --channel '#channel' --message-id <messageId>"
112
- ]
113
- },
114
- {
115
- toolName: "unclaim_task",
116
- description: `${DEPRECATED_MCP_SHIM_HEADER}
117
- Use \`slock task unclaim ...\`.`,
118
- schema: unclaimTaskDeprecatedSchema,
119
- cliExamples: [
120
- "slock task unclaim --channel '#channel' --number 123"
121
- ]
122
- },
123
- {
124
- toolName: "update_task_status",
125
- description: `${DEPRECATED_MCP_SHIM_HEADER}
126
- Use \`slock task update ...\`.`,
127
- schema: updateTaskStatusDeprecatedSchema,
128
- cliExamples: [
129
- "slock task update --channel '#channel' --number 123 --status in_review"
130
- ]
131
- }
132
- ];
133
- function buildDeprecatedMcpToolErrorText(tool) {
134
- return [
135
- DEPRECATED_MCP_SHIM_HEADER,
136
- "",
137
- ...tool.cliExamples
138
- ].join("\n");
139
- }
140
-
141
- // src/historyFormatting.ts
142
- function toLocalHistoryTime(iso) {
143
- const d = new Date(iso);
144
- if (Number.isNaN(d.getTime())) return iso;
145
- const pad = (n) => String(n).padStart(2, "0");
146
- return `${d.getFullYear()}-${pad(d.getMonth() + 1)}-${pad(d.getDate())} ${pad(d.getHours())}:${pad(d.getMinutes())}:${pad(d.getSeconds())}`;
147
- }
148
- function formatHistorySenderHandle(message) {
149
- return message.senderDescription ? `@${message.senderName} \u2014 ${message.senderDescription}` : `@${message.senderName}`;
150
- }
151
- function formatHistoryMessageLine(message) {
152
- const headerParts = [
153
- `seq=${message.seq}`,
154
- `msg=${message.id || "-"}`,
155
- `time=${message.createdAt ? toLocalHistoryTime(message.createdAt) : "-"}`
156
- ];
157
- if (message.senderType) {
158
- headerParts.push(`type=${message.senderType}`);
159
- }
160
- if (message.threadId) {
161
- headerParts.push(`threadId=${message.threadId}`);
162
- }
163
- if ((message.replyCount ?? 0) > 0) {
164
- headerParts.push(`replyCount=${message.replyCount}`);
165
- }
166
- const attachSuffix = message.attachments?.length ? ` [${message.attachments.length} attachment${message.attachments.length > 1 ? "s" : ""}: ${message.attachments.map((attachment) => `${attachment.filename} (id:${attachment.id})`).join(", ")} \u2014 use view_file to download]` : "";
167
- const taskSuffix = message.taskStatus ? ` [task #${message.taskNumber} status=${message.taskStatus}${message.taskAssigneeId ? ` assignee=${message.taskAssigneeType}:${message.taskAssigneeId}` : ""}]` : "";
168
- return `[${headerParts.join(" ")}] ${formatHistorySenderHandle(message)}: ${message.content}${attachSuffix}${taskSuffix}`;
169
- }
170
-
171
- // src/chatBridgeSendRequest.ts
172
- import { randomUUID } from "crypto";
173
- async function executeRetrySafeSendRequest(url, buildInit, {
174
- fetchImpl,
175
- target,
176
- timeoutMs = DEFAULT_CHAT_BRIDGE_TOOL_TIMEOUT_MS,
177
- now = () => Date.now(),
178
- warn = (message) => logger.warn(message),
179
- idempotencyKey = randomUUID()
180
- }) {
181
- let lastError;
182
- for (let attempt = 1; attempt <= 2; attempt += 1) {
183
- try {
184
- const result = await executeJsonRequest(
185
- url,
186
- buildInit(idempotencyKey),
187
- {
188
- toolName: "send_message",
189
- target,
190
- timeoutMs,
191
- fetchImpl,
192
- now,
193
- warn
194
- }
195
- );
196
- return {
197
- ...result,
198
- idempotencyKey,
199
- attempts: attempt
200
- };
201
- } catch (error) {
202
- lastError = error;
203
- if (attempt === 2) break;
204
- warn(
205
- `[ChatBridgeRetry] tool=send_message target=${target} attempt=${attempt + 1} reason=${describeRetryReason(error)}`
206
- );
207
- }
208
- }
209
- throw lastError;
210
- }
211
- function describeRetryReason(error) {
212
- if (error && typeof error === "object" && "name" in error && error.name === "ChatBridgeToolTimeoutError") {
213
- return error.message;
214
- }
215
- return error instanceof Error ? `${error.name}: ${error.message}` : String(error);
216
- }
217
11
 
218
12
  // src/perfAttribution.ts
219
13
  var PERF_CALLER_CONTEXT_HEADER = "X-Perf-Caller-Context";
@@ -232,30 +26,16 @@ function buildChatBridgeCommonHeaders(authToken2, { includeContentType = true }
232
26
  }
233
27
 
234
28
  // src/chat-bridge.ts
235
- var MAX_UPLOAD_FILE_BYTES = 50 * 1024 * 1024;
236
- var MAX_UPLOAD_FILE_LABEL = "50MB";
237
- function toLocalTime(iso) {
238
- const d = new Date(iso);
239
- if (isNaN(d.getTime())) return iso;
240
- const pad = (n) => String(n).padStart(2, "0");
241
- return `${d.getFullYear()}-${pad(d.getMonth() + 1)}-${pad(d.getDate())} ${pad(d.getHours())}:${pad(d.getMinutes())}:${pad(d.getSeconds())}`;
242
- }
243
29
  var args = process.argv.slice(2);
244
30
  var agentId = "";
245
31
  var serverUrl = "http://localhost:3001";
246
32
  var authToken = "";
247
- var runtime = "unknown";
248
33
  var launchId = "";
249
- var deprecatedShimMode = false;
250
- var runtimeActionsOnlyMode = false;
251
34
  for (let i = 0; i < args.length; i++) {
252
35
  if (args[i] === "--agent-id" && args[i + 1]) agentId = args[++i];
253
36
  if (args[i] === "--server-url" && args[i + 1]) serverUrl = args[++i];
254
37
  if (args[i] === "--auth-token" && args[i + 1]) authToken = args[++i];
255
- if (args[i] === "--runtime" && args[i + 1]) runtime = args[++i];
256
38
  if (args[i] === "--launch-id" && args[i + 1]) launchId = args[++i];
257
- if (args[i] === "--deprecated-shim") deprecatedShimMode = true;
258
- if (args[i] === "--runtime-actions-only") runtimeActionsOnlyMode = true;
259
39
  }
260
40
  if (!agentId) {
261
41
  console.error("Missing --agent-id");
@@ -271,234 +51,16 @@ function bridgeFetch(url, init = {}) {
271
51
  const requestInit = dispatcher ? { ...init, dispatcher } : init;
272
52
  return fetch(url, requestInit);
273
53
  }
274
- var RECENT_DELIVERY_CACHE_LIMIT = 5e3;
275
- var deliveredMessageKeys = /* @__PURE__ */ new Set();
276
- var deliveredMessageOrder = [];
277
- function messageDeliveryKey(message) {
278
- if (message.seq) return `seq:${message.seq}`;
279
- if (message.message_id) return `msg:${message.message_id}`;
280
- return null;
281
- }
282
- function rememberDeliveredMessages(messages) {
283
- const unseen = [];
284
- for (const message of messages) {
285
- const key = messageDeliveryKey(message);
286
- if (!key) {
287
- unseen.push(message);
288
- continue;
289
- }
290
- if (deliveredMessageKeys.has(key)) {
291
- continue;
292
- }
293
- deliveredMessageKeys.add(key);
294
- deliveredMessageOrder.push(key);
295
- unseen.push(message);
296
- }
297
- while (deliveredMessageOrder.length > RECENT_DELIVERY_CACHE_LIMIT) {
298
- const evicted = deliveredMessageOrder.shift();
299
- if (evicted) deliveredMessageKeys.delete(evicted);
300
- }
301
- return unseen;
302
- }
303
- async function acknowledgeReceivedMessages(messages) {
304
- const seqs = [...new Set(
305
- messages.map((message) => message.seq).filter((seq) => typeof seq === "number" && Number.isInteger(seq) && seq > 0)
306
- )];
307
- if (seqs.length === 0) return;
308
- try {
309
- const res = await bridgeFetch(`${serverUrl}/internal/agent/${agentId}/receive-ack`, {
310
- method: "POST",
311
- headers: commonHeaders,
312
- body: JSON.stringify({ seqs })
313
- });
314
- if (!res.ok) {
315
- console.warn(`[chat-bridge] receive-ack failed (${res.status}) for agent ${agentId}; delivery will replay on the next poll`);
316
- }
317
- } catch (err) {
318
- console.warn(
319
- `[chat-bridge] receive-ack errored for agent ${agentId}; delivery will replay on the next poll: ${err instanceof Error ? err.message : String(err)}`
320
- );
321
- }
322
- }
323
- function formatAttachmentSuffix(attachments) {
324
- if (!attachments?.length) return "";
325
- return ` [${attachments.length} attachment${attachments.length > 1 ? "s" : ""}: ${attachments.map((a) => `${a.filename} (id:${a.id})`).join(", ")} \u2014 use view_file to download]`;
326
- }
327
- function guessMimeTypeFromFilename(filename) {
328
- const ext = filename.includes(".") ? filename.slice(filename.lastIndexOf(".")).toLowerCase() : "";
329
- const mimeMap = {
330
- ".jpg": "image/jpeg",
331
- ".jpeg": "image/jpeg",
332
- ".png": "image/png",
333
- ".gif": "image/gif",
334
- ".webp": "image/webp",
335
- ".pdf": "application/pdf",
336
- ".txt": "text/plain",
337
- ".md": "text/markdown",
338
- ".json": "application/json",
339
- ".csv": "text/csv",
340
- ".zip": "application/zip",
341
- ".tar": "application/x-tar",
342
- ".gz": "application/gzip",
343
- ".mp3": "audio/mpeg",
344
- ".wav": "audio/wav",
345
- ".mp4": "video/mp4",
346
- ".mov": "video/quicktime",
347
- ".doc": "application/msword",
348
- ".docx": "application/vnd.openxmlformats-officedocument.wordprocessingml.document",
349
- ".xls": "application/vnd.ms-excel",
350
- ".xlsx": "application/vnd.openxmlformats-officedocument.spreadsheetml.sheet",
351
- ".ppt": "application/vnd.ms-powerpoint",
352
- ".pptx": "application/vnd.openxmlformats-officedocument.presentationml.presentation"
353
- };
354
- return mimeMap[ext] || "application/octet-stream";
355
- }
356
- function parseFilenameFromContentDisposition(contentDisposition) {
357
- if (!contentDisposition) return null;
358
- const utf8Match = contentDisposition.match(/filename\*\s*=\s*UTF-8''([^;]+)/i);
359
- if (utf8Match?.[1]) return decodeURIComponent(utf8Match[1]);
360
- const plainMatch = contentDisposition.match(/filename\s*=\s*"([^"]+)"/i) || contentDisposition.match(/filename\s*=\s*([^;]+)/i);
361
- return plainMatch?.[1] ? plainMatch[1].trim() : null;
362
- }
363
- function extensionForContentType(contentType) {
364
- const normalized = contentType.split(";")[0]?.trim().toLowerCase();
365
- const extMap = {
366
- "image/jpeg": ".jpg",
367
- "image/png": ".png",
368
- "image/gif": ".gif",
369
- "image/webp": ".webp",
370
- "application/pdf": ".pdf",
371
- "text/plain": ".txt",
372
- "text/markdown": ".md",
373
- "application/json": ".json",
374
- "text/csv": ".csv",
375
- "application/zip": ".zip",
376
- "application/x-tar": ".tar",
377
- "application/gzip": ".gz",
378
- "audio/mpeg": ".mp3",
379
- "audio/wav": ".wav",
380
- "video/mp4": ".mp4",
381
- "video/quicktime": ".mov"
382
- };
383
- return extMap[normalized] || ".bin";
384
- }
385
- function formatTarget(m) {
386
- if (m.channel_type === "thread" && m.parent_channel_name) {
387
- const shortId = m.channel_name.startsWith("thread-") ? m.channel_name.slice(7) : m.channel_name;
388
- if (m.parent_channel_type === "dm") {
389
- return `dm:@${m.parent_channel_name}:${shortId}`;
390
- }
391
- return `#${m.parent_channel_name}:${shortId}`;
392
- }
393
- if (m.channel_type === "dm") {
394
- return `dm:@${m.channel_name}`;
395
- }
396
- return `#${m.channel_name}`;
397
- }
398
- function formatSearchTarget(result) {
399
- if (result.channelType === "thread") {
400
- const shortId = typeof result.channelName === "string" && result.channelName.startsWith("thread-") ? result.channelName.slice(7) : typeof result.threadId === "string" && result.threadId ? result.threadId.slice(0, 8) : result.channelName;
401
- if (result.parentChannelType === "dm") {
402
- return `dm:@${result.parentChannelName}:${shortId}`;
403
- }
404
- return `#${result.parentChannelName}:${shortId}`;
405
- }
406
- if (result.channelType === "dm") {
407
- return `dm:@${result.channelName}`;
408
- }
409
- return `#${result.channelName}`;
410
- }
411
- function formatSenderHandle(message) {
412
- const senderName = message.sender_name ?? message.senderName ?? "unknown";
413
- const senderDescription = message.sender_description ?? message.senderDescription ?? null;
414
- return senderDescription ? `@${senderName} \u2014 ${senderDescription}` : `@${senderName}`;
415
- }
416
- function formatRuntimeContext(ctx) {
417
- if (!ctx) return "";
418
- const lines = [
419
- "### Current Runtime",
420
- "Authoritative context for this agent process. Do not infer computer identity from hostname or cwd when this section is present."
421
- ];
422
- if (ctx.agentId) lines.push(`- Agent ID: ${ctx.agentId}`);
423
- if (ctx.serverId) lines.push(`- Server ID: ${ctx.serverId}`);
424
- if (ctx.machineName || ctx.machineId) {
425
- const label = ctx.machineName && ctx.machineId ? `${ctx.machineName} (${ctx.machineId})` : ctx.machineName || ctx.machineId;
426
- lines.push(`- Computer: ${label}`);
427
- }
428
- if (ctx.machineHostname) lines.push(`- Hostname: ${ctx.machineHostname}`);
429
- if (ctx.machineOs) lines.push(`- OS: ${ctx.machineOs}`);
430
- if (ctx.daemonVersion) lines.push(`- Daemon: v${ctx.daemonVersion}`);
431
- if (ctx.workspacePath) lines.push(`- Workspace: ${ctx.workspacePath}`);
432
- return lines.length > 2 ? `${lines.join("\n")}
433
-
434
- ` : "";
435
- }
436
- async function listServerChannels() {
437
- const { response: res, data } = await executeJsonRequest(
438
- `${serverUrl}/internal/agent/${agentId}/server`,
439
- { method: "GET", headers: commonHeaders },
440
- {
441
- toolName: "list_server.channels",
442
- fetchImpl: bridgeFetch
443
- }
444
- );
445
- if (!res.ok) {
446
- throw new Error("Failed to load server channels");
447
- }
448
- return data.channels ?? [];
449
- }
450
- function parseRegularChannelTarget(target) {
451
- if (!target.startsWith("#")) return null;
452
- if (target.includes(":")) return null;
453
- const name = target.slice(1).trim();
454
- return name.length > 0 ? name : null;
455
- }
456
- async function resolveRegularChannelTarget(target) {
457
- const channelName = parseRegularChannelTarget(target);
458
- if (!channelName) {
459
- throw new Error("Target must be a regular channel in the form '#channel-name'");
460
- }
461
- const channels = await listServerChannels();
462
- const channel = channels.find((candidate) => candidate.name === channelName);
463
- if (!channel) {
464
- throw new Error(`Channel not found: ${target}`);
465
- }
466
- return channel;
467
- }
468
54
  var server = new McpServer({
469
55
  name: "chat",
470
56
  version: "1.0.0"
471
57
  });
472
- function logDeprecatedShimInvocation(tool) {
473
- logger.warn(
474
- `[ChatBridgeDeprecatedShim] tool=${tool.toolName} runtime=${runtime} agent_id=${agentId} outcome=deprecated`
475
- );
476
- }
477
- function registerDeprecatedTool(tool) {
478
- server.tool(
479
- tool.toolName,
480
- tool.description,
481
- tool.schema,
482
- async () => {
483
- logDeprecatedShimInvocation(tool);
484
- return {
485
- isError: true,
486
- content: [{ type: "text", text: buildDeprecatedMcpToolErrorText(tool) }]
487
- };
488
- }
489
- );
490
- }
491
- if (deprecatedShimMode && !runtimeActionsOnlyMode) {
492
- for (const tool of DEPRECATED_MCP_TOOL_DEFINITIONS) {
493
- registerDeprecatedTool(tool);
494
- }
495
- }
496
58
  var RUNTIME_PROFILE_MIGRATION_DONE_TOOL_NAME = "runtime_profile_migration_done";
497
59
  server.tool(
498
60
  RUNTIME_PROFILE_MIGRATION_DONE_TOOL_NAME,
499
61
  "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.",
500
62
  {
501
- migration_key: z2.string().describe("The migration key from the Runtime Profile migration hint")
63
+ migration_key: z.string().describe("The migration key from the Runtime Profile migration hint")
502
64
  },
503
65
  async ({ migration_key }) => {
504
66
  const key = migration_key.trim();
@@ -538,960 +100,5 @@ server.tool(
538
100
  }
539
101
  }
540
102
  );
541
- if (!deprecatedShimMode && !runtimeActionsOnlyMode) {
542
- let formatMessages = function(messages) {
543
- return messages.map((m) => {
544
- const target = formatTarget(m);
545
- const msgId = m.message_id ? m.message_id.slice(0, 8) : "-";
546
- const time = m.timestamp ? toLocalTime(m.timestamp) : "-";
547
- const senderType = ` type=${m.sender_type}`;
548
- const renderedContent = m.content;
549
- const attachSuffix = formatAttachmentSuffix(m.attachments);
550
- 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}` : ""}]` : "";
551
- return `[target=${target} msg=${msgId} time=${time}${senderType}] ${formatSenderHandle(m)}: ${renderedContent}${attachSuffix}${taskSuffix}`;
552
- }).join("\n");
553
- }, formatReminder = function(r) {
554
- const fireLocal = toLocalTime(r.fireAt);
555
- const ref = r.msgRef ? ` ref=${r.msgRef}` : "";
556
- const repeat = r.recurrence ? ` repeat=${r.recurrence.description}` : "";
557
- return `#${r.reminderId.slice(0, 8)} [${r.status}] fires=${fireLocal} "${r.title}"${ref}${repeat}`;
558
- };
559
- formatMessages2 = formatMessages, formatReminder2 = formatReminder;
560
- server.tool(
561
- "send_message",
562
- "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'.",
563
- {
564
- target: z2.string().describe(
565
- "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'."
566
- ),
567
- content: z2.string().describe("The message content"),
568
- attachment_ids: z2.array(z2.string()).optional().describe("Optional attachment IDs from upload_file to include with the message")
569
- },
570
- async ({ target, content, attachment_ids }) => {
571
- try {
572
- const { response: res, data } = await executeRetrySafeSendRequest(
573
- `${serverUrl}/internal/agent/${agentId}/send`,
574
- (idempotencyKey) => ({
575
- method: "POST",
576
- headers: commonHeaders,
577
- body: JSON.stringify({ target, content, attachmentIds: attachment_ids, idempotencyKey })
578
- }),
579
- {
580
- target,
581
- fetchImpl: bridgeFetch
582
- }
583
- );
584
- if (!res.ok) {
585
- return {
586
- content: [
587
- { type: "text", text: `Error: ${data.error}` }
588
- ]
589
- };
590
- }
591
- const shortId = data.messageId ? data.messageId.slice(0, 8) : null;
592
- const replyHint = shortId ? ` (to reply in this message's thread, use target "${target.includes(":") ? target : target + ":" + shortId}")` : "";
593
- let unreadSection = "";
594
- if (data.recentUnread && data.recentUnread.length > 0) {
595
- await acknowledgeReceivedMessages(data.recentUnread);
596
- const unreadToShow = rememberDeliveredMessages(data.recentUnread);
597
- if (unreadToShow.length > 0) {
598
- unreadSection = `
599
-
600
- --- New messages you may have missed ---
601
- ${formatMessages(unreadToShow)}`;
602
- }
603
- }
604
- return {
605
- content: [
606
- {
607
- type: "text",
608
- text: `Message sent to ${target}. Message ID: ${data.messageId}${replyHint}${unreadSection}`
609
- }
610
- ]
611
- };
612
- } catch (err) {
613
- return {
614
- isError: true,
615
- content: [{ type: "text", text: `Error: ${err.message}` }]
616
- };
617
- }
618
- }
619
- );
620
- server.tool(
621
- "upload_file",
622
- `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: ${MAX_UPLOAD_FILE_LABEL}. Video files are downloadable attachments and are not parsed by agents. Large PDFs may need to be downloaded and inspected in smaller chunks.`,
623
- {
624
- file_path: z2.string().describe("Absolute path to the file on your local filesystem"),
625
- channel: z2.string().describe("The channel target where this file will be used (e.g. '#general', 'dm:@richard')")
626
- },
627
- async ({ file_path, channel }) => {
628
- try {
629
- const fs = await import("fs");
630
- const path = await import("path");
631
- if (!fs.existsSync(file_path)) {
632
- return {
633
- isError: true,
634
- content: [{ type: "text", text: `Error: File not found: ${file_path}` }]
635
- };
636
- }
637
- const stat = fs.statSync(file_path);
638
- if (stat.size > MAX_UPLOAD_FILE_BYTES) {
639
- return {
640
- isError: true,
641
- content: [{ type: "text", text: `Error: File too large (${(stat.size / 1024 / 1024).toFixed(1)}MB). Max ${MAX_UPLOAD_FILE_LABEL} per file.` }]
642
- };
643
- }
644
- if (stat.size === 0) {
645
- return {
646
- isError: true,
647
- content: [{ type: "text", text: "Error: File is empty; refusing to upload a 0-byte attachment." }]
648
- };
649
- }
650
- const { response: listRes, data: listData } = await executeJsonRequest(
651
- `${serverUrl}/internal/agent/${agentId}/resolve-channel`,
652
- {
653
- method: "POST",
654
- headers: commonHeaders,
655
- body: JSON.stringify({ target: channel })
656
- },
657
- {
658
- toolName: "upload_file.resolve_channel",
659
- target: channel,
660
- fetchImpl: bridgeFetch
661
- }
662
- );
663
- if (!listRes.ok || !listData.channelId) {
664
- return {
665
- isError: true,
666
- content: [{ type: "text", text: `Error: ${listData.error || `Could not resolve channel: ${channel}`}` }]
667
- };
668
- }
669
- const channelId = listData.channelId;
670
- const fileBuffer = fs.readFileSync(file_path);
671
- const filename = path.basename(file_path);
672
- const mimeType = guessMimeTypeFromFilename(filename);
673
- const blob = new Blob([fileBuffer], { type: mimeType });
674
- const formData = new FormData();
675
- formData.append("file", blob, filename);
676
- formData.append("channelId", channelId);
677
- const uploadHeaders = buildChatBridgeCommonHeaders(authToken, { includeContentType: false });
678
- const { response: res, data } = await executeJsonRequest(
679
- `${serverUrl}/internal/agent/${agentId}/upload`,
680
- {
681
- method: "POST",
682
- headers: uploadHeaders,
683
- body: formData
684
- },
685
- {
686
- toolName: "upload_file",
687
- target: channel,
688
- fetchImpl: bridgeFetch
689
- }
690
- );
691
- if (!res.ok) {
692
- return {
693
- isError: true,
694
- content: [{ type: "text", text: `Error: ${data.error}` }]
695
- };
696
- }
697
- return {
698
- content: [
699
- {
700
- type: "text",
701
- text: `File uploaded: ${data.filename} (${(data.sizeBytes / 1024).toFixed(1)}KB)
702
- Attachment ID: ${data.id}
703
-
704
- Use this ID in send_message's attachment_ids parameter to include it in a message.`
705
- }
706
- ]
707
- };
708
- } catch (err) {
709
- return {
710
- isError: true,
711
- content: [{ type: "text", text: `Error: ${err.message}` }]
712
- };
713
- }
714
- }
715
- );
716
- server.tool(
717
- "view_file",
718
- "Download an attached file by its attachment ID and save it locally so you can inspect it. Returns the local file path.",
719
- {
720
- attachment_id: z2.string().describe("The attachment UUID (from the 'id:...' shown in the message)")
721
- },
722
- async ({ attachment_id }) => {
723
- try {
724
- const fs = await import("fs");
725
- const path = await import("path");
726
- const cacheDir = resolveSlockHomePath("attachments");
727
- fs.mkdirSync(cacheDir, { recursive: true });
728
- const existing = fs.readdirSync(cacheDir).find((f) => f.startsWith(attachment_id));
729
- if (existing) {
730
- const cachedPath = path.join(cacheDir, existing);
731
- return {
732
- content: [{ type: "text", text: `File already cached at: ${cachedPath}` }]
733
- };
734
- }
735
- const downloadHeaders = buildChatBridgeCommonHeaders(authToken, { includeContentType: false });
736
- const { response: res } = await executeResponseRequest(
737
- `${serverUrl}/api/attachments/${attachment_id}`,
738
- {
739
- headers: downloadHeaders,
740
- redirect: "follow"
741
- },
742
- {
743
- toolName: "view_file",
744
- target: attachment_id,
745
- fetchImpl: bridgeFetch
746
- }
747
- );
748
- if (!res.ok) {
749
- return {
750
- isError: true,
751
- content: [{ type: "text", text: `Error: Failed to download attachment (${res.status})` }]
752
- };
753
- }
754
- const contentType = res.headers.get("content-type") || "application/octet-stream";
755
- const filename = parseFilenameFromContentDisposition(res.headers.get("content-disposition"));
756
- const ext = filename ? path.extname(filename) || extensionForContentType(contentType) : extensionForContentType(contentType);
757
- const filePath = path.join(cacheDir, `${attachment_id}${ext}`);
758
- const buffer = Buffer.from(await res.arrayBuffer());
759
- fs.writeFileSync(filePath, buffer);
760
- return {
761
- content: [{ type: "text", text: `Downloaded to: ${filePath}` }]
762
- };
763
- } catch (err) {
764
- return {
765
- isError: true,
766
- content: [{ type: "text", text: `Error: ${err.message}` }]
767
- };
768
- }
769
- }
770
- );
771
- server.tool(
772
- "check_messages",
773
- "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.",
774
- {},
775
- async () => {
776
- try {
777
- const { response: res, data } = await executeJsonRequest(
778
- `${serverUrl}/internal/agent/${agentId}/receive`,
779
- { method: "GET", headers: commonHeaders },
780
- {
781
- toolName: "check_messages",
782
- timeoutMs: 1e4,
783
- fetchImpl: bridgeFetch
784
- }
785
- );
786
- if (!res.ok) {
787
- return { isError: true, content: [{ type: "text", text: `Error: ${data.error || res.statusText}` }] };
788
- }
789
- const messages = data.messages ?? [];
790
- if (messages.length > 0) {
791
- await acknowledgeReceivedMessages(messages);
792
- const messagesToShow = rememberDeliveredMessages(messages);
793
- if (messagesToShow.length > 0) {
794
- return { content: [{ type: "text", text: formatMessages(messagesToShow) }] };
795
- }
796
- }
797
- return {
798
- content: [{ type: "text", text: "No new messages." }]
799
- };
800
- } catch (err) {
801
- return {
802
- isError: true,
803
- content: [{ type: "text", text: `Error: ${err.message}` }]
804
- };
805
- }
806
- }
807
- );
808
- server.tool(
809
- "list_server",
810
- "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.",
811
- {},
812
- async () => {
813
- try {
814
- const { response: res, data } = await executeJsonRequest(
815
- `${serverUrl}/internal/agent/${agentId}/server`,
816
- { method: "GET", headers: commonHeaders },
817
- {
818
- toolName: "list_server",
819
- fetchImpl: bridgeFetch
820
- }
821
- );
822
- let text = "## Server\n\n";
823
- const channels = data.channels ?? [];
824
- const agents = data.agents ?? [];
825
- const humans = data.humans ?? [];
826
- text += formatRuntimeContext(data.runtimeContext);
827
- text += "### Channels\n";
828
- text += 'Visible public channels may appear even when `joined=false`. Private channels are shown only when you are a member; do not disclose private-channel names, membership, or content outside that channel. Use `read_history(channel="#name")` to inspect visible channels. 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';
829
- if (channels.length > 0) {
830
- for (const t of channels) {
831
- const visibility = t.type === "private" ? "private" : "public";
832
- const status = `${visibility}, ${t.joined ? "joined" : "not joined"}`;
833
- text += t.description ? ` - #${t.name} [${status}] \u2014 ${t.description}
834
- ` : ` - #${t.name} [${status}]
835
- `;
836
- }
837
- } else {
838
- text += " (none)\n";
839
- }
840
- text += "\n### Agents\n";
841
- text += "Other AI agents in this server.\n";
842
- if (agents.length > 0) {
843
- for (const a of agents) {
844
- text += a.description ? ` - @${a.name} (${a.status}) \u2014 ${a.description}
845
- ` : ` - @${a.name} (${a.status})
846
- `;
847
- }
848
- } else {
849
- text += " (none)\n";
850
- }
851
- text += "\n### Humans\n";
852
- text += 'To start a new DM: send_message(target="dm:@name"). To reply in an existing DM: reuse the target from received messages.\n';
853
- if (humans.length > 0) {
854
- for (const u of humans) {
855
- text += u.description ? ` - @${u.name} \u2014 ${u.description}
856
- ` : ` - @${u.name}
857
- `;
858
- }
859
- } else {
860
- text += " (none)\n";
861
- }
862
- return {
863
- content: [{ type: "text", text }]
864
- };
865
- } catch (err) {
866
- return {
867
- isError: true,
868
- content: [{ type: "text", text: `Error: ${err.message}` }]
869
- };
870
- }
871
- }
872
- );
873
- server.tool(
874
- "leave_channel",
875
- "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.",
876
- {
877
- target: z2.string().describe("Regular channel to leave, in the form '#channel-name'. DMs and thread targets are not supported.")
878
- },
879
- async ({ target }) => {
880
- try {
881
- const channel = await resolveRegularChannelTarget(target);
882
- if (!channel.joined) {
883
- return {
884
- content: [{ type: "text", text: `Already not joined in ${target}.` }]
885
- };
886
- }
887
- const { response: res, data } = await executeJsonRequest(
888
- `${serverUrl}/internal/agent/${agentId}/channels/${channel.id}/leave`,
889
- {
890
- method: "POST",
891
- headers: commonHeaders
892
- },
893
- {
894
- toolName: "leave_channel",
895
- target,
896
- fetchImpl: bridgeFetch
897
- }
898
- );
899
- if (!res.ok) {
900
- return {
901
- isError: true,
902
- content: [{ type: "text", text: `Error: ${data.error || `Failed to leave ${target}`}` }]
903
- };
904
- }
905
- return {
906
- 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.` }]
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
- "search_messages",
918
- "Search messages visible to the agent. Use this to find relevant conversations, then inspect a hit with read_history(channel=..., around=messageId).",
919
- {
920
- query: z2.string().describe("Search query"),
921
- channel: z2.string().optional().describe("Optional target to scope the search, e.g. '#general', 'dm:@richard', '#general:abcd1234'"),
922
- sender_id: z2.string().optional().describe("Optional exact sender id filter."),
923
- after: z2.string().optional().describe("Optional inclusive ISO datetime lower bound for message created_at."),
924
- before: z2.string().optional().describe("Optional inclusive ISO datetime upper bound for message created_at."),
925
- sort: z2.enum(["relevance", "recent"]).optional().describe("Optional result sort. Use 'relevance' for best match first or 'recent' for newest first."),
926
- limit: z2.number().default(10).describe("Max number of search results to return (default 10, max 20)")
927
- },
928
- async ({ query, channel, sender_id, after, before, sort, limit }) => {
929
- try {
930
- const trimmed = query.trim();
931
- if (!trimmed) {
932
- return {
933
- content: [{ type: "text", text: "Search query cannot be empty." }]
934
- };
935
- }
936
- const params = new URLSearchParams();
937
- params.set("q", trimmed);
938
- params.set("limit", String(Math.min(limit, 20)));
939
- if (channel) params.set("channel", channel);
940
- if (sender_id) params.set("senderId", sender_id);
941
- if (after) params.set("after", after);
942
- if (before) params.set("before", before);
943
- if (sort) params.set("sort", sort);
944
- const { response: res, data } = await executeJsonRequest(
945
- `${serverUrl}/internal/agent/${agentId}/search?${params}`,
946
- { method: "GET", headers: commonHeaders },
947
- {
948
- toolName: "search_messages",
949
- target: channel ?? null,
950
- fetchImpl: bridgeFetch
951
- }
952
- );
953
- if (!res.ok) {
954
- return {
955
- content: [{ type: "text", text: `Error: ${data.error}` }]
956
- };
957
- }
958
- if (!data.results || data.results.length === 0) {
959
- return {
960
- content: [{ type: "text", text: "No search results." }]
961
- };
962
- }
963
- const formatted = data.results.map((result, index) => {
964
- const target = formatSearchTarget(result);
965
- const threadInfo = result.channelType === "thread" ? `
966
- thread: ${result.parentChannelName} -> ${target}` : "";
967
- return [
968
- `[${index + 1}] msg=${result.id} seq=${result.seq} time=${toLocalTime(result.createdAt)}`,
969
- `target: ${target}${threadInfo}`,
970
- `sender: @${result.senderName} (${result.senderType})`,
971
- `content: ${result.content}`,
972
- `match: ${result.snippet}`,
973
- `next: read_history(channel="${target}", around="${result.id}", limit=20)`
974
- ].join("\n");
975
- }).join("\n\n");
976
- return {
977
- content: [{
978
- type: "text",
979
- text: `## Search Results for "${trimmed}" (${data.results.length} results)
980
-
981
- ${formatted}`
982
- }]
983
- };
984
- } catch (err) {
985
- return {
986
- isError: true,
987
- content: [{ type: "text", text: `Error: ${err.message}` }]
988
- };
989
- }
990
- }
991
- );
992
- server.tool(
993
- "read_history",
994
- "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).",
995
- {
996
- channel: z2.string().describe("The target to read history from \u2014 e.g. '#general', 'dm:@richard', '#general:abcd1234', 'dm:@richard:abcd1234'"),
997
- limit: z2.number().default(50).describe("Max number of messages to return (default 50, max 100)"),
998
- around: z2.union([z2.string(), z2.number()]).optional().describe("Center the result window around a messageId or seq in this channel/thread."),
999
- before: z2.number().optional().describe("Return messages before this seq number (for backward pagination). Omit for latest messages."),
1000
- after: z2.number().optional().describe("Return messages after this seq number (for catching up on unread). Returns oldest-first.")
1001
- },
1002
- async ({ channel, limit, around, before, after }) => {
1003
- try {
1004
- const params = new URLSearchParams();
1005
- params.set("channel", channel);
1006
- params.set("limit", String(Math.min(limit, 100)));
1007
- if (around !== void 0) params.set("around", String(around));
1008
- if (before) params.set("before", String(before));
1009
- if (after) params.set("after", String(after));
1010
- const { response: res, data } = await executeJsonRequest(
1011
- `${serverUrl}/internal/agent/${agentId}/history?${params}`,
1012
- { method: "GET", headers: commonHeaders },
1013
- {
1014
- toolName: "read_history",
1015
- target: channel,
1016
- fetchImpl: bridgeFetch
1017
- }
1018
- );
1019
- if (!res.ok) {
1020
- return {
1021
- content: [
1022
- { type: "text", text: `Error: ${data.error}` }
1023
- ]
1024
- };
1025
- }
1026
- if (!data.messages || data.messages.length === 0) {
1027
- return {
1028
- content: [
1029
- { type: "text", text: "No messages in this channel." }
1030
- ]
1031
- };
1032
- }
1033
- const formatted = data.messages.map((m) => formatHistoryMessageLine({
1034
- ...m,
1035
- senderName: m.senderName ?? m.sender_name ?? "unknown",
1036
- senderDescription: m.senderDescription ?? m.sender_description ?? null
1037
- })).join("\n");
1038
- let footer = "";
1039
- if (data.historyLimited) {
1040
- footer = `
1041
-
1042
- --- ${data.historyLimitMessage || "Message history is limited on this plan."} ---`;
1043
- } else if (around && data.messages.length > 0 && (data.has_older || data.has_newer)) {
1044
- const minSeq = data.messages[0].seq;
1045
- const maxSeq = data.messages[data.messages.length - 1].seq;
1046
- footer = `
1047
-
1048
- --- Context window shown. Use before=${minSeq} to load older messages or after=${maxSeq} to load newer messages. ---`;
1049
- } else if (data.has_more && data.messages.length > 0) {
1050
- if (after) {
1051
- const maxSeq = data.messages[data.messages.length - 1].seq;
1052
- footer = `
1053
-
1054
- --- ${data.messages.length} messages shown. Use after=${maxSeq} to load more recent messages. ---`;
1055
- } else {
1056
- const minSeq = data.messages[0].seq;
1057
- footer = `
1058
-
1059
- --- ${data.messages.length} messages shown. Use before=${minSeq} to load older messages. ---`;
1060
- }
1061
- }
1062
- let header = `## Message History for ${channel}${around ? ` around ${around}` : ""} (${data.messages.length} messages)`;
1063
- if ((data.last_read_seq ?? 0) > 0 && !after && !before && !around) {
1064
- header += `
1065
- Your last read position: seq ${data.last_read_seq}. Use read_history(channel="${channel}", after=${data.last_read_seq}) to see only unread messages.`;
1066
- }
1067
- return {
1068
- content: [
1069
- {
1070
- type: "text",
1071
- text: `${header}
1072
-
1073
- ${formatted}${footer}`
1074
- }
1075
- ]
1076
- };
1077
- } catch (err) {
1078
- return {
1079
- isError: true,
1080
- content: [{ type: "text", text: `Error: ${err.message}` }]
1081
- };
1082
- }
1083
- }
1084
- );
1085
- server.tool(
1086
- "list_tasks",
1087
- "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.",
1088
- {
1089
- channel: z2.string().describe("The channel whose task board to view \u2014 e.g. '#engineering', '#proj-slock'"),
1090
- status: z2.enum(["all", "todo", "in_progress", "in_review", "done", "closed"]).default("all").describe("Filter by status (default: all)")
1091
- },
1092
- async ({ channel, status }) => {
1093
- try {
1094
- const params = new URLSearchParams();
1095
- params.set("channel", channel);
1096
- if (status !== "all") params.set("status", status);
1097
- const { response: res, data } = await executeJsonRequest(
1098
- `${serverUrl}/internal/agent/${agentId}/tasks?${params}`,
1099
- { method: "GET", headers: commonHeaders },
1100
- {
1101
- toolName: "list_tasks",
1102
- target: channel,
1103
- fetchImpl: bridgeFetch
1104
- }
1105
- );
1106
- if (!res.ok) {
1107
- return {
1108
- isError: true,
1109
- content: [{ type: "text", text: `Error: ${data.error}` }]
1110
- };
1111
- }
1112
- if (!data.tasks || data.tasks.length === 0) {
1113
- return {
1114
- content: [{ type: "text", text: `No${status !== "all" ? ` ${status}` : ""} tasks in ${channel}.` }]
1115
- };
1116
- }
1117
- const formatted = data.tasks.map((t) => {
1118
- const assignee = t.claimedByName ? ` \u2192 @${t.claimedByName}` : "";
1119
- const creator = t.createdByName ? ` (by @${t.createdByName})` : "";
1120
- const msgId = t.messageId ? ` msg=${t.messageId.slice(0, 8)}` : "";
1121
- const legacy = t.isLegacy ? " [LEGACY \u2014 read-only]" : "";
1122
- return `#${t.taskNumber} [${t.status}] ${t.title}${assignee}${creator}${msgId}${legacy}`;
1123
- }).join("\n");
1124
- return {
1125
- content: [
1126
- {
1127
- type: "text",
1128
- text: `## Task Board for ${channel} (${data.tasks.length} tasks)
1129
-
1130
- ${formatted}`
1131
- }
1132
- ]
1133
- };
1134
- } catch (err) {
1135
- return {
1136
- isError: true,
1137
- content: [{ type: "text", text: `Error: ${err.message}` }]
1138
- };
1139
- }
1140
- }
1141
- );
1142
- server.tool(
1143
- "create_tasks",
1144
- "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.",
1145
- {
1146
- channel: z2.string().describe("The channel to create tasks in \u2014 e.g. '#engineering'"),
1147
- tasks: z2.array(
1148
- z2.object({
1149
- title: z2.string().describe("Task title")
1150
- })
1151
- ).describe("Array of tasks to create")
1152
- },
1153
- async ({ channel, tasks }) => {
1154
- try {
1155
- const { response: res, data } = await executeJsonRequest(
1156
- `${serverUrl}/internal/agent/${agentId}/tasks`,
1157
- {
1158
- method: "POST",
1159
- headers: commonHeaders,
1160
- body: JSON.stringify({ channel, tasks })
1161
- },
1162
- {
1163
- toolName: "create_tasks",
1164
- target: channel,
1165
- fetchImpl: bridgeFetch
1166
- }
1167
- );
1168
- if (!res.ok) {
1169
- return {
1170
- isError: true,
1171
- content: [{ type: "text", text: `Error: ${data.error}` }]
1172
- };
1173
- }
1174
- const created = data.tasks.map((t) => `#${t.taskNumber} msg=${t.messageId.slice(0, 8)} "${t.title}"`).join("\n");
1175
- const threadHints = data.tasks.map((t) => `#${t.taskNumber} \u2192 send_message to "${channel}:${t.messageId.slice(0, 8)}"`).join("\n");
1176
- return {
1177
- content: [
1178
- {
1179
- type: "text",
1180
- text: `Created ${data.tasks.length} task(s) in ${channel}:
1181
- ${created}
1182
-
1183
- To follow up in each task's thread:
1184
- ${threadHints}`
1185
- }
1186
- ]
1187
- };
1188
- } catch (err) {
1189
- return {
1190
- isError: true,
1191
- content: [{ type: "text", text: `Error: ${err.message}` }]
1192
- };
1193
- }
1194
- }
1195
- );
1196
- server.tool(
1197
- "claim_tasks",
1198
- `Claim tasks so you are assigned to work on them. Two modes:
1199
- 1. By task number: claim existing tasks shown in list_tasks. Use task_numbers=[1, 3].
1200
- 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.
1201
-
1202
- 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.`,
1203
- {
1204
- channel: z2.string().describe("The channel \u2014 e.g. '#engineering'"),
1205
- task_numbers: z2.array(z2.number()).optional().describe("Task numbers to claim (from list_tasks output, e.g. [1, 3])"),
1206
- 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.")
1207
- },
1208
- async ({ channel, task_numbers, message_ids }) => {
1209
- try {
1210
- if ((!task_numbers || task_numbers.length === 0) && (!message_ids || message_ids.length === 0)) {
1211
- return {
1212
- content: [{ type: "text", text: "Error: provide at least one of task_numbers or message_ids" }]
1213
- };
1214
- }
1215
- const body = { channel };
1216
- if (task_numbers && task_numbers.length > 0) body.task_numbers = task_numbers;
1217
- if (message_ids && message_ids.length > 0) body.message_ids = message_ids;
1218
- const { response: res, data } = await executeJsonRequest(
1219
- `${serverUrl}/internal/agent/${agentId}/tasks/claim`,
1220
- {
1221
- method: "POST",
1222
- headers: commonHeaders,
1223
- body: JSON.stringify(body)
1224
- },
1225
- {
1226
- toolName: "claim_tasks",
1227
- target: channel,
1228
- fetchImpl: bridgeFetch
1229
- }
1230
- );
1231
- if (!res.ok) {
1232
- return {
1233
- isError: true,
1234
- content: [{ type: "text", text: `Error: ${data.error}` }]
1235
- };
1236
- }
1237
- const lines = data.results.map((r) => {
1238
- const label = r.taskNumber ? `#${r.taskNumber}` : `msg:${r.messageId}`;
1239
- if (r.success) {
1240
- const msgShort = r.messageId ? r.messageId.slice(0, 8) : "";
1241
- return `${label} (msg:${msgShort}): claimed`;
1242
- }
1243
- return `${label}: FAILED \u2014 ${r.reason || "already claimed"}. Do not reply.`;
1244
- });
1245
- const succeeded = data.results.filter((r) => r.success).length;
1246
- const failed = data.results.length - succeeded;
1247
- let summary = `${succeeded} claimed`;
1248
- if (failed > 0) summary += `, ${failed} failed`;
1249
- 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");
1250
- const threadHint = claimedMsgs ? `
1251
-
1252
- Follow up in each task's thread:
1253
- ${claimedMsgs}` : "";
1254
- return {
1255
- content: [
1256
- {
1257
- type: "text",
1258
- text: `Claim results (${summary}):
1259
- ${lines.join("\n")}${threadHint}`
1260
- }
1261
- ]
1262
- };
1263
- } catch (err) {
1264
- return {
1265
- isError: true,
1266
- content: [{ type: "text", text: `Error: ${err.message}` }]
1267
- };
1268
- }
1269
- }
1270
- );
1271
- server.tool(
1272
- "unclaim_task",
1273
- "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.",
1274
- {
1275
- channel: z2.string().describe("The channel \u2014 e.g. '#engineering'"),
1276
- task_number: z2.number().describe("The task number to unclaim (e.g. 3)")
1277
- },
1278
- async ({ channel, task_number }) => {
1279
- try {
1280
- const { response: res, data } = await executeJsonRequest(
1281
- `${serverUrl}/internal/agent/${agentId}/tasks/unclaim`,
1282
- {
1283
- method: "POST",
1284
- headers: commonHeaders,
1285
- body: JSON.stringify({ channel, task_number })
1286
- },
1287
- {
1288
- toolName: "unclaim_task",
1289
- target: channel,
1290
- fetchImpl: bridgeFetch
1291
- }
1292
- );
1293
- if (!res.ok) {
1294
- return {
1295
- isError: true,
1296
- content: [{ type: "text", text: `Error: ${data.error}` }]
1297
- };
1298
- }
1299
- return {
1300
- content: [
1301
- { type: "text", text: `#${task_number} unclaimed \u2014 now open.` }
1302
- ]
1303
- };
1304
- } catch (err) {
1305
- return {
1306
- isError: true,
1307
- content: [{ type: "text", text: `Error: ${err.message}` }]
1308
- };
1309
- }
1310
- }
1311
- );
1312
- server.tool(
1313
- "update_task_status",
1314
- "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. Use closed to mark a task as won't-do (cancelled / abandoned / out-of-scope) \u2014 distinct from done. Valid transitions: todo\u2192{in_progress,closed}, in_progress\u2192{in_review,done,closed}, in_review\u2192{done,in_progress,closed}, done\u2192{todo,in_progress,in_review,closed}, closed\u2192todo (reopen).",
1315
- {
1316
- channel: z2.string().describe("The channel \u2014 e.g. '#engineering'"),
1317
- task_number: z2.number().describe("The task number to update (e.g. 3)"),
1318
- status: z2.enum(["todo", "in_progress", "in_review", "done", "closed"]).describe("The new status")
1319
- },
1320
- async ({ channel, task_number, status }) => {
1321
- try {
1322
- const { response: res, data } = await executeJsonRequest(
1323
- `${serverUrl}/internal/agent/${agentId}/tasks/update-status`,
1324
- {
1325
- method: "POST",
1326
- headers: commonHeaders,
1327
- body: JSON.stringify({ channel, task_number, status })
1328
- },
1329
- {
1330
- toolName: "update_task_status",
1331
- target: channel,
1332
- fetchImpl: bridgeFetch
1333
- }
1334
- );
1335
- if (!res.ok) {
1336
- return {
1337
- isError: true,
1338
- content: [{ type: "text", text: `Error: ${data.error}` }]
1339
- };
1340
- }
1341
- return {
1342
- content: [
1343
- { type: "text", text: `#${task_number} moved to ${status}.` }
1344
- ]
1345
- };
1346
- } catch (err) {
1347
- return {
1348
- isError: true,
1349
- content: [{ type: "text", text: `Error: ${err.message}` }]
1350
- };
1351
- }
1352
- }
1353
- );
1354
- server.tool(
1355
- "schedule_reminder",
1356
- "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.",
1357
- {
1358
- title: z2.string().describe("Short description of what the reminder is about. This is what you'll see when it fires."),
1359
- 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."),
1360
- 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."),
1361
- 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."),
1362
- 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."),
1363
- 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.")
1364
- },
1365
- async ({ title, delay_seconds, fire_at, repeat, channel, msg_id }) => {
1366
- try {
1367
- const body = { title, msgId: msg_id };
1368
- if (delay_seconds !== void 0) body.delaySeconds = delay_seconds;
1369
- if (fire_at !== void 0) body.fireAt = fire_at;
1370
- if (repeat !== void 0) {
1371
- body.repeat = repeat;
1372
- body.tz = Intl.DateTimeFormat().resolvedOptions().timeZone;
1373
- }
1374
- if (channel !== void 0) body.channel = channel;
1375
- const { response: res, data } = await executeJsonRequest(
1376
- `${serverUrl}/internal/agent/${agentId}/reminders`,
1377
- {
1378
- method: "POST",
1379
- headers: commonHeaders,
1380
- body: JSON.stringify(body)
1381
- },
1382
- {
1383
- toolName: "schedule_reminder",
1384
- fetchImpl: bridgeFetch
1385
- }
1386
- );
1387
- if (!res.ok || !data.reminder) {
1388
- return { isError: true, content: [{ type: "text", text: `Error: ${data.error || res.statusText}` }] };
1389
- }
1390
- const lines = [`Reminder scheduled: ${formatReminder(data.reminder)}`];
1391
- if (data.warning) lines.push(`Warning: ${data.warning}`);
1392
- return { content: [{ type: "text", text: lines.join("\n") }] };
1393
- } catch (err) {
1394
- return {
1395
- isError: true,
1396
- content: [{ type: "text", text: `Error: ${err.message}` }]
1397
- };
1398
- }
1399
- }
1400
- );
1401
- server.tool(
1402
- "list_reminders",
1403
- "List your own reminders. Defaults to scheduled (pending) ones; pass status to include fired or canceled.",
1404
- {
1405
- status: z2.string().optional().describe("Comma-separated statuses to include (scheduled,fired,canceled). Defaults to 'scheduled'.")
1406
- },
1407
- async ({ status }) => {
1408
- try {
1409
- const qs = new URLSearchParams();
1410
- qs.set("status", status && status.trim().length > 0 ? status.trim() : "scheduled");
1411
- const { response: res, data } = await executeJsonRequest(
1412
- `${serverUrl}/internal/agent/${agentId}/reminders?${qs.toString()}`,
1413
- { method: "GET", headers: commonHeaders },
1414
- {
1415
- toolName: "list_reminders",
1416
- fetchImpl: bridgeFetch
1417
- }
1418
- );
1419
- if (!res.ok) {
1420
- return { isError: true, content: [{ type: "text", text: `Error: ${data.error || res.statusText}` }] };
1421
- }
1422
- const list = data.reminders ?? [];
1423
- if (list.length === 0) {
1424
- return { content: [{ type: "text", text: "No reminders." }] };
1425
- }
1426
- return {
1427
- content: [
1428
- { type: "text", text: list.map(formatReminder).join("\n") }
1429
- ]
1430
- };
1431
- } catch (err) {
1432
- return {
1433
- isError: true,
1434
- content: [{ type: "text", text: `Error: ${err.message}` }]
1435
- };
1436
- }
1437
- }
1438
- );
1439
- server.tool(
1440
- "cancel_reminder",
1441
- "Cancel one of your own scheduled reminders by id. Only reminders in 'scheduled' status can be canceled.",
1442
- {
1443
- reminder_id: z2.string().describe("The reminder id (full uuid, or the short 8-char prefix shown by schedule_reminder / list_reminders).")
1444
- },
1445
- async ({ reminder_id }) => {
1446
- try {
1447
- let fullId = reminder_id;
1448
- if (reminder_id.length < 32) {
1449
- const { response: listRes, data: listData } = await executeJsonRequest(
1450
- `${serverUrl}/internal/agent/${agentId}/reminders?status=scheduled`,
1451
- { method: "GET", headers: commonHeaders },
1452
- {
1453
- toolName: "cancel_reminder.resolve",
1454
- fetchImpl: bridgeFetch
1455
- }
1456
- );
1457
- if (!listRes.ok) {
1458
- return { isError: true, content: [{ type: "text", text: `Error: ${listData.error || listRes.statusText}` }] };
1459
- }
1460
- const matches = (listData.reminders ?? []).filter((r) => r.reminderId.startsWith(reminder_id));
1461
- if (matches.length === 0) {
1462
- return { isError: true, content: [{ type: "text", text: `No scheduled reminder matches id prefix '${reminder_id}'.` }] };
1463
- }
1464
- if (matches.length > 1) {
1465
- return { isError: true, content: [{ type: "text", text: `Ambiguous id prefix '${reminder_id}' matches ${matches.length} reminders; pass a longer id.` }] };
1466
- }
1467
- fullId = matches[0].reminderId;
1468
- }
1469
- const { response: res, data } = await executeJsonRequest(
1470
- `${serverUrl}/internal/agent/${agentId}/reminders/${fullId}`,
1471
- { method: "DELETE", headers: commonHeaders },
1472
- {
1473
- toolName: "cancel_reminder",
1474
- fetchImpl: bridgeFetch
1475
- }
1476
- );
1477
- if (!res.ok || !data.reminder) {
1478
- return { isError: true, content: [{ type: "text", text: `Error: ${data.error || res.statusText}` }] };
1479
- }
1480
- return {
1481
- content: [
1482
- { type: "text", text: `Reminder canceled: ${formatReminder(data.reminder)}` }
1483
- ]
1484
- };
1485
- } catch (err) {
1486
- return {
1487
- isError: true,
1488
- content: [{ type: "text", text: `Error: ${err.message}` }]
1489
- };
1490
- }
1491
- }
1492
- );
1493
- }
1494
- var formatMessages2;
1495
- var formatReminder2;
1496
103
  var transport = new StdioServerTransport();
1497
104
  await server.connect(transport);