nothumanallowed 15.1.36 → 15.1.38
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/package.json +1 -1
- package/src/constants.mjs +1 -1
- package/src/server/routes/integrations.mjs +73 -7
- package/src/services/message-responder.mjs +52 -2
- package/src/services/slack.mjs +169 -7
- package/src/services/tool-executor.mjs +66 -3
- package/src/ui-dist/assets/index-BksXdTm1.js +812 -0
- package/src/ui-dist/index.html +1 -1
- package/src/ui-dist/assets/index-W_O89F51.js +0 -812
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "nothumanallowed",
|
|
3
|
-
"version": "15.1.
|
|
3
|
+
"version": "15.1.38",
|
|
4
4
|
"description": "NotHumanAllowed — 38 AI agents, 80 tools, Studio (visual agentic workflows). Email, calendar, browser automation, screen capture, canvas, cron/heartbeat, Alexandria E2E messaging, GitHub, Notion, Slack, voice chat, free AI (Liara), 28 languages. Zero-dependency CLI.",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"bin": {
|
package/src/constants.mjs
CHANGED
|
@@ -5,7 +5,7 @@ import { fileURLToPath } from 'url';
|
|
|
5
5
|
const __filename = fileURLToPath(import.meta.url);
|
|
6
6
|
const __dirname = path.dirname(__filename);
|
|
7
7
|
|
|
8
|
-
export const VERSION = '15.1.
|
|
8
|
+
export const VERSION = '15.1.38';
|
|
9
9
|
export const BASE_URL = 'https://nothumanallowed.com/cli';
|
|
10
10
|
export const API_BASE = 'https://nothumanallowed.com/api/v1';
|
|
11
11
|
|
|
@@ -102,22 +102,87 @@ export function register(router) {
|
|
|
102
102
|
} catch (e) { sendError(res, 500, e.message); }
|
|
103
103
|
});
|
|
104
104
|
|
|
105
|
-
// ── Slack
|
|
105
|
+
// ── Slack (enterprise UI: rich channels, search, threads, reactions) ──
|
|
106
106
|
|
|
107
107
|
router.get('/api/slack/channels', async (_req, res) => {
|
|
108
108
|
try {
|
|
109
|
-
const {
|
|
109
|
+
const { listChannelsRich, getWorkspaceInfo } = await import('../../services/slack.mjs');
|
|
110
110
|
const config = loadConfig();
|
|
111
|
-
|
|
112
|
-
|
|
111
|
+
const [channels, workspace] = await Promise.all([
|
|
112
|
+
listChannelsRich(config, { maxResults: 200 }),
|
|
113
|
+
getWorkspaceInfo(config),
|
|
114
|
+
]);
|
|
115
|
+
sendJSON(res, 200, { channels, workspace: workspace?.name || '', workspaceIcon: workspace?.icon || '' });
|
|
116
|
+
} catch (e) { sendJSON(res, 200, { channels: [], error: e.message }); }
|
|
113
117
|
});
|
|
114
118
|
|
|
115
119
|
router.get('/api/slack/messages', async (req, res) => {
|
|
116
120
|
try {
|
|
117
|
-
const {
|
|
121
|
+
const { listMessagesRich } = await import('../../services/slack.mjs');
|
|
118
122
|
const config = loadConfig();
|
|
119
123
|
const url = new URL(req.url, 'http://localhost');
|
|
120
|
-
|
|
124
|
+
const channel = url.searchParams.get('channel');
|
|
125
|
+
const limit = parseInt(url.searchParams.get('limit') || '50', 10);
|
|
126
|
+
if (!channel) return sendError(res, 400, 'channel param required');
|
|
127
|
+
sendJSON(res, 200, { messages: await listMessagesRich(config, channel, { limit }) });
|
|
128
|
+
} catch (e) { sendError(res, 500, e.message); }
|
|
129
|
+
});
|
|
130
|
+
|
|
131
|
+
router.get('/api/slack/thread', async (req, res) => {
|
|
132
|
+
try {
|
|
133
|
+
const { getThreadReplies } = await import('../../services/slack.mjs');
|
|
134
|
+
const config = loadConfig();
|
|
135
|
+
const url = new URL(req.url, 'http://localhost');
|
|
136
|
+
const channel = url.searchParams.get('channel');
|
|
137
|
+
const ts = url.searchParams.get('ts');
|
|
138
|
+
if (!channel || !ts) return sendError(res, 400, 'channel and ts required');
|
|
139
|
+
sendJSON(res, 200, { messages: await getThreadReplies(config, channel, ts) });
|
|
140
|
+
} catch (e) { sendError(res, 500, e.message); }
|
|
141
|
+
});
|
|
142
|
+
|
|
143
|
+
router.get('/api/slack/search', async (req, res) => {
|
|
144
|
+
try {
|
|
145
|
+
const { searchMessages } = await import('../../services/slack.mjs');
|
|
146
|
+
const config = loadConfig();
|
|
147
|
+
const url = new URL(req.url, 'http://localhost');
|
|
148
|
+
const q = url.searchParams.get('q') || '';
|
|
149
|
+
if (!q.trim()) return sendJSON(res, 200, { results: [] });
|
|
150
|
+
const count = parseInt(url.searchParams.get('count') || '20', 10);
|
|
151
|
+
sendJSON(res, 200, { results: await searchMessages(config, q, { count }) });
|
|
152
|
+
} catch (e) { sendJSON(res, 200, { results: [], error: e.message }); }
|
|
153
|
+
});
|
|
154
|
+
|
|
155
|
+
router.post('/api/slack/react', async (req, res) => {
|
|
156
|
+
try {
|
|
157
|
+
const { addReaction } = await import('../../services/slack.mjs');
|
|
158
|
+
const config = loadConfig();
|
|
159
|
+
const body = await parseBody(req);
|
|
160
|
+
if (!body.channel || !body.ts || !body.emoji) return sendError(res, 400, 'channel, ts, emoji required');
|
|
161
|
+
await addReaction(config, body.channel, body.ts, body.emoji);
|
|
162
|
+
sendJSON(res, 200, { ok: true });
|
|
163
|
+
} catch (e) { sendError(res, 500, e.message); }
|
|
164
|
+
});
|
|
165
|
+
|
|
166
|
+
router.post('/api/slack/mark-read', async (req, res) => {
|
|
167
|
+
try {
|
|
168
|
+
const { markRead } = await import('../../services/slack.mjs');
|
|
169
|
+
const config = loadConfig();
|
|
170
|
+
const body = await parseBody(req);
|
|
171
|
+
if (!body.channel) return sendError(res, 400, 'channel required');
|
|
172
|
+
await markRead(config, body.channel, body.ts);
|
|
173
|
+
sendJSON(res, 200, { ok: true });
|
|
174
|
+
} catch (e) { sendError(res, 500, e.message); }
|
|
175
|
+
});
|
|
176
|
+
|
|
177
|
+
router.post('/api/slack/dm', async (req, res) => {
|
|
178
|
+
try {
|
|
179
|
+
const { openDM, sendMessage } = await import('../../services/slack.mjs');
|
|
180
|
+
const config = loadConfig();
|
|
181
|
+
const body = await parseBody(req);
|
|
182
|
+
if (!body.user) return sendError(res, 400, 'user required (id or name or email)');
|
|
183
|
+
const channelId = await openDM(config, body.user);
|
|
184
|
+
if (body.text) await sendMessage(config, channelId, body.text);
|
|
185
|
+
sendJSON(res, 200, { ok: true, channelId });
|
|
121
186
|
} catch (e) { sendError(res, 500, e.message); }
|
|
122
187
|
});
|
|
123
188
|
|
|
@@ -308,7 +373,8 @@ export function register(router) {
|
|
|
308
373
|
const body = await parseBody(req);
|
|
309
374
|
const config = loadConfig();
|
|
310
375
|
if (!body.channel || !body.text) return sendError(res, 400, 'channel and text required');
|
|
311
|
-
|
|
376
|
+
// threadTs optional — if provided, the message is posted as a thread reply
|
|
377
|
+
await sendMessage(config, body.channel, body.text, body.threadTs || null);
|
|
312
378
|
sendJSON(res, 200, { ok: true });
|
|
313
379
|
} catch (e) { sendError(res, 500, e.message); }
|
|
314
380
|
});
|
|
@@ -530,6 +530,37 @@ class TelegramResponder {
|
|
|
530
530
|
saveTelegramContext(this._lastContextByChatId);
|
|
531
531
|
}
|
|
532
532
|
|
|
533
|
+
/**
|
|
534
|
+
* Language-agnostic intent classifier (15.1.38).
|
|
535
|
+
*
|
|
536
|
+
* Previous versions hard-coded confirmation/reaction keywords for Italian
|
|
537
|
+
* and English. That couldn't scale — every new language ("schedule it"
|
|
538
|
+
* in Spanish, German, Japanese, Polish, Turkish, ...) would need a new
|
|
539
|
+
* keyword list. Now we delegate to a tiny LLM call: 16 tokens output,
|
|
540
|
+
* works in any human language the model knows.
|
|
541
|
+
*
|
|
542
|
+
* Fast path: if the keyword matcher already returned a confident verdict,
|
|
543
|
+
* skip the LLM. Only ambiguous cases (short messages, no obvious keyword)
|
|
544
|
+
* pay the ~50ms LLM cost.
|
|
545
|
+
*
|
|
546
|
+
* @returns 'continuation' | 'new_request' | 'unknown'
|
|
547
|
+
*/
|
|
548
|
+
async _classifyIntent(text, lastCtx) {
|
|
549
|
+
if (!lastCtx) return 'new_request';
|
|
550
|
+
const hasKey = !!(this.config.llm?.apiKey || this.config.llm?.openaiKey || this.config.llm?.geminiKey || (this.config.llm?.provider === 'nha'));
|
|
551
|
+
if (!hasKey) return 'unknown';
|
|
552
|
+
try {
|
|
553
|
+
const prev = (lastCtx.agentReply || '').slice(0, 600);
|
|
554
|
+
const sys = 'You are a binary intent classifier for a chat assistant. Reply with EXACTLY one word: "continuation" or "new_request". Use "continuation" when the user is confirming, denying, reacting to, or refining the assistant\'s previous message (in any language). Use "new_request" when the user is starting an unrelated topic. Do not output anything else.';
|
|
555
|
+
const usr = `Assistant just said:\n"${prev}"\n\nUser now writes:\n"${text}"\n\nClassify the user\'s message: continuation or new_request?`;
|
|
556
|
+
const ans = await callLLM(this.config, sys, usr, { max_tokens: 8 });
|
|
557
|
+
const v = String(ans || '').trim().toLowerCase().replace(/[^a-z_]/g, '');
|
|
558
|
+
if (v === 'continuation' || v.startsWith('cont')) return 'continuation';
|
|
559
|
+
if (v === 'new_request' || v.startsWith('new')) return 'new_request';
|
|
560
|
+
return 'unknown';
|
|
561
|
+
} catch { return 'unknown'; }
|
|
562
|
+
}
|
|
563
|
+
|
|
533
564
|
/**
|
|
534
565
|
* Route a fresh message to the best agent.
|
|
535
566
|
*
|
|
@@ -823,8 +854,27 @@ class TelegramResponder {
|
|
|
823
854
|
const withinStickyWindow = stickyAge < 5 * 60 * 1000; // 5 min
|
|
824
855
|
|
|
825
856
|
// Determine if this message is a continuation of the previous turn
|
|
826
|
-
// (confirmation, reaction, short reply) vs a new independent request
|
|
827
|
-
|
|
857
|
+
// (confirmation, reaction, short reply) vs a new independent request.
|
|
858
|
+
//
|
|
859
|
+
// Two-stage detection:
|
|
860
|
+
// 1. Fast keyword path (IT + EN) — handles the obvious cases (~80%) with zero cost.
|
|
861
|
+
// 2. LLM classifier fallback — for ambiguous cases in any language
|
|
862
|
+
// ("agéndalo", "termin verschieben", "schedule it",
|
|
863
|
+
// "ya", "tamam", "harika", ...). Costs ~50 ms and 8 output tokens
|
|
864
|
+
// but is language-agnostic by design.
|
|
865
|
+
let isContinuation = withinStickyWindow && isContinuationMessage(cleanText, lastCtx);
|
|
866
|
+
if (withinStickyWindow && !isContinuation && lastCtx) {
|
|
867
|
+
// Only invoke the LLM if keyword fast-path didn't flag continuation
|
|
868
|
+
// AND the message is short enough to plausibly be a reaction (≤ 12 words).
|
|
869
|
+
const wordCount = cleanText.trim().split(/\s+/).length;
|
|
870
|
+
if (wordCount <= 12) {
|
|
871
|
+
const verdict = await this._classifyIntent(cleanText, lastCtx);
|
|
872
|
+
if (verdict === 'continuation') {
|
|
873
|
+
isContinuation = true;
|
|
874
|
+
this.log(`[Telegram] ${fromUser}: LLM classifier overrode keyword → continuation`);
|
|
875
|
+
}
|
|
876
|
+
}
|
|
877
|
+
}
|
|
828
878
|
|
|
829
879
|
// If last response was a completed action, don't carry history forward —
|
|
830
880
|
// the next message is a fresh request even if it looks like a reaction
|
package/src/services/slack.mjs
CHANGED
|
@@ -127,19 +127,181 @@ export async function listMessages(config, channel, maxResults = 15) {
|
|
|
127
127
|
}
|
|
128
128
|
|
|
129
129
|
/**
|
|
130
|
-
* Send a message to a channel.
|
|
131
|
-
* @returns {Promise<string>} formatted result
|
|
130
|
+
* Send a message to a channel — supports thread replies via threadTs.
|
|
131
|
+
* @returns {Promise<string>} formatted result with the new message ts
|
|
132
132
|
*/
|
|
133
|
-
export async function sendMessage(config, channel, text) {
|
|
133
|
+
export async function sendMessage(config, channel, text, threadTs = null) {
|
|
134
134
|
if (!channel) return 'Channel name or ID required.';
|
|
135
135
|
if (!text) return 'Message text required.';
|
|
136
136
|
|
|
137
137
|
const channelId = await resolveChannelId(config, channel);
|
|
138
138
|
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
139
|
+
const params = { channel: channelId, text };
|
|
140
|
+
if (threadTs) params.thread_ts = threadTs;
|
|
141
|
+
|
|
142
|
+
const res = await slackFetch(config, 'chat.postMessage', params);
|
|
143
|
+
|
|
144
|
+
return `Message sent to #${channel}${threadTs ? ' (thread reply)' : ''}. ts=${res.ts}`;
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
// ── Advanced operations (15.1.37) ───────────────────────────────────────────
|
|
148
|
+
|
|
149
|
+
/**
|
|
150
|
+
* Cache of user_id → display info to avoid re-fetching on every list call.
|
|
151
|
+
* Cleared when the responder restarts. TTL implicit per process lifetime.
|
|
152
|
+
*/
|
|
153
|
+
const _userCache = new Map();
|
|
154
|
+
|
|
155
|
+
async function _resolveUsers(config, userIds) {
|
|
156
|
+
const unknown = [...new Set(userIds)].filter(u => u && !_userCache.has(u));
|
|
157
|
+
for (const uid of unknown.slice(0, 30)) {
|
|
158
|
+
try {
|
|
159
|
+
const u = await slackFetch(config, 'users.info', { user: uid });
|
|
160
|
+
_userCache.set(uid, {
|
|
161
|
+
id: uid,
|
|
162
|
+
name: u.user?.real_name || u.user?.name || uid,
|
|
163
|
+
avatar: u.user?.profile?.image_48 || '',
|
|
164
|
+
title: u.user?.profile?.title || '',
|
|
165
|
+
is_bot: !!u.user?.is_bot,
|
|
166
|
+
});
|
|
167
|
+
} catch {
|
|
168
|
+
_userCache.set(uid, { id: uid, name: uid, avatar: '', title: '', is_bot: false });
|
|
169
|
+
}
|
|
170
|
+
}
|
|
171
|
+
return _userCache;
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
/** Return raw channel objects with member-resolved metadata. */
|
|
175
|
+
export async function listChannelsRich(config, { types = 'public_channel,private_channel,mpim,im', maxResults = 200, excludeArchived = true } = {}) {
|
|
176
|
+
const data = await slackFetch(config, 'conversations.list', {
|
|
177
|
+
types, limit: maxResults, exclude_archived: excludeArchived,
|
|
142
178
|
});
|
|
179
|
+
const channels = data.channels || [];
|
|
180
|
+
// For DMs, resolve the other user's name
|
|
181
|
+
const dmUserIds = channels.filter(c => c.is_im).map(c => c.user).filter(Boolean);
|
|
182
|
+
if (dmUserIds.length) await _resolveUsers(config, dmUserIds);
|
|
183
|
+
return channels.map(c => ({
|
|
184
|
+
id: c.id,
|
|
185
|
+
name: c.is_im ? (`@${_userCache.get(c.user)?.name || c.user}`) : c.name,
|
|
186
|
+
is_member: c.is_member,
|
|
187
|
+
is_private: c.is_private,
|
|
188
|
+
is_im: c.is_im,
|
|
189
|
+
is_mpim: c.is_mpim,
|
|
190
|
+
is_archived: c.is_archived,
|
|
191
|
+
num_members: c.num_members,
|
|
192
|
+
purpose: c.purpose?.value || '',
|
|
193
|
+
topic: c.topic?.value || '',
|
|
194
|
+
unread_count: c.unread_count_display || 0,
|
|
195
|
+
}));
|
|
196
|
+
}
|
|
143
197
|
|
|
144
|
-
|
|
198
|
+
/** Return raw message objects with user names resolved + thread metadata. */
|
|
199
|
+
export async function listMessagesRich(config, channel, { limit = 50, oldest, latest } = {}) {
|
|
200
|
+
const channelId = await resolveChannelId(config, channel);
|
|
201
|
+
const params = { channel: channelId, limit };
|
|
202
|
+
if (oldest) params.oldest = oldest;
|
|
203
|
+
if (latest) params.latest = latest;
|
|
204
|
+
const data = await slackFetch(config, 'conversations.history', params);
|
|
205
|
+
const messages = (data.messages || []).reverse();
|
|
206
|
+
const userIds = messages.map(m => m.user || m.bot_id).filter(Boolean);
|
|
207
|
+
await _resolveUsers(config, userIds);
|
|
208
|
+
return messages.map(m => ({
|
|
209
|
+
ts: m.ts,
|
|
210
|
+
user: _userCache.get(m.user)?.name || m.user || (m.username ? m.username : 'bot'),
|
|
211
|
+
user_id: m.user,
|
|
212
|
+
avatar: _userCache.get(m.user)?.avatar || '',
|
|
213
|
+
text: m.text || '',
|
|
214
|
+
type: m.type,
|
|
215
|
+
subtype: m.subtype,
|
|
216
|
+
thread_ts: m.thread_ts,
|
|
217
|
+
reply_count: m.reply_count || 0,
|
|
218
|
+
reactions: (m.reactions || []).map(r => ({ name: r.name, count: r.count })),
|
|
219
|
+
files: (m.files || []).map(f => ({ id: f.id, name: f.name, mimetype: f.mimetype, url: f.url_private })),
|
|
220
|
+
}));
|
|
145
221
|
}
|
|
222
|
+
|
|
223
|
+
/** Get all replies in a thread. */
|
|
224
|
+
export async function getThreadReplies(config, channel, threadTs) {
|
|
225
|
+
const channelId = await resolveChannelId(config, channel);
|
|
226
|
+
const data = await slackFetch(config, 'conversations.replies', {
|
|
227
|
+
channel: channelId, ts: threadTs, limit: 100,
|
|
228
|
+
});
|
|
229
|
+
const messages = data.messages || [];
|
|
230
|
+
const userIds = messages.map(m => m.user || m.bot_id).filter(Boolean);
|
|
231
|
+
await _resolveUsers(config, userIds);
|
|
232
|
+
return messages.map(m => ({
|
|
233
|
+
ts: m.ts,
|
|
234
|
+
user: _userCache.get(m.user)?.name || m.user || 'bot',
|
|
235
|
+
text: m.text || '',
|
|
236
|
+
reactions: (m.reactions || []).map(r => ({ name: r.name, count: r.count })),
|
|
237
|
+
}));
|
|
238
|
+
}
|
|
239
|
+
|
|
240
|
+
/** Search messages across the workspace (requires search:read scope). */
|
|
241
|
+
export async function searchMessages(config, query, { count = 20 } = {}) {
|
|
242
|
+
const data = await slackFetch(config, 'search.messages', { query, count, sort: 'timestamp', sort_dir: 'desc' });
|
|
243
|
+
const matches = data.messages?.matches || [];
|
|
244
|
+
const userIds = matches.map(m => m.user).filter(Boolean);
|
|
245
|
+
await _resolveUsers(config, userIds);
|
|
246
|
+
return matches.map(m => ({
|
|
247
|
+
ts: m.ts,
|
|
248
|
+
user: _userCache.get(m.user)?.name || m.user || m.username || 'unknown',
|
|
249
|
+
text: m.text || '',
|
|
250
|
+
channel: m.channel?.name || m.channel?.id,
|
|
251
|
+
channel_id: m.channel?.id,
|
|
252
|
+
permalink: m.permalink,
|
|
253
|
+
}));
|
|
254
|
+
}
|
|
255
|
+
|
|
256
|
+
/** Add an emoji reaction to a message. */
|
|
257
|
+
export async function addReaction(config, channel, ts, emoji) {
|
|
258
|
+
const channelId = await resolveChannelId(config, channel);
|
|
259
|
+
await slackFetch(config, 'reactions.add', {
|
|
260
|
+
channel: channelId, timestamp: ts, name: emoji.replace(/^:|:$/g, ''),
|
|
261
|
+
});
|
|
262
|
+
return `Added :${emoji}: reaction.`;
|
|
263
|
+
}
|
|
264
|
+
|
|
265
|
+
/** Mark a channel as read up to a given ts (or now). */
|
|
266
|
+
export async function markRead(config, channel, ts = null) {
|
|
267
|
+
const channelId = await resolveChannelId(config, channel);
|
|
268
|
+
await slackFetch(config, 'conversations.mark', {
|
|
269
|
+
channel: channelId, ts: ts || (Date.now() / 1000).toString(),
|
|
270
|
+
});
|
|
271
|
+
return `Marked #${channel} as read.`;
|
|
272
|
+
}
|
|
273
|
+
|
|
274
|
+
/** Open a DM with a user by ID or name lookup. */
|
|
275
|
+
export async function openDM(config, userIdOrName) {
|
|
276
|
+
let uid = userIdOrName;
|
|
277
|
+
// If it doesn't look like a user ID (U...), try to look it up
|
|
278
|
+
if (!/^U[A-Z0-9]{8,}$/.test(uid)) {
|
|
279
|
+
const data = await slackFetch(config, 'users.list', { limit: 1000 });
|
|
280
|
+
const match = (data.members || []).find(u =>
|
|
281
|
+
u.real_name?.toLowerCase() === uid.toLowerCase() ||
|
|
282
|
+
u.name?.toLowerCase() === uid.toLowerCase() ||
|
|
283
|
+
u.profile?.email?.toLowerCase() === uid.toLowerCase(),
|
|
284
|
+
);
|
|
285
|
+
if (!match) throw new Error(`User "${userIdOrName}" not found in workspace.`);
|
|
286
|
+
uid = match.id;
|
|
287
|
+
}
|
|
288
|
+
const data = await slackFetch(config, 'conversations.open', { users: uid });
|
|
289
|
+
return data.channel?.id;
|
|
290
|
+
}
|
|
291
|
+
|
|
292
|
+
/** Workspace info (team name, icon). */
|
|
293
|
+
export async function getWorkspaceInfo(config) {
|
|
294
|
+
try {
|
|
295
|
+
const data = await slackFetch(config, 'team.info', {});
|
|
296
|
+
return {
|
|
297
|
+
id: data.team?.id,
|
|
298
|
+
name: data.team?.name,
|
|
299
|
+
domain: data.team?.domain,
|
|
300
|
+
icon: data.team?.icon?.image_88,
|
|
301
|
+
};
|
|
302
|
+
} catch { return null; }
|
|
303
|
+
}
|
|
304
|
+
|
|
305
|
+
/** Update the cached user info — useful before listing messages. */
|
|
306
|
+
export { _resolveUsers as resolveUsers };
|
|
307
|
+
|
|
@@ -294,8 +294,34 @@ TOOLS:
|
|
|
294
294
|
40. slack_messages(channel: string, maxResults?: number)
|
|
295
295
|
List recent messages in a Slack channel (name or ID). Default max 15.
|
|
296
296
|
|
|
297
|
-
41. slack_send(channel: string, text: string)
|
|
297
|
+
41. slack_send(channel: string, text: string, threadTs?: string)
|
|
298
298
|
Send a message to a Slack channel. ALWAYS confirm before sending.
|
|
299
|
+
If `threadTs` is provided, post as a thread reply instead of a top-level message.
|
|
300
|
+
|
|
301
|
+
41b. slack_search(query: string, count?: number)
|
|
302
|
+
Full-text search messages across the whole Slack workspace. Returns the most
|
|
303
|
+
recent matches with channel, user, text and a Slack permalink each. Use this
|
|
304
|
+
for "find the message where X talked about Y", "trova il messaggio di Marco
|
|
305
|
+
su X", "did anyone post about the release", etc.
|
|
306
|
+
|
|
307
|
+
41c. slack_dm(user: string, text?: string)
|
|
308
|
+
Open or send a direct message to a user. `user` accepts Slack user ID (Uxxx),
|
|
309
|
+
username, real name, or email — the tool resolves them automatically.
|
|
310
|
+
If `text` is provided, the message is sent immediately; otherwise the DM
|
|
311
|
+
channel is just opened.
|
|
312
|
+
|
|
313
|
+
41d. slack_thread(channel: string, ts: string)
|
|
314
|
+
List all replies in a thread. `ts` is the parent message timestamp (returned
|
|
315
|
+
by slack_messages or slack_search). Use this before posting a contextual
|
|
316
|
+
reply with slack_send + threadTs.
|
|
317
|
+
|
|
318
|
+
41e. slack_react(channel: string, ts: string, emoji: string)
|
|
319
|
+
Add an emoji reaction to a message. `emoji` is the name without colons
|
|
320
|
+
(e.g. "thumbsup", "rocket", "white_check_mark"). Confirm before reacting on
|
|
321
|
+
someone else's message.
|
|
322
|
+
|
|
323
|
+
41f. slack_mark_read(channel: string, ts?: string)
|
|
324
|
+
Mark a Slack channel as read up to a given timestamp (default: now).
|
|
299
325
|
|
|
300
326
|
--- FILE ATTACHMENT ---
|
|
301
327
|
|
|
@@ -752,7 +778,7 @@ browser_open(url) → returns sessionId. browser_screenshot(sessionId) · browse
|
|
|
752
778
|
note_add(text) · note_list()
|
|
753
779
|
github_issues(repo) · github_prs(repo) · github_notifications() · github_create_issue(repo, title, body)
|
|
754
780
|
notion_search(query) · notion_page(pageId)
|
|
755
|
-
slack_channels() · slack_messages(channel) · slack_send(channel, text)
|
|
781
|
+
slack_channels() · slack_messages(channel) · slack_send(channel, text, threadTs?) · slack_search(query) · slack_dm(user, text?) · slack_thread(channel, ts) · slack_react(channel, ts, emoji) · slack_mark_read(channel, ts?)
|
|
756
782
|
|
|
757
783
|
### Files / Drive
|
|
758
784
|
file_list(dir?) · file_read(path) · file_write(path, content) · file_info(path) · file_search(query)
|
|
@@ -2009,7 +2035,44 @@ export async function executeTool(action, params, config) {
|
|
|
2009
2035
|
|
|
2010
2036
|
case 'slack_send': {
|
|
2011
2037
|
const sl = await import('./slack.mjs');
|
|
2012
|
-
return sl.sendMessage(config, params.channel, params.text);
|
|
2038
|
+
return sl.sendMessage(config, params.channel, params.text, params.threadTs || null);
|
|
2039
|
+
}
|
|
2040
|
+
|
|
2041
|
+
case 'slack_search': {
|
|
2042
|
+
const sl = await import('./slack.mjs');
|
|
2043
|
+
const results = await sl.searchMessages(config, params.query, { count: params.count || 20 });
|
|
2044
|
+
if (!results.length) return `No messages match "${params.query}".`;
|
|
2045
|
+
return results.map((r, i) =>
|
|
2046
|
+
`${i + 1}. [#${r.channel}] ${r.user} (${new Date(parseFloat(r.ts) * 1000).toLocaleString()}): ${r.text.slice(0, 200)}${r.permalink ? `\n → ${r.permalink}` : ''}`,
|
|
2047
|
+
).join('\n');
|
|
2048
|
+
}
|
|
2049
|
+
|
|
2050
|
+
case 'slack_dm': {
|
|
2051
|
+
const sl = await import('./slack.mjs');
|
|
2052
|
+
const channelId = await sl.openDM(config, params.user);
|
|
2053
|
+
if (params.text) await sl.sendMessage(config, channelId, params.text);
|
|
2054
|
+
return params.text
|
|
2055
|
+
? `DM sent to ${params.user} (channel ${channelId}).`
|
|
2056
|
+
: `Opened DM channel ${channelId} with ${params.user}.`;
|
|
2057
|
+
}
|
|
2058
|
+
|
|
2059
|
+
case 'slack_thread': {
|
|
2060
|
+
const sl = await import('./slack.mjs');
|
|
2061
|
+
const replies = await sl.getThreadReplies(config, params.channel, params.ts);
|
|
2062
|
+
if (!replies.length) return 'Thread has no replies.';
|
|
2063
|
+
return replies.map((r, i) =>
|
|
2064
|
+
`${i + 1}. ${r.user} (${new Date(parseFloat(r.ts) * 1000).toLocaleTimeString()}): ${r.text.slice(0, 200)}`,
|
|
2065
|
+
).join('\n');
|
|
2066
|
+
}
|
|
2067
|
+
|
|
2068
|
+
case 'slack_react': {
|
|
2069
|
+
const sl = await import('./slack.mjs');
|
|
2070
|
+
return sl.addReaction(config, params.channel, params.ts, params.emoji || 'thumbsup');
|
|
2071
|
+
}
|
|
2072
|
+
|
|
2073
|
+
case 'slack_mark_read': {
|
|
2074
|
+
const sl = await import('./slack.mjs');
|
|
2075
|
+
return sl.markRead(config, params.channel, params.ts || null);
|
|
2013
2076
|
}
|
|
2014
2077
|
|
|
2015
2078
|
// ── Maps Directions ──────────────────────────────────────────────────
|