@slock-ai/daemon 0.14.0 → 0.16.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.
@@ -51,14 +51,15 @@ server.tool(
51
51
  target: z.string().describe(
52
52
  "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'."
53
53
  ),
54
- content: z.string().describe("The message content")
54
+ content: z.string().describe("The message content"),
55
+ attachment_ids: z.array(z.string()).optional().describe("Optional attachment IDs from upload_file to include with the message")
55
56
  },
56
- async ({ target, content }) => {
57
+ async ({ target, content, attachment_ids }) => {
57
58
  try {
58
59
  const res = await fetch(`${serverUrl}/internal/agent/${agentId}/send`, {
59
60
  method: "POST",
60
61
  headers: commonHeaders,
61
- body: JSON.stringify({ target, content })
62
+ body: JSON.stringify({ target, content, attachmentIds: attachment_ids })
62
63
  });
63
64
  const data = await res.json();
64
65
  if (!res.ok) {
@@ -85,6 +86,148 @@ server.tool(
85
86
  }
86
87
  }
87
88
  );
89
+ server.tool(
90
+ "upload_file",
91
+ "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.",
92
+ {
93
+ file_path: z.string().describe("Absolute path to the image file on your local filesystem"),
94
+ channel: z.string().describe("The channel target where this file will be used (e.g. '#general', 'dm:@richard')")
95
+ },
96
+ async ({ file_path, channel }) => {
97
+ try {
98
+ const fs = await import("fs");
99
+ const path = await import("path");
100
+ if (!fs.existsSync(file_path)) {
101
+ return {
102
+ content: [{ type: "text", text: `Error: File not found: ${file_path}` }]
103
+ };
104
+ }
105
+ const stat = fs.statSync(file_path);
106
+ if (stat.size > 5 * 1024 * 1024) {
107
+ return {
108
+ content: [{ type: "text", text: `Error: File too large (${(stat.size / 1024 / 1024).toFixed(1)}MB). Max 5MB.` }]
109
+ };
110
+ }
111
+ const listRes = await fetch(`${serverUrl}/internal/agent/${agentId}/resolve-channel`, {
112
+ method: "POST",
113
+ headers: commonHeaders,
114
+ body: JSON.stringify({ target: channel })
115
+ });
116
+ let channelId;
117
+ if (listRes.ok) {
118
+ const listData = await listRes.json();
119
+ channelId = listData.channelId;
120
+ } else {
121
+ return {
122
+ content: [{ type: "text", text: `Error: Could not resolve channel: ${channel}` }]
123
+ };
124
+ }
125
+ const fileBuffer = fs.readFileSync(file_path);
126
+ const filename = path.basename(file_path);
127
+ const ext = path.extname(file_path).toLowerCase();
128
+ const mimeMap = {
129
+ ".jpg": "image/jpeg",
130
+ ".jpeg": "image/jpeg",
131
+ ".png": "image/png",
132
+ ".gif": "image/gif",
133
+ ".webp": "image/webp"
134
+ };
135
+ const mimeType = mimeMap[ext] || "application/octet-stream";
136
+ const blob = new Blob([fileBuffer], { type: mimeType });
137
+ const formData = new FormData();
138
+ formData.append("file", blob, filename);
139
+ formData.append("channelId", channelId);
140
+ const uploadHeaders = {};
141
+ if (authToken) {
142
+ uploadHeaders["Authorization"] = `Bearer ${authToken}`;
143
+ }
144
+ const res = await fetch(`${serverUrl}/internal/agent/${agentId}/upload`, {
145
+ method: "POST",
146
+ headers: uploadHeaders,
147
+ body: formData
148
+ });
149
+ const data = await res.json();
150
+ if (!res.ok) {
151
+ return {
152
+ content: [{ type: "text", text: `Error: ${data.error}` }]
153
+ };
154
+ }
155
+ return {
156
+ content: [
157
+ {
158
+ type: "text",
159
+ text: `File uploaded: ${data.filename} (${(data.sizeBytes / 1024).toFixed(1)}KB)
160
+ Attachment ID: ${data.id}
161
+
162
+ Use this ID in send_message's attachment_ids parameter to include it in a message.`
163
+ }
164
+ ]
165
+ };
166
+ } catch (err) {
167
+ return {
168
+ content: [{ type: "text", text: `Error: ${err.message}` }]
169
+ };
170
+ }
171
+ }
172
+ );
173
+ server.tool(
174
+ "view_file",
175
+ "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.",
176
+ {
177
+ attachment_id: z.string().describe("The attachment UUID (from the 'id:...' shown in the message)")
178
+ },
179
+ async ({ attachment_id }) => {
180
+ try {
181
+ const fs = await import("fs");
182
+ const path = await import("path");
183
+ const os = await import("os");
184
+ const cacheDir = path.join(os.homedir(), ".slock", "attachments");
185
+ fs.mkdirSync(cacheDir, { recursive: true });
186
+ const existing = fs.readdirSync(cacheDir).find((f) => f.startsWith(attachment_id));
187
+ if (existing) {
188
+ const cachedPath = path.join(cacheDir, existing);
189
+ return {
190
+ content: [{ type: "text", text: `File already cached at: ${cachedPath}
191
+
192
+ Use your Read tool to view this image.` }]
193
+ };
194
+ }
195
+ const downloadHeaders = {};
196
+ if (authToken) {
197
+ downloadHeaders["Authorization"] = `Bearer ${authToken}`;
198
+ }
199
+ const res = await fetch(`${serverUrl}/api/attachments/${attachment_id}`, {
200
+ headers: downloadHeaders,
201
+ redirect: "follow"
202
+ });
203
+ if (!res.ok) {
204
+ return {
205
+ content: [{ type: "text", text: `Error: Failed to download attachment (${res.status})` }]
206
+ };
207
+ }
208
+ const contentType = res.headers.get("content-type") || "application/octet-stream";
209
+ const extMap = {
210
+ "image/jpeg": ".jpg",
211
+ "image/png": ".png",
212
+ "image/gif": ".gif",
213
+ "image/webp": ".webp"
214
+ };
215
+ const ext = extMap[contentType] || ".bin";
216
+ const filePath = path.join(cacheDir, `${attachment_id}${ext}`);
217
+ const buffer = Buffer.from(await res.arrayBuffer());
218
+ fs.writeFileSync(filePath, buffer);
219
+ return {
220
+ content: [{ type: "text", text: `Downloaded to: ${filePath}
221
+
222
+ Use your Read tool to view this image.` }]
223
+ };
224
+ } catch (err) {
225
+ return {
226
+ content: [{ type: "text", text: `Error: ${err.message}` }]
227
+ };
228
+ }
229
+ }
230
+ );
88
231
  server.tool(
89
232
  "receive_message",
90
233
  "Receive new messages. Use block=true to wait for new messages. Returns messages formatted as [#channel], [dm:@peer], or [thread:#channel:id] followed by the sender and content.",
@@ -112,7 +255,8 @@ server.tool(
112
255
  const msgId = m.message_id ? m.message_id.slice(0, 8) : "-";
113
256
  const time = m.timestamp ? toLocalTime(m.timestamp) : "-";
114
257
  const senderType = m.sender_type === "agent" ? " type=agent" : "";
115
- return `[target=${target} msg=${msgId} time=${time}${senderType}] @${m.sender_name}: ${m.content}`;
258
+ 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]` : "";
259
+ return `[target=${target} msg=${msgId} time=${time}${senderType}] @${m.sender_name}: ${m.content}${attachSuffix}`;
116
260
  }).join("\n");
117
261
  return {
118
262
  content: [{ type: "text", text: formatted }]
@@ -217,7 +361,8 @@ server.tool(
217
361
  const senderType = m.senderType === "agent" ? " type=agent" : "";
218
362
  const time = m.createdAt ? toLocalTime(m.createdAt) : "-";
219
363
  const msgId = m.id ? m.id.slice(0, 8) : "-";
220
- return `[seq=${m.seq} msg=${msgId} time=${time}${senderType}] @${m.senderName}: ${m.content}`;
364
+ 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]` : "";
365
+ return `[seq=${m.seq} msg=${msgId} time=${time}${senderType}] @${m.senderName}: ${m.content}${attachSuffix}`;
221
366
  }).join("\n");
222
367
  let footer = "";
223
368
  if (data.historyLimited) {
package/dist/index.js CHANGED
@@ -129,6 +129,7 @@ You have MCP tools from the "chat" server. Use ONLY these for communication:
129
129
  7. **${t("claim_tasks")}** \u2014 Claim tasks by number (supports batch, handles conflicts).
130
130
  8. **${t("unclaim_task")}** \u2014 Release your claim on a task.
131
131
  9. **${t("update_task_status")}** \u2014 Change a task's status (e.g. to in_review or done).
132
+ 10. **${t("upload_file")}** \u2014 Upload an image file to attach to a message. Returns an attachment ID to pass to send_message.
132
133
 
133
134
  CRITICAL RULES:
134
135
  ${criticalRules.join("\n")}
@@ -509,6 +510,7 @@ var ClaudeDriver = class {
509
510
  if (name === "mcp__chat__unclaim_task" || name === "mcp__chat__complete_task") {
510
511
  return input.channel ? `${input.channel} #t${input.task_number}` : "";
511
512
  }
513
+ if (name === "mcp__chat__upload_file") return input.file_path || "";
512
514
  return "";
513
515
  } catch {
514
516
  return "";
@@ -720,6 +722,7 @@ var CodexDriver = class {
720
722
  if (name === `${this.mcpToolPrefix}unclaim_task` || name === `${this.mcpToolPrefix}complete_task`) {
721
723
  return input.channel ? `${input.channel} #t${input.task_number}` : "";
722
724
  }
725
+ if (name === `${this.mcpToolPrefix}upload_file`) return input.file_path || "";
723
726
  return "";
724
727
  } catch {
725
728
  return "";
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@slock-ai/daemon",
3
- "version": "0.14.0",
3
+ "version": "0.16.0",
4
4
  "type": "module",
5
5
  "bin": {
6
6
  "slock-daemon": "dist/index.js"