nothumanallowed 15.1.36 → 15.1.37
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/slack.mjs +169 -7
- package/src/services/tool-executor.mjs +39 -2
- 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.37",
|
|
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.37';
|
|
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
|
});
|
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
|
+
|
|
@@ -752,7 +752,7 @@ browser_open(url) → returns sessionId. browser_screenshot(sessionId) · browse
|
|
|
752
752
|
note_add(text) · note_list()
|
|
753
753
|
github_issues(repo) · github_prs(repo) · github_notifications() · github_create_issue(repo, title, body)
|
|
754
754
|
notion_search(query) · notion_page(pageId)
|
|
755
|
-
slack_channels() · slack_messages(channel) · slack_send(channel, text)
|
|
755
|
+
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
756
|
|
|
757
757
|
### Files / Drive
|
|
758
758
|
file_list(dir?) · file_read(path) · file_write(path, content) · file_info(path) · file_search(query)
|
|
@@ -2009,7 +2009,44 @@ export async function executeTool(action, params, config) {
|
|
|
2009
2009
|
|
|
2010
2010
|
case 'slack_send': {
|
|
2011
2011
|
const sl = await import('./slack.mjs');
|
|
2012
|
-
return sl.sendMessage(config, params.channel, params.text);
|
|
2012
|
+
return sl.sendMessage(config, params.channel, params.text, params.threadTs || null);
|
|
2013
|
+
}
|
|
2014
|
+
|
|
2015
|
+
case 'slack_search': {
|
|
2016
|
+
const sl = await import('./slack.mjs');
|
|
2017
|
+
const results = await sl.searchMessages(config, params.query, { count: params.count || 20 });
|
|
2018
|
+
if (!results.length) return `No messages match "${params.query}".`;
|
|
2019
|
+
return results.map((r, i) =>
|
|
2020
|
+
`${i + 1}. [#${r.channel}] ${r.user} (${new Date(parseFloat(r.ts) * 1000).toLocaleString()}): ${r.text.slice(0, 200)}${r.permalink ? `\n → ${r.permalink}` : ''}`,
|
|
2021
|
+
).join('\n');
|
|
2022
|
+
}
|
|
2023
|
+
|
|
2024
|
+
case 'slack_dm': {
|
|
2025
|
+
const sl = await import('./slack.mjs');
|
|
2026
|
+
const channelId = await sl.openDM(config, params.user);
|
|
2027
|
+
if (params.text) await sl.sendMessage(config, channelId, params.text);
|
|
2028
|
+
return params.text
|
|
2029
|
+
? `DM sent to ${params.user} (channel ${channelId}).`
|
|
2030
|
+
: `Opened DM channel ${channelId} with ${params.user}.`;
|
|
2031
|
+
}
|
|
2032
|
+
|
|
2033
|
+
case 'slack_thread': {
|
|
2034
|
+
const sl = await import('./slack.mjs');
|
|
2035
|
+
const replies = await sl.getThreadReplies(config, params.channel, params.ts);
|
|
2036
|
+
if (!replies.length) return 'Thread has no replies.';
|
|
2037
|
+
return replies.map((r, i) =>
|
|
2038
|
+
`${i + 1}. ${r.user} (${new Date(parseFloat(r.ts) * 1000).toLocaleTimeString()}): ${r.text.slice(0, 200)}`,
|
|
2039
|
+
).join('\n');
|
|
2040
|
+
}
|
|
2041
|
+
|
|
2042
|
+
case 'slack_react': {
|
|
2043
|
+
const sl = await import('./slack.mjs');
|
|
2044
|
+
return sl.addReaction(config, params.channel, params.ts, params.emoji || 'thumbsup');
|
|
2045
|
+
}
|
|
2046
|
+
|
|
2047
|
+
case 'slack_mark_read': {
|
|
2048
|
+
const sl = await import('./slack.mjs');
|
|
2049
|
+
return sl.markRead(config, params.channel, params.ts || null);
|
|
2013
2050
|
}
|
|
2014
2051
|
|
|
2015
2052
|
// ── Maps Directions ──────────────────────────────────────────────────
|