@slock-ai/daemon 0.33.0 → 0.35.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.
@@ -35,6 +35,68 @@ function bridgeFetch(url, init = {}) {
35
35
  const requestInit = dispatcher ? { ...init, dispatcher } : init;
36
36
  return fetch(url, requestInit);
37
37
  }
38
+ function formatAttachmentSuffix(attachments) {
39
+ if (!attachments?.length) return "";
40
+ return ` [${attachments.length} attachment${attachments.length > 1 ? "s" : ""}: ${attachments.map((a) => `${a.filename} (id:${a.id})`).join(", ")} \u2014 use view_file to download]`;
41
+ }
42
+ function guessMimeTypeFromFilename(filename) {
43
+ const ext = filename.includes(".") ? filename.slice(filename.lastIndexOf(".")).toLowerCase() : "";
44
+ const mimeMap = {
45
+ ".jpg": "image/jpeg",
46
+ ".jpeg": "image/jpeg",
47
+ ".png": "image/png",
48
+ ".gif": "image/gif",
49
+ ".webp": "image/webp",
50
+ ".pdf": "application/pdf",
51
+ ".txt": "text/plain",
52
+ ".md": "text/markdown",
53
+ ".json": "application/json",
54
+ ".csv": "text/csv",
55
+ ".zip": "application/zip",
56
+ ".tar": "application/x-tar",
57
+ ".gz": "application/gzip",
58
+ ".mp3": "audio/mpeg",
59
+ ".wav": "audio/wav",
60
+ ".mp4": "video/mp4",
61
+ ".mov": "video/quicktime",
62
+ ".doc": "application/msword",
63
+ ".docx": "application/vnd.openxmlformats-officedocument.wordprocessingml.document",
64
+ ".xls": "application/vnd.ms-excel",
65
+ ".xlsx": "application/vnd.openxmlformats-officedocument.spreadsheetml.sheet",
66
+ ".ppt": "application/vnd.ms-powerpoint",
67
+ ".pptx": "application/vnd.openxmlformats-officedocument.presentationml.presentation"
68
+ };
69
+ return mimeMap[ext] || "application/octet-stream";
70
+ }
71
+ function parseFilenameFromContentDisposition(contentDisposition) {
72
+ if (!contentDisposition) return null;
73
+ const utf8Match = contentDisposition.match(/filename\*\s*=\s*UTF-8''([^;]+)/i);
74
+ if (utf8Match?.[1]) return decodeURIComponent(utf8Match[1]);
75
+ const plainMatch = contentDisposition.match(/filename\s*=\s*"([^"]+)"/i) || contentDisposition.match(/filename\s*=\s*([^;]+)/i);
76
+ return plainMatch?.[1] ? plainMatch[1].trim() : null;
77
+ }
78
+ function extensionForContentType(contentType) {
79
+ const normalized = contentType.split(";")[0]?.trim().toLowerCase();
80
+ const extMap = {
81
+ "image/jpeg": ".jpg",
82
+ "image/png": ".png",
83
+ "image/gif": ".gif",
84
+ "image/webp": ".webp",
85
+ "application/pdf": ".pdf",
86
+ "text/plain": ".txt",
87
+ "text/markdown": ".md",
88
+ "application/json": ".json",
89
+ "text/csv": ".csv",
90
+ "application/zip": ".zip",
91
+ "application/x-tar": ".tar",
92
+ "application/gzip": ".gz",
93
+ "audio/mpeg": ".mp3",
94
+ "audio/wav": ".wav",
95
+ "video/mp4": ".mp4",
96
+ "video/quicktime": ".mov"
97
+ };
98
+ return extMap[normalized] || ".bin";
99
+ }
38
100
  function formatTarget(m) {
39
101
  if (m.channel_type === "thread" && m.parent_channel_name) {
40
102
  const shortId = m.channel_name.startsWith("thread-") ? m.channel_name.slice(7) : m.channel_name;
@@ -61,6 +123,11 @@ function formatSearchTarget(result) {
61
123
  }
62
124
  return `#${result.channelName}`;
63
125
  }
126
+ function formatSenderHandle(message) {
127
+ const senderName = message.sender_name ?? message.senderName ?? "unknown";
128
+ const senderDescription = message.sender_description ?? message.senderDescription ?? null;
129
+ return senderDescription ? `@${senderName} \u2014 ${senderDescription}` : `@${senderName}`;
130
+ }
64
131
  var server = new McpServer({
65
132
  name: "chat",
66
133
  version: "1.0.0"
@@ -117,9 +184,9 @@ ${formatMessages(data.recentUnread)}`;
117
184
  );
118
185
  server.tool(
119
186
  "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.",
187
+ "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: 5MB.",
121
188
  {
122
- file_path: z.string().describe("Absolute path to the image file on your local filesystem"),
189
+ file_path: z.string().describe("Absolute path to the file on your local filesystem"),
123
190
  channel: z.string().describe("The channel target where this file will be used (e.g. '#general', 'dm:@richard')")
124
191
  },
125
192
  async ({ file_path, channel }) => {
@@ -136,7 +203,7 @@ server.tool(
136
203
  if (stat.size > 5 * 1024 * 1024) {
137
204
  return {
138
205
  isError: true,
139
- content: [{ type: "text", text: `Error: File too large (${(stat.size / 1024 / 1024).toFixed(1)}MB). Max 5MB.` }]
206
+ content: [{ type: "text", text: `Error: File too large (${(stat.size / 1024 / 1024).toFixed(1)}MB). Max 5MB per file.` }]
140
207
  };
141
208
  }
142
209
  const listRes = await bridgeFetch(`${serverUrl}/internal/agent/${agentId}/resolve-channel`, {
@@ -156,15 +223,7 @@ server.tool(
156
223
  }
157
224
  const fileBuffer = fs.readFileSync(file_path);
158
225
  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";
226
+ const mimeType = guessMimeTypeFromFilename(filename);
168
227
  const blob = new Blob([fileBuffer], { type: mimeType });
169
228
  const formData = new FormData();
170
229
  formData.append("file", blob, filename);
@@ -206,7 +265,7 @@ Use this ID in send_message's attachment_ids parameter to include it in a messag
206
265
  );
207
266
  server.tool(
208
267
  "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.",
268
+ "Download an attached file by its attachment ID and save it locally so you can inspect it. Returns the local file path.",
210
269
  {
211
270
  attachment_id: z.string().describe("The attachment UUID (from the 'id:...' shown in the message)")
212
271
  },
@@ -221,9 +280,7 @@ server.tool(
221
280
  if (existing) {
222
281
  const cachedPath = path.join(cacheDir, existing);
223
282
  return {
224
- content: [{ type: "text", text: `File already cached at: ${cachedPath}
225
-
226
- Use your Read tool to view this image.` }]
283
+ content: [{ type: "text", text: `File already cached at: ${cachedPath}` }]
227
284
  };
228
285
  }
229
286
  const downloadHeaders = {};
@@ -241,20 +298,13 @@ Use your Read tool to view this image.` }]
241
298
  };
242
299
  }
243
300
  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";
301
+ const filename = parseFilenameFromContentDisposition(res.headers.get("content-disposition"));
302
+ const ext = filename ? path.extname(filename) || extensionForContentType(contentType) : extensionForContentType(contentType);
251
303
  const filePath = path.join(cacheDir, `${attachment_id}${ext}`);
252
304
  const buffer = Buffer.from(await res.arrayBuffer());
253
305
  fs.writeFileSync(filePath, buffer);
254
306
  return {
255
- content: [{ type: "text", text: `Downloaded to: ${filePath}
256
-
257
- Use your Read tool to view this image.` }]
307
+ content: [{ type: "text", text: `Downloaded to: ${filePath}` }]
258
308
  };
259
309
  } catch (err) {
260
310
  return {
@@ -298,11 +348,11 @@ function formatMessages(messages) {
298
348
  const target = formatTarget(m);
299
349
  const msgId = m.message_id ? m.message_id.slice(0, 8) : "-";
300
350
  const time = m.timestamp ? toLocalTime(m.timestamp) : "-";
301
- const senderType = m.sender_type === "agent" ? " type=agent" : "";
351
+ const senderType = ` type=${m.sender_type}`;
302
352
  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]` : "";
353
+ const attachSuffix = formatAttachmentSuffix(m.attachments);
304
354
  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}`;
355
+ return `[target=${target} msg=${msgId} time=${time}${senderType}] ${formatSenderHandle(m)}: ${renderedContent}${attachSuffix}${taskSuffix}`;
306
356
  }).join("\n");
307
357
  }
308
358
  server.tool(
@@ -333,7 +383,8 @@ server.tool(
333
383
  text += "Other AI agents in this server.\n";
334
384
  if (data.agents?.length > 0) {
335
385
  for (const a of data.agents) {
336
- text += ` - @${a.name} (${a.status})
386
+ text += a.description ? ` - @${a.name} (${a.status}) \u2014 ${a.description}
387
+ ` : ` - @${a.name} (${a.status})
337
388
  `;
338
389
  }
339
390
  } else {
@@ -343,7 +394,8 @@ server.tool(
343
394
  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
395
  if (data.humans?.length > 0) {
345
396
  for (const u of data.humans) {
346
- text += ` - @${u.name}
397
+ text += u.description ? ` - @${u.name} \u2014 ${u.description}
398
+ ` : ` - @${u.name}
347
399
  `;
348
400
  }
349
401
  } else {
@@ -408,7 +460,7 @@ thread: ${result.parentChannelName} -> ${target}` : "";
408
460
  return [
409
461
  `[${index + 1}] msg=${result.id} seq=${result.seq} time=${toLocalTime(result.createdAt)}`,
410
462
  `target: ${target}${threadInfo}`,
411
- `sender: @${result.senderName}${result.senderType === "agent" ? " (agent)" : ""}`,
463
+ `sender: @${result.senderName} (${result.senderType})`,
412
464
  `content: ${result.content}`,
413
465
  `match: ${result.snippet}`,
414
466
  `next: read_history(channel="${target}", around="${result.id}", limit=20)`
@@ -468,13 +520,13 @@ server.tool(
468
520
  };
469
521
  }
470
522
  const formatted = data.messages.map((m) => {
471
- const senderType = m.senderType === "agent" ? " type=agent" : "";
523
+ const senderType = ` type=${m.senderType}`;
472
524
  const time = m.createdAt ? toLocalTime(m.createdAt) : "-";
473
525
  const msgId = m.id || "-";
474
526
  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]` : "";
527
+ const attachSuffix = formatAttachmentSuffix(m.attachments);
476
528
  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}`;
529
+ return `[seq=${m.seq} msg=${msgId} time=${time}${senderType}] ${formatSenderHandle(m)}: ${renderedContent}${attachSuffix}${taskSuffix}`;
478
530
  }).join("\n");
479
531
  let footer = "";
480
532
  if (data.historyLimited) {