@slock-ai/daemon 0.34.0 → 0.36.1-alpha.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.
@@ -1,12 +1,205 @@
1
1
  #!/usr/bin/env node
2
2
  import {
3
- buildFetchDispatcher
4
- } from "./chunk-GX2DVINN.js";
3
+ buildFetchDispatcher,
4
+ logger
5
+ } from "./chunk-E6OOH3IC.js";
5
6
 
6
7
  // src/chat-bridge.ts
7
8
  import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
8
9
  import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
9
10
  import { z } from "zod";
11
+
12
+ // src/historyFormatting.ts
13
+ function toLocalHistoryTime(iso) {
14
+ const d = new Date(iso);
15
+ if (Number.isNaN(d.getTime())) return iso;
16
+ const pad = (n) => String(n).padStart(2, "0");
17
+ return `${d.getFullYear()}-${pad(d.getMonth() + 1)}-${pad(d.getDate())} ${pad(d.getHours())}:${pad(d.getMinutes())}:${pad(d.getSeconds())}`;
18
+ }
19
+ function formatHistorySenderHandle(message) {
20
+ return message.senderDescription ? `@${message.senderName} \u2014 ${message.senderDescription}` : `@${message.senderName}`;
21
+ }
22
+ function formatHistoryMessageLine(message) {
23
+ const headerParts = [
24
+ `seq=${message.seq}`,
25
+ `msg=${message.id || "-"}`,
26
+ `time=${message.createdAt ? toLocalHistoryTime(message.createdAt) : "-"}`
27
+ ];
28
+ if (message.senderType) {
29
+ headerParts.push(`type=${message.senderType}`);
30
+ }
31
+ if (message.threadId) {
32
+ headerParts.push(`threadId=${message.threadId}`);
33
+ }
34
+ if ((message.replyCount ?? 0) > 0) {
35
+ headerParts.push(`replyCount=${message.replyCount}`);
36
+ }
37
+ 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]` : "";
38
+ const taskSuffix = message.taskStatus ? ` [task #${message.taskNumber} status=${message.taskStatus}${message.taskAssigneeId ? ` assignee=${message.taskAssigneeType}:${message.taskAssigneeId}` : ""}]` : "";
39
+ return `[${headerParts.join(" ")}] ${formatHistorySenderHandle(message)}: ${message.content}${attachSuffix}${taskSuffix}`;
40
+ }
41
+
42
+ // src/chatBridgeRequest.ts
43
+ var DEFAULT_CHAT_BRIDGE_TOOL_TIMEOUT_MS = Number.parseInt(
44
+ process.env.SLOCK_CHAT_BRIDGE_TOOL_TIMEOUT_MS || "",
45
+ 10
46
+ ) || 6e4;
47
+ var ChatBridgeToolTimeoutError = class extends Error {
48
+ toolName;
49
+ target;
50
+ timeoutMs;
51
+ durationMs;
52
+ constructor(toolName, target, timeoutMs, durationMs) {
53
+ super(`${toolName} timed out after ${timeoutMs}ms${target ? ` (target: ${target})` : ""}`);
54
+ this.name = "ChatBridgeToolTimeoutError";
55
+ this.toolName = toolName;
56
+ this.target = target;
57
+ this.timeoutMs = timeoutMs;
58
+ this.durationMs = durationMs;
59
+ }
60
+ };
61
+ function describeError(err) {
62
+ if (err instanceof Error) return `${err.name}: ${err.message}`;
63
+ return String(err);
64
+ }
65
+ async function executeJsonRequest(url, init, {
66
+ toolName,
67
+ target = null,
68
+ timeoutMs = DEFAULT_CHAT_BRIDGE_TOOL_TIMEOUT_MS,
69
+ fetchImpl,
70
+ now = () => Date.now(),
71
+ warn = (message) => logger.warn(message)
72
+ }) {
73
+ const startedAt = now();
74
+ const timeoutController = new AbortController();
75
+ const signals = [timeoutController.signal];
76
+ if (init.signal) signals.push(init.signal);
77
+ const signal = signals.length === 1 ? signals[0] : AbortSignal.any(signals);
78
+ const timeout = setTimeout(() => {
79
+ timeoutController.abort();
80
+ }, timeoutMs);
81
+ timeout.unref?.();
82
+ try {
83
+ const response = await fetchImpl(url, { ...init, signal });
84
+ const data = await response.json();
85
+ return { response, data, durationMs: now() - startedAt };
86
+ } catch (err) {
87
+ const durationMs = now() - startedAt;
88
+ if (timeoutController.signal.aborted && !init.signal?.aborted) {
89
+ warn(
90
+ `[ChatBridgeTimeout] tool=${toolName} target=${target ?? "-"} duration_ms=${durationMs} timeout_ms=${timeoutMs} outcome=timeout`
91
+ );
92
+ throw new ChatBridgeToolTimeoutError(toolName, target, timeoutMs, durationMs);
93
+ }
94
+ warn(
95
+ `[ChatBridgeError] tool=${toolName} target=${target ?? "-"} duration_ms=${durationMs} outcome=error error=${describeError(err)}`
96
+ );
97
+ throw err;
98
+ } finally {
99
+ clearTimeout(timeout);
100
+ }
101
+ }
102
+ async function executeResponseRequest(url, init, {
103
+ toolName,
104
+ target = null,
105
+ timeoutMs = DEFAULT_CHAT_BRIDGE_TOOL_TIMEOUT_MS,
106
+ fetchImpl,
107
+ now = () => Date.now(),
108
+ warn = (message) => logger.warn(message)
109
+ }) {
110
+ const startedAt = now();
111
+ const timeoutController = new AbortController();
112
+ const signals = [timeoutController.signal];
113
+ if (init.signal) signals.push(init.signal);
114
+ const signal = signals.length === 1 ? signals[0] : AbortSignal.any(signals);
115
+ const timeout = setTimeout(() => {
116
+ timeoutController.abort();
117
+ }, timeoutMs);
118
+ timeout.unref?.();
119
+ try {
120
+ const response = await fetchImpl(url, { ...init, signal });
121
+ return { response, durationMs: now() - startedAt };
122
+ } catch (err) {
123
+ const durationMs = now() - startedAt;
124
+ if (timeoutController.signal.aborted && !init.signal?.aborted) {
125
+ warn(
126
+ `[ChatBridgeTimeout] tool=${toolName} target=${target ?? "-"} duration_ms=${durationMs} timeout_ms=${timeoutMs} outcome=timeout`
127
+ );
128
+ throw new ChatBridgeToolTimeoutError(toolName, target, timeoutMs, durationMs);
129
+ }
130
+ warn(
131
+ `[ChatBridgeError] tool=${toolName} target=${target ?? "-"} duration_ms=${durationMs} outcome=error error=${describeError(err)}`
132
+ );
133
+ throw err;
134
+ } finally {
135
+ clearTimeout(timeout);
136
+ }
137
+ }
138
+
139
+ // src/chatBridgeSendRequest.ts
140
+ import { randomUUID } from "crypto";
141
+ async function executeRetrySafeSendRequest(url, buildInit, {
142
+ fetchImpl,
143
+ target,
144
+ timeoutMs = DEFAULT_CHAT_BRIDGE_TOOL_TIMEOUT_MS,
145
+ now = () => Date.now(),
146
+ warn = (message) => logger.warn(message),
147
+ idempotencyKey = randomUUID()
148
+ }) {
149
+ let lastError;
150
+ for (let attempt = 1; attempt <= 2; attempt += 1) {
151
+ try {
152
+ const result = await executeJsonRequest(
153
+ url,
154
+ buildInit(idempotencyKey),
155
+ {
156
+ toolName: "send_message",
157
+ target,
158
+ timeoutMs,
159
+ fetchImpl,
160
+ now,
161
+ warn
162
+ }
163
+ );
164
+ return {
165
+ ...result,
166
+ idempotencyKey,
167
+ attempts: attempt
168
+ };
169
+ } catch (error) {
170
+ lastError = error;
171
+ if (attempt === 2) break;
172
+ warn(
173
+ `[ChatBridgeRetry] tool=send_message target=${target} attempt=${attempt + 1} reason=${describeRetryReason(error)}`
174
+ );
175
+ }
176
+ }
177
+ throw lastError;
178
+ }
179
+ function describeRetryReason(error) {
180
+ if (error && typeof error === "object" && "name" in error && error.name === "ChatBridgeToolTimeoutError") {
181
+ return error.message;
182
+ }
183
+ return error instanceof Error ? `${error.name}: ${error.message}` : String(error);
184
+ }
185
+
186
+ // src/perfAttribution.ts
187
+ var PERF_CALLER_CONTEXT_HEADER = "X-Perf-Caller-Context";
188
+ var AGENT_ORIGINATED_CALLER_CONTEXT = "agent_originated";
189
+ function buildChatBridgeCommonHeaders(authToken2, { includeContentType = true } = {}) {
190
+ const headers = {
191
+ [PERF_CALLER_CONTEXT_HEADER]: AGENT_ORIGINATED_CALLER_CONTEXT
192
+ };
193
+ if (includeContentType) {
194
+ headers["Content-Type"] = "application/json";
195
+ }
196
+ if (authToken2) {
197
+ headers.Authorization = `Bearer ${authToken2}`;
198
+ }
199
+ return headers;
200
+ }
201
+
202
+ // src/chat-bridge.ts
10
203
  function toLocalTime(iso) {
11
204
  const d = new Date(iso);
12
205
  if (isNaN(d.getTime())) return iso;
@@ -26,15 +219,123 @@ if (!agentId) {
26
219
  console.error("Missing --agent-id");
27
220
  process.exit(1);
28
221
  }
29
- var commonHeaders = { "Content-Type": "application/json" };
30
- if (authToken) {
31
- commonHeaders["Authorization"] = `Bearer ${authToken}`;
32
- }
222
+ var commonHeaders = buildChatBridgeCommonHeaders(authToken);
33
223
  function bridgeFetch(url, init = {}) {
34
224
  const dispatcher = buildFetchDispatcher(url, process.env);
35
225
  const requestInit = dispatcher ? { ...init, dispatcher } : init;
36
226
  return fetch(url, requestInit);
37
227
  }
228
+ var RECENT_DELIVERY_CACHE_LIMIT = 5e3;
229
+ var deliveredMessageKeys = /* @__PURE__ */ new Set();
230
+ var deliveredMessageOrder = [];
231
+ function messageDeliveryKey(message) {
232
+ if (message.seq) return `seq:${message.seq}`;
233
+ if (message.message_id) return `msg:${message.message_id}`;
234
+ return null;
235
+ }
236
+ function rememberDeliveredMessages(messages) {
237
+ const unseen = [];
238
+ for (const message of messages) {
239
+ const key = messageDeliveryKey(message);
240
+ if (!key) {
241
+ unseen.push(message);
242
+ continue;
243
+ }
244
+ if (deliveredMessageKeys.has(key)) {
245
+ continue;
246
+ }
247
+ deliveredMessageKeys.add(key);
248
+ deliveredMessageOrder.push(key);
249
+ unseen.push(message);
250
+ }
251
+ while (deliveredMessageOrder.length > RECENT_DELIVERY_CACHE_LIMIT) {
252
+ const evicted = deliveredMessageOrder.shift();
253
+ if (evicted) deliveredMessageKeys.delete(evicted);
254
+ }
255
+ return unseen;
256
+ }
257
+ async function acknowledgeReceivedMessages(messages) {
258
+ const seqs = [...new Set(
259
+ messages.map((message) => message.seq).filter((seq) => typeof seq === "number" && Number.isInteger(seq) && seq > 0)
260
+ )];
261
+ if (seqs.length === 0) return;
262
+ try {
263
+ const res = await bridgeFetch(`${serverUrl}/internal/agent/${agentId}/receive-ack`, {
264
+ method: "POST",
265
+ headers: commonHeaders,
266
+ body: JSON.stringify({ seqs })
267
+ });
268
+ if (!res.ok) {
269
+ console.warn(`[chat-bridge] receive-ack failed (${res.status}) for agent ${agentId}; delivery will replay on the next poll`);
270
+ }
271
+ } catch (err) {
272
+ console.warn(
273
+ `[chat-bridge] receive-ack errored for agent ${agentId}; delivery will replay on the next poll: ${err instanceof Error ? err.message : String(err)}`
274
+ );
275
+ }
276
+ }
277
+ function formatAttachmentSuffix(attachments) {
278
+ if (!attachments?.length) return "";
279
+ return ` [${attachments.length} attachment${attachments.length > 1 ? "s" : ""}: ${attachments.map((a) => `${a.filename} (id:${a.id})`).join(", ")} \u2014 use view_file to download]`;
280
+ }
281
+ function guessMimeTypeFromFilename(filename) {
282
+ const ext = filename.includes(".") ? filename.slice(filename.lastIndexOf(".")).toLowerCase() : "";
283
+ const mimeMap = {
284
+ ".jpg": "image/jpeg",
285
+ ".jpeg": "image/jpeg",
286
+ ".png": "image/png",
287
+ ".gif": "image/gif",
288
+ ".webp": "image/webp",
289
+ ".pdf": "application/pdf",
290
+ ".txt": "text/plain",
291
+ ".md": "text/markdown",
292
+ ".json": "application/json",
293
+ ".csv": "text/csv",
294
+ ".zip": "application/zip",
295
+ ".tar": "application/x-tar",
296
+ ".gz": "application/gzip",
297
+ ".mp3": "audio/mpeg",
298
+ ".wav": "audio/wav",
299
+ ".mp4": "video/mp4",
300
+ ".mov": "video/quicktime",
301
+ ".doc": "application/msword",
302
+ ".docx": "application/vnd.openxmlformats-officedocument.wordprocessingml.document",
303
+ ".xls": "application/vnd.ms-excel",
304
+ ".xlsx": "application/vnd.openxmlformats-officedocument.spreadsheetml.sheet",
305
+ ".ppt": "application/vnd.ms-powerpoint",
306
+ ".pptx": "application/vnd.openxmlformats-officedocument.presentationml.presentation"
307
+ };
308
+ return mimeMap[ext] || "application/octet-stream";
309
+ }
310
+ function parseFilenameFromContentDisposition(contentDisposition) {
311
+ if (!contentDisposition) return null;
312
+ const utf8Match = contentDisposition.match(/filename\*\s*=\s*UTF-8''([^;]+)/i);
313
+ if (utf8Match?.[1]) return decodeURIComponent(utf8Match[1]);
314
+ const plainMatch = contentDisposition.match(/filename\s*=\s*"([^"]+)"/i) || contentDisposition.match(/filename\s*=\s*([^;]+)/i);
315
+ return plainMatch?.[1] ? plainMatch[1].trim() : null;
316
+ }
317
+ function extensionForContentType(contentType) {
318
+ const normalized = contentType.split(";")[0]?.trim().toLowerCase();
319
+ const extMap = {
320
+ "image/jpeg": ".jpg",
321
+ "image/png": ".png",
322
+ "image/gif": ".gif",
323
+ "image/webp": ".webp",
324
+ "application/pdf": ".pdf",
325
+ "text/plain": ".txt",
326
+ "text/markdown": ".md",
327
+ "application/json": ".json",
328
+ "text/csv": ".csv",
329
+ "application/zip": ".zip",
330
+ "application/x-tar": ".tar",
331
+ "application/gzip": ".gz",
332
+ "audio/mpeg": ".mp3",
333
+ "audio/wav": ".wav",
334
+ "video/mp4": ".mp4",
335
+ "video/quicktime": ".mov"
336
+ };
337
+ return extMap[normalized] || ".bin";
338
+ }
38
339
  function formatTarget(m) {
39
340
  if (m.channel_type === "thread" && m.parent_channel_name) {
40
341
  const shortId = m.channel_name.startsWith("thread-") ? m.channel_name.slice(7) : m.channel_name;
@@ -61,6 +362,11 @@ function formatSearchTarget(result) {
61
362
  }
62
363
  return `#${result.channelName}`;
63
364
  }
365
+ function formatSenderHandle(message) {
366
+ const senderName = message.sender_name ?? message.senderName ?? "unknown";
367
+ const senderDescription = message.sender_description ?? message.senderDescription ?? null;
368
+ return senderDescription ? `@${senderName} \u2014 ${senderDescription}` : `@${senderName}`;
369
+ }
64
370
  var server = new McpServer({
65
371
  name: "chat",
66
372
  version: "1.0.0"
@@ -77,12 +383,18 @@ server.tool(
77
383
  },
78
384
  async ({ target, content, attachment_ids }) => {
79
385
  try {
80
- const res = await bridgeFetch(`${serverUrl}/internal/agent/${agentId}/send`, {
81
- method: "POST",
82
- headers: commonHeaders,
83
- body: JSON.stringify({ target, content, attachmentIds: attachment_ids })
84
- });
85
- const data = await res.json();
386
+ const { response: res, data } = await executeRetrySafeSendRequest(
387
+ `${serverUrl}/internal/agent/${agentId}/send`,
388
+ (idempotencyKey) => ({
389
+ method: "POST",
390
+ headers: commonHeaders,
391
+ body: JSON.stringify({ target, content, attachmentIds: attachment_ids, idempotencyKey })
392
+ }),
393
+ {
394
+ target,
395
+ fetchImpl: bridgeFetch
396
+ }
397
+ );
86
398
  if (!res.ok) {
87
399
  return {
88
400
  content: [
@@ -94,10 +406,14 @@ server.tool(
94
406
  const replyHint = shortId ? ` (to reply in this message's thread, use target "${target.includes(":") ? target : target + ":" + shortId}")` : "";
95
407
  let unreadSection = "";
96
408
  if (data.recentUnread && data.recentUnread.length > 0) {
97
- unreadSection = `
409
+ await acknowledgeReceivedMessages(data.recentUnread);
410
+ const unreadToShow = rememberDeliveredMessages(data.recentUnread);
411
+ if (unreadToShow.length > 0) {
412
+ unreadSection = `
98
413
 
99
414
  --- New messages you may have missed ---
100
- ${formatMessages(data.recentUnread)}`;
415
+ ${formatMessages(unreadToShow)}`;
416
+ }
101
417
  }
102
418
  return {
103
419
  content: [
@@ -117,9 +433,9 @@ ${formatMessages(data.recentUnread)}`;
117
433
  );
118
434
  server.tool(
119
435
  "upload_file",
120
- "Upload an image file to attach to a message. Returns an attachment ID that you can pass to send_message's attachment_ids parameter. Supported formats: JPEG, PNG, GIF, WebP. Max size: 5MB.",
436
+ "Upload a file to attach to a message. Returns an attachment ID that you can pass to send_message's attachment_ids parameter. Images keep preview behavior; other files are sent as downloadable attachments. Max size: 10MB.",
121
437
  {
122
- file_path: z.string().describe("Absolute path to the image file on your local filesystem"),
438
+ file_path: z.string().describe("Absolute path to the file on your local filesystem"),
123
439
  channel: z.string().describe("The channel target where this file will be used (e.g. '#general', 'dm:@richard')")
124
440
  },
125
441
  async ({ file_path, channel }) => {
@@ -133,52 +449,53 @@ server.tool(
133
449
  };
134
450
  }
135
451
  const stat = fs.statSync(file_path);
136
- if (stat.size > 5 * 1024 * 1024) {
452
+ if (stat.size > 10 * 1024 * 1024) {
137
453
  return {
138
454
  isError: true,
139
- content: [{ type: "text", text: `Error: File too large (${(stat.size / 1024 / 1024).toFixed(1)}MB). Max 5MB.` }]
455
+ content: [{ type: "text", text: `Error: File too large (${(stat.size / 1024 / 1024).toFixed(1)}MB). Max 10MB per file.` }]
140
456
  };
141
457
  }
142
- const listRes = await bridgeFetch(`${serverUrl}/internal/agent/${agentId}/resolve-channel`, {
143
- method: "POST",
144
- headers: commonHeaders,
145
- body: JSON.stringify({ target: channel })
146
- });
147
- let channelId;
148
- if (listRes.ok) {
149
- const listData = await listRes.json();
150
- channelId = listData.channelId;
151
- } else {
458
+ const { response: listRes, data: listData } = await executeJsonRequest(
459
+ `${serverUrl}/internal/agent/${agentId}/resolve-channel`,
460
+ {
461
+ method: "POST",
462
+ headers: commonHeaders,
463
+ body: JSON.stringify({ target: channel })
464
+ },
465
+ {
466
+ toolName: "upload_file.resolve_channel",
467
+ target: channel,
468
+ fetchImpl: bridgeFetch
469
+ }
470
+ );
471
+ if (!listRes.ok || !listData.channelId) {
152
472
  return {
153
473
  isError: true,
154
- content: [{ type: "text", text: `Error: Could not resolve channel: ${channel}` }]
474
+ content: [{ type: "text", text: `Error: ${listData.error || `Could not resolve channel: ${channel}`}` }]
155
475
  };
156
476
  }
477
+ const channelId = listData.channelId;
157
478
  const fileBuffer = fs.readFileSync(file_path);
158
479
  const filename = path.basename(file_path);
159
- const ext = path.extname(file_path).toLowerCase();
160
- const mimeMap = {
161
- ".jpg": "image/jpeg",
162
- ".jpeg": "image/jpeg",
163
- ".png": "image/png",
164
- ".gif": "image/gif",
165
- ".webp": "image/webp"
166
- };
167
- const mimeType = mimeMap[ext] || "application/octet-stream";
480
+ const mimeType = guessMimeTypeFromFilename(filename);
168
481
  const blob = new Blob([fileBuffer], { type: mimeType });
169
482
  const formData = new FormData();
170
483
  formData.append("file", blob, filename);
171
484
  formData.append("channelId", channelId);
172
- const uploadHeaders = {};
173
- if (authToken) {
174
- uploadHeaders["Authorization"] = `Bearer ${authToken}`;
175
- }
176
- const res = await bridgeFetch(`${serverUrl}/internal/agent/${agentId}/upload`, {
177
- method: "POST",
178
- headers: uploadHeaders,
179
- body: formData
180
- });
181
- const data = await res.json();
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
+ );
182
499
  if (!res.ok) {
183
500
  return {
184
501
  isError: true,
@@ -206,7 +523,7 @@ Use this ID in send_message's attachment_ids parameter to include it in a messag
206
523
  );
207
524
  server.tool(
208
525
  "view_file",
209
- "Download an attached image by its attachment ID and save it locally so you can view it. Returns the local file path. Use this when you see '[use view_file to see]' in a message with images.",
526
+ "Download an attached file by its attachment ID and save it locally so you can inspect it. Returns the local file path.",
210
527
  {
211
528
  attachment_id: z.string().describe("The attachment UUID (from the 'id:...' shown in the message)")
212
529
  },
@@ -221,19 +538,22 @@ server.tool(
221
538
  if (existing) {
222
539
  const cachedPath = path.join(cacheDir, existing);
223
540
  return {
224
- content: [{ type: "text", text: `File already cached at: ${cachedPath}
225
-
226
- Use your Read tool to view this image.` }]
541
+ content: [{ type: "text", text: `File already cached at: ${cachedPath}` }]
227
542
  };
228
543
  }
229
- const downloadHeaders = {};
230
- if (authToken) {
231
- downloadHeaders["Authorization"] = `Bearer ${authToken}`;
232
- }
233
- const res = await bridgeFetch(`${serverUrl}/api/attachments/${attachment_id}`, {
234
- headers: downloadHeaders,
235
- redirect: "follow"
236
- });
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
+ );
237
557
  if (!res.ok) {
238
558
  return {
239
559
  isError: true,
@@ -241,20 +561,13 @@ Use your Read tool to view this image.` }]
241
561
  };
242
562
  }
243
563
  const contentType = res.headers.get("content-type") || "application/octet-stream";
244
- const extMap = {
245
- "image/jpeg": ".jpg",
246
- "image/png": ".png",
247
- "image/gif": ".gif",
248
- "image/webp": ".webp"
249
- };
250
- const ext = extMap[contentType] || ".bin";
564
+ const filename = parseFilenameFromContentDisposition(res.headers.get("content-disposition"));
565
+ const ext = filename ? path.extname(filename) || extensionForContentType(contentType) : extensionForContentType(contentType);
251
566
  const filePath = path.join(cacheDir, `${attachment_id}${ext}`);
252
567
  const buffer = Buffer.from(await res.arrayBuffer());
253
568
  fs.writeFileSync(filePath, buffer);
254
569
  return {
255
- content: [{ type: "text", text: `Downloaded to: ${filePath}
256
-
257
- Use your Read tool to view this image.` }]
570
+ content: [{ type: "text", text: `Downloaded to: ${filePath}` }]
258
571
  };
259
572
  } catch (err) {
260
573
  return {
@@ -270,17 +583,25 @@ server.tool(
270
583
  {},
271
584
  async () => {
272
585
  try {
273
- const res = await bridgeFetch(
586
+ const { response: res, data } = await executeJsonRequest(
274
587
  `${serverUrl}/internal/agent/${agentId}/receive`,
275
- { method: "GET", headers: commonHeaders }
588
+ { method: "GET", headers: commonHeaders },
589
+ {
590
+ toolName: "check_messages",
591
+ timeoutMs: 1e4,
592
+ fetchImpl: bridgeFetch
593
+ }
276
594
  );
277
595
  if (!res.ok) {
278
- const errData = await res.json().catch(() => ({}));
279
- return { isError: true, content: [{ type: "text", text: `Error: ${errData.error || res.statusText}` }] };
596
+ return { isError: true, content: [{ type: "text", text: `Error: ${data.error || res.statusText}` }] };
280
597
  }
281
- const data = await res.json();
282
- if (data.messages?.length > 0) {
283
- return { content: [{ type: "text", text: formatMessages(data.messages) }] };
598
+ const messages = data.messages ?? [];
599
+ if (messages.length > 0) {
600
+ await acknowledgeReceivedMessages(messages);
601
+ const messagesToShow = rememberDeliveredMessages(messages);
602
+ if (messagesToShow.length > 0) {
603
+ return { content: [{ type: "text", text: formatMessages(messagesToShow) }] };
604
+ }
284
605
  }
285
606
  return {
286
607
  content: [{ type: "text", text: "No new messages." }]
@@ -298,11 +619,11 @@ function formatMessages(messages) {
298
619
  const target = formatTarget(m);
299
620
  const msgId = m.message_id ? m.message_id.slice(0, 8) : "-";
300
621
  const time = m.timestamp ? toLocalTime(m.timestamp) : "-";
301
- const senderType = m.sender_type === "agent" ? " type=agent" : "";
622
+ const senderType = ` type=${m.sender_type}`;
302
623
  const renderedContent = m.content;
303
- const attachSuffix = m.attachments?.length ? ` [${m.attachments.length} image${m.attachments.length > 1 ? "s" : ""}: ${m.attachments.map((a) => `${a.filename} (id:${a.id})`).join(", ")} \u2014 use view_file to see]` : "";
624
+ const attachSuffix = formatAttachmentSuffix(m.attachments);
304
625
  const taskSuffix = m.task_status ? ` [task #${m.task_number} status=${m.task_status}${m.task_assignee_id ? ` assignee=${m.task_assignee_type}:${m.task_assignee_id}` : ""}]` : "";
305
- return `[target=${target} msg=${msgId} time=${time}${senderType}] @${m.sender_name}: ${renderedContent}${attachSuffix}${taskSuffix}`;
626
+ return `[target=${target} msg=${msgId} time=${time}${senderType}] ${formatSenderHandle(m)}: ${renderedContent}${attachSuffix}${taskSuffix}`;
306
627
  }).join("\n");
307
628
  }
308
629
  server.tool(
@@ -311,16 +632,22 @@ server.tool(
311
632
  {},
312
633
  async () => {
313
634
  try {
314
- const res = await bridgeFetch(
635
+ const { response: res, data } = await executeJsonRequest(
315
636
  `${serverUrl}/internal/agent/${agentId}/server`,
316
- { method: "GET", headers: commonHeaders }
637
+ { method: "GET", headers: commonHeaders },
638
+ {
639
+ toolName: "list_server",
640
+ fetchImpl: bridgeFetch
641
+ }
317
642
  );
318
- const data = await res.json();
319
643
  let text = "## Server\n\n";
644
+ const channels = data.channels ?? [];
645
+ const agents = data.agents ?? [];
646
+ const humans = data.humans ?? [];
320
647
  text += "### Channels\n";
321
648
  text += "Use `#channel-name` with send_message to post in a channel. `joined` means you currently belong to that channel.\n";
322
- if (data.channels?.length > 0) {
323
- for (const t of data.channels) {
649
+ if (channels.length > 0) {
650
+ for (const t of channels) {
324
651
  const status = t.joined ? "joined" : "not joined";
325
652
  text += t.description ? ` - #${t.name} [${status}] \u2014 ${t.description}
326
653
  ` : ` - #${t.name} [${status}]
@@ -331,9 +658,10 @@ server.tool(
331
658
  }
332
659
  text += "\n### Agents\n";
333
660
  text += "Other AI agents in this server.\n";
334
- if (data.agents?.length > 0) {
335
- for (const a of data.agents) {
336
- text += ` - @${a.name} (${a.status})
661
+ if (agents.length > 0) {
662
+ for (const a of agents) {
663
+ text += a.description ? ` - @${a.name} (${a.status}) \u2014 ${a.description}
664
+ ` : ` - @${a.name} (${a.status})
337
665
  `;
338
666
  }
339
667
  } else {
@@ -341,9 +669,10 @@ server.tool(
341
669
  }
342
670
  text += "\n### Humans\n";
343
671
  text += 'To start a new DM: send_message(target="dm:@name"). To reply in an existing DM: reuse the target from received messages.\n';
344
- if (data.humans?.length > 0) {
345
- for (const u of data.humans) {
346
- text += ` - @${u.name}
672
+ if (humans.length > 0) {
673
+ for (const u of humans) {
674
+ text += u.description ? ` - @${u.name} \u2014 ${u.description}
675
+ ` : ` - @${u.name}
347
676
  `;
348
677
  }
349
678
  } else {
@@ -386,11 +715,15 @@ server.tool(
386
715
  if (sender_id) params.set("senderId", sender_id);
387
716
  if (after) params.set("after", after);
388
717
  if (before) params.set("before", before);
389
- const res = await bridgeFetch(
718
+ const { response: res, data } = await executeJsonRequest(
390
719
  `${serverUrl}/internal/agent/${agentId}/search?${params}`,
391
- { method: "GET", headers: commonHeaders }
720
+ { method: "GET", headers: commonHeaders },
721
+ {
722
+ toolName: "search_messages",
723
+ target: channel ?? null,
724
+ fetchImpl: bridgeFetch
725
+ }
392
726
  );
393
- const data = await res.json();
394
727
  if (!res.ok) {
395
728
  return {
396
729
  content: [{ type: "text", text: `Error: ${data.error}` }]
@@ -408,7 +741,7 @@ thread: ${result.parentChannelName} -> ${target}` : "";
408
741
  return [
409
742
  `[${index + 1}] msg=${result.id} seq=${result.seq} time=${toLocalTime(result.createdAt)}`,
410
743
  `target: ${target}${threadInfo}`,
411
- `sender: @${result.senderName}${result.senderType === "agent" ? " (agent)" : ""}`,
744
+ `sender: @${result.senderName} (${result.senderType})`,
412
745
  `content: ${result.content}`,
413
746
  `match: ${result.snippet}`,
414
747
  `next: read_history(channel="${target}", around="${result.id}", limit=20)`
@@ -448,11 +781,15 @@ server.tool(
448
781
  if (around !== void 0) params.set("around", String(around));
449
782
  if (before) params.set("before", String(before));
450
783
  if (after) params.set("after", String(after));
451
- const res = await bridgeFetch(
784
+ const { response: res, data } = await executeJsonRequest(
452
785
  `${serverUrl}/internal/agent/${agentId}/history?${params}`,
453
- { method: "GET", headers: commonHeaders }
786
+ { method: "GET", headers: commonHeaders },
787
+ {
788
+ toolName: "read_history",
789
+ target: channel,
790
+ fetchImpl: bridgeFetch
791
+ }
454
792
  );
455
- const data = await res.json();
456
793
  if (!res.ok) {
457
794
  return {
458
795
  content: [
@@ -467,15 +804,11 @@ server.tool(
467
804
  ]
468
805
  };
469
806
  }
470
- const formatted = data.messages.map((m) => {
471
- const senderType = m.senderType === "agent" ? " type=agent" : "";
472
- const time = m.createdAt ? toLocalTime(m.createdAt) : "-";
473
- const msgId = m.id || "-";
474
- const renderedContent = m.content;
475
- const attachSuffix = m.attachments?.length ? ` [${m.attachments.length} image${m.attachments.length > 1 ? "s" : ""}: ${m.attachments.map((a) => `${a.filename} (id:${a.id})`).join(", ")} \u2014 use view_file to see]` : "";
476
- const taskSuffix = m.taskStatus ? ` [task #${m.taskNumber} status=${m.taskStatus}${m.taskAssigneeId ? ` assignee=${m.taskAssigneeType}:${m.taskAssigneeId}` : ""}]` : "";
477
- return `[seq=${m.seq} msg=${msgId} time=${time}${senderType}] @${m.senderName}: ${renderedContent}${attachSuffix}${taskSuffix}`;
478
- }).join("\n");
807
+ const formatted = data.messages.map((m) => formatHistoryMessageLine({
808
+ ...m,
809
+ senderName: m.senderName ?? m.sender_name ?? "unknown",
810
+ senderDescription: m.senderDescription ?? m.sender_description ?? null
811
+ })).join("\n");
479
812
  let footer = "";
480
813
  if (data.historyLimited) {
481
814
  footer = `
@@ -501,7 +834,7 @@ server.tool(
501
834
  }
502
835
  }
503
836
  let header = `## Message History for ${channel}${around ? ` around ${around}` : ""} (${data.messages.length} messages)`;
504
- if (data.last_read_seq > 0 && !after && !before && !around) {
837
+ if ((data.last_read_seq ?? 0) > 0 && !after && !before && !around) {
505
838
  header += `
506
839
  Your last read position: seq ${data.last_read_seq}. Use read_history(channel="${channel}", after=${data.last_read_seq}) to see only unread messages.`;
507
840
  }
@@ -535,11 +868,15 @@ server.tool(
535
868
  const params = new URLSearchParams();
536
869
  params.set("channel", channel);
537
870
  if (status !== "all") params.set("status", status);
538
- const res = await bridgeFetch(
871
+ const { response: res, data } = await executeJsonRequest(
539
872
  `${serverUrl}/internal/agent/${agentId}/tasks?${params}`,
540
- { method: "GET", headers: commonHeaders }
873
+ { method: "GET", headers: commonHeaders },
874
+ {
875
+ toolName: "list_tasks",
876
+ target: channel,
877
+ fetchImpl: bridgeFetch
878
+ }
541
879
  );
542
- const data = await res.json();
543
880
  if (!res.ok) {
544
881
  return {
545
882
  isError: true,
@@ -589,12 +926,19 @@ server.tool(
589
926
  },
590
927
  async ({ channel, tasks }) => {
591
928
  try {
592
- const res = await bridgeFetch(`${serverUrl}/internal/agent/${agentId}/tasks`, {
593
- method: "POST",
594
- headers: commonHeaders,
595
- body: JSON.stringify({ channel, tasks })
596
- });
597
- const data = await res.json();
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
940
+ }
941
+ );
598
942
  if (!res.ok) {
599
943
  return {
600
944
  isError: true,
@@ -645,15 +989,19 @@ Thread messages cannot be claimed or converted into tasks. If a task is in "todo
645
989
  const body = { channel };
646
990
  if (task_numbers && task_numbers.length > 0) body.task_numbers = task_numbers;
647
991
  if (message_ids && message_ids.length > 0) body.message_ids = message_ids;
648
- const res = await bridgeFetch(
992
+ const { response: res, data } = await executeJsonRequest(
649
993
  `${serverUrl}/internal/agent/${agentId}/tasks/claim`,
650
994
  {
651
995
  method: "POST",
652
996
  headers: commonHeaders,
653
997
  body: JSON.stringify(body)
998
+ },
999
+ {
1000
+ toolName: "claim_tasks",
1001
+ target: channel,
1002
+ fetchImpl: bridgeFetch
654
1003
  }
655
1004
  );
656
- const data = await res.json();
657
1005
  if (!res.ok) {
658
1006
  return {
659
1007
  isError: true,
@@ -703,15 +1051,19 @@ server.tool(
703
1051
  },
704
1052
  async ({ channel, task_number }) => {
705
1053
  try {
706
- const res = await bridgeFetch(
1054
+ const { response: res, data } = await executeJsonRequest(
707
1055
  `${serverUrl}/internal/agent/${agentId}/tasks/unclaim`,
708
1056
  {
709
1057
  method: "POST",
710
1058
  headers: commonHeaders,
711
1059
  body: JSON.stringify({ channel, task_number })
1060
+ },
1061
+ {
1062
+ toolName: "unclaim_task",
1063
+ target: channel,
1064
+ fetchImpl: bridgeFetch
712
1065
  }
713
1066
  );
714
- const data = await res.json();
715
1067
  if (!res.ok) {
716
1068
  return {
717
1069
  isError: true,
@@ -741,15 +1093,19 @@ server.tool(
741
1093
  },
742
1094
  async ({ channel, task_number, status }) => {
743
1095
  try {
744
- const res = await bridgeFetch(
1096
+ const { response: res, data } = await executeJsonRequest(
745
1097
  `${serverUrl}/internal/agent/${agentId}/tasks/update-status`,
746
1098
  {
747
1099
  method: "POST",
748
1100
  headers: commonHeaders,
749
1101
  body: JSON.stringify({ channel, task_number, status })
1102
+ },
1103
+ {
1104
+ toolName: "update_task_status",
1105
+ target: channel,
1106
+ fetchImpl: bridgeFetch
750
1107
  }
751
1108
  );
752
- const data = await res.json();
753
1109
  if (!res.ok) {
754
1110
  return {
755
1111
  isError: true,