@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.
- package/dist/chat-bridge.js +479 -123
- package/dist/{chunk-ZXIMDHR7.js → chunk-7RQ2H2AM.js} +900 -131
- package/dist/{chunk-GX2DVINN.js → chunk-E6OOH3IC.js} +46 -1
- package/dist/core.js +4 -3
- package/dist/index.js +2 -2
- package/package.json +1 -1
package/dist/chat-bridge.js
CHANGED
|
@@ -1,12 +1,205 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
2
|
import {
|
|
3
|
-
buildFetchDispatcher
|
|
4
|
-
|
|
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 =
|
|
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
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
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
|
-
|
|
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(
|
|
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
|
|
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
|
|
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 >
|
|
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
|
|
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
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
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
|
|
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
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
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
|
|
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
|
-
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
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
|
|
245
|
-
|
|
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
|
|
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
|
-
|
|
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
|
|
282
|
-
if (
|
|
283
|
-
|
|
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
|
|
622
|
+
const senderType = ` type=${m.sender_type}`;
|
|
302
623
|
const renderedContent = m.content;
|
|
303
|
-
const attachSuffix = m.attachments
|
|
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}]
|
|
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
|
|
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 (
|
|
323
|
-
for (const t of
|
|
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 (
|
|
335
|
-
for (const a of
|
|
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 (
|
|
345
|
-
for (const u of
|
|
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
|
|
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
|
|
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
|
|
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
|
-
|
|
472
|
-
|
|
473
|
-
|
|
474
|
-
|
|
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
|
|
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
|
|
593
|
-
|
|
594
|
-
|
|
595
|
-
|
|
596
|
-
|
|
597
|
-
|
|
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
|
|
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
|
|
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
|
|
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,
|