@slock-ai/daemon 0.14.0 → 0.17.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")}
@@ -465,11 +466,15 @@ var ClaudeDriver = class {
465
466
  });
466
467
  }
467
468
  toolDisplayName(name) {
468
- if (name === "mcp__chat__list_tasks") return "Viewing task board\u2026";
469
+ if (name === "mcp__chat__upload_file") return "Uploading file\u2026";
470
+ if (name === "mcp__chat__view_file") return "Viewing file\u2026";
471
+ if (name === "mcp__chat__list_tasks") return "Listing tasks\u2026";
469
472
  if (name === "mcp__chat__create_tasks") return "Creating tasks\u2026";
470
473
  if (name === "mcp__chat__claim_tasks") return "Claiming tasks\u2026";
471
474
  if (name === "mcp__chat__unclaim_task") return "Unclaiming task\u2026";
472
- if (name === "mcp__chat__complete_task") return "Completing task\u2026";
475
+ if (name === "mcp__chat__update_task_status") return "Updating task status\u2026";
476
+ if (name === "mcp__chat__list_server") return "Listing server\u2026";
477
+ if (name === "mcp__chat__read_history") return "Reading history\u2026";
473
478
  if (name.startsWith("mcp__chat__")) return "";
474
479
  if (name === "Read" || name === "read_file") return "Reading file\u2026";
475
480
  if (name === "Write" || name === "write_file") return "Writing file\u2026";
@@ -506,9 +511,13 @@ var ClaudeDriver = class {
506
511
  const nums = input.task_numbers;
507
512
  return input.channel ? `${input.channel} #t${Array.isArray(nums) ? nums.join(",#t") : nums}` : "";
508
513
  }
509
- if (name === "mcp__chat__unclaim_task" || name === "mcp__chat__complete_task") {
514
+ if (name === "mcp__chat__unclaim_task") {
510
515
  return input.channel ? `${input.channel} #t${input.task_number}` : "";
511
516
  }
517
+ if (name === "mcp__chat__update_task_status") {
518
+ return input.channel ? `${input.channel} #t${input.task_number}` : "";
519
+ }
520
+ if (name === "mcp__chat__upload_file") return input.file_path || "";
512
521
  return "";
513
522
  } catch {
514
523
  return "";
@@ -682,11 +691,15 @@ var CodexDriver = class {
682
691
  });
683
692
  }
684
693
  toolDisplayName(name) {
685
- if (name === `${this.mcpToolPrefix}list_tasks`) return "Viewing task board\u2026";
694
+ if (name === `${this.mcpToolPrefix}upload_file`) return "Uploading file\u2026";
695
+ if (name === `${this.mcpToolPrefix}view_file`) return "Viewing file\u2026";
696
+ if (name === `${this.mcpToolPrefix}list_tasks`) return "Listing tasks\u2026";
686
697
  if (name === `${this.mcpToolPrefix}create_tasks`) return "Creating tasks\u2026";
687
698
  if (name === `${this.mcpToolPrefix}claim_tasks`) return "Claiming tasks\u2026";
688
699
  if (name === `${this.mcpToolPrefix}unclaim_task`) return "Unclaiming task\u2026";
689
- if (name === `${this.mcpToolPrefix}complete_task`) return "Completing task\u2026";
700
+ if (name === `${this.mcpToolPrefix}update_task_status`) return "Updating task status\u2026";
701
+ if (name === `${this.mcpToolPrefix}list_server`) return "Listing server\u2026";
702
+ if (name === `${this.mcpToolPrefix}read_history`) return "Reading history\u2026";
690
703
  if (name.startsWith(this.mcpToolPrefix)) return "";
691
704
  if (name === "shell" || name === "command_execution") return "Running command\u2026";
692
705
  if (name === "file_change") return "Editing file\u2026";
@@ -717,9 +730,13 @@ var CodexDriver = class {
717
730
  const nums = input.task_numbers;
718
731
  return input.channel ? `${input.channel} #t${Array.isArray(nums) ? nums.join(",#t") : nums}` : "";
719
732
  }
720
- if (name === `${this.mcpToolPrefix}unclaim_task` || name === `${this.mcpToolPrefix}complete_task`) {
733
+ if (name === `${this.mcpToolPrefix}unclaim_task`) {
734
+ return input.channel ? `${input.channel} #t${input.task_number}` : "";
735
+ }
736
+ if (name === `${this.mcpToolPrefix}update_task_status`) {
721
737
  return input.channel ? `${input.channel} #t${input.task_number}` : "";
722
738
  }
739
+ if (name === `${this.mcpToolPrefix}upload_file`) return input.file_path || "";
723
740
  return "";
724
741
  } catch {
725
742
  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.17.0",
4
4
  "type": "module",
5
5
  "bin": {
6
6
  "slock-daemon": "dist/index.js"