@owloops/browserbird 1.0.2 → 1.0.3
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/bin/browserbird +7 -1
- package/dist/db-BsYEYsul.mjs +1011 -0
- package/dist/index.mjs +4748 -0
- package/package.json +6 -3
- package/src/channel/blocks.ts +0 -485
- package/src/channel/coalesce.ts +0 -79
- package/src/channel/commands.ts +0 -216
- package/src/channel/handler.ts +0 -272
- package/src/channel/slack.ts +0 -573
- package/src/channel/types.ts +0 -59
- package/src/cli/banner.ts +0 -10
- package/src/cli/birds.ts +0 -396
- package/src/cli/config.ts +0 -77
- package/src/cli/doctor.ts +0 -63
- package/src/cli/index.ts +0 -5
- package/src/cli/jobs.ts +0 -166
- package/src/cli/logs.ts +0 -67
- package/src/cli/run.ts +0 -148
- package/src/cli/sessions.ts +0 -158
- package/src/cli/style.ts +0 -19
- package/src/config.ts +0 -291
- package/src/core/logger.ts +0 -78
- package/src/core/redact.ts +0 -75
- package/src/core/types.ts +0 -83
- package/src/core/uid.ts +0 -26
- package/src/core/utils.ts +0 -137
- package/src/cron/parse.ts +0 -146
- package/src/cron/scheduler.ts +0 -242
- package/src/daemon.ts +0 -169
- package/src/db/auth.ts +0 -49
- package/src/db/birds.ts +0 -357
- package/src/db/core.ts +0 -377
- package/src/db/index.ts +0 -10
- package/src/db/jobs.ts +0 -289
- package/src/db/logs.ts +0 -64
- package/src/db/messages.ts +0 -79
- package/src/db/path.ts +0 -30
- package/src/db/sessions.ts +0 -165
- package/src/jobs.ts +0 -140
- package/src/provider/claude.test.ts +0 -95
- package/src/provider/claude.ts +0 -196
- package/src/provider/opencode.test.ts +0 -169
- package/src/provider/opencode.ts +0 -248
- package/src/provider/session.ts +0 -65
- package/src/provider/spawn.ts +0 -173
- package/src/provider/stream.ts +0 -67
- package/src/provider/types.ts +0 -24
- package/src/server/auth.ts +0 -135
- package/src/server/health.ts +0 -87
- package/src/server/http.ts +0 -132
- package/src/server/index.ts +0 -6
- package/src/server/lifecycle.ts +0 -135
- package/src/server/routes.ts +0 -1199
- package/src/server/sse.ts +0 -54
- package/src/server/static.ts +0 -45
- package/src/server/vnc-proxy.ts +0 -75
package/src/channel/slack.ts
DELETED
|
@@ -1,573 +0,0 @@
|
|
|
1
|
-
/** @fileoverview Slack adapter: SocketModeClient + WebClient, streaming, and assistant APIs. */
|
|
2
|
-
|
|
3
|
-
import type { Config, SlackConfig } from '../core/types.ts';
|
|
4
|
-
import type {
|
|
5
|
-
ChannelClient,
|
|
6
|
-
ChannelHandle,
|
|
7
|
-
MessageOptions,
|
|
8
|
-
StreamHandle,
|
|
9
|
-
StreamStartOptions,
|
|
10
|
-
} from './types.ts';
|
|
11
|
-
import type { ModalView } from './blocks.ts';
|
|
12
|
-
import type { SlashCommandBody } from './commands.ts';
|
|
13
|
-
|
|
14
|
-
import type { Handler } from './handler.ts';
|
|
15
|
-
|
|
16
|
-
import { SocketModeClient } from '@slack/socket-mode';
|
|
17
|
-
import { WebClient, LogLevel } from '@slack/web-api';
|
|
18
|
-
import { createCoalescer } from './coalesce.ts';
|
|
19
|
-
import { createHandler } from './handler.ts';
|
|
20
|
-
import { handleSlashCommand } from './commands.ts';
|
|
21
|
-
import { logger } from '../core/logger.ts';
|
|
22
|
-
import { isWithinTimeRange } from '../core/utils.ts';
|
|
23
|
-
|
|
24
|
-
class SlackChannelClient implements ChannelClient {
|
|
25
|
-
private readonly web: WebClient;
|
|
26
|
-
|
|
27
|
-
constructor(web: WebClient) {
|
|
28
|
-
this.web = web;
|
|
29
|
-
}
|
|
30
|
-
|
|
31
|
-
async postMessage(
|
|
32
|
-
channelId: string,
|
|
33
|
-
threadTs: string,
|
|
34
|
-
text: string,
|
|
35
|
-
opts?: MessageOptions,
|
|
36
|
-
): Promise<string> {
|
|
37
|
-
const result = await this.web.chat.postMessage({
|
|
38
|
-
channel: channelId,
|
|
39
|
-
thread_ts: threadTs,
|
|
40
|
-
text,
|
|
41
|
-
...(opts?.blocks ? { blocks: opts.blocks } : {}),
|
|
42
|
-
});
|
|
43
|
-
return result.ts ?? '';
|
|
44
|
-
}
|
|
45
|
-
|
|
46
|
-
async postEphemeral(
|
|
47
|
-
channelId: string,
|
|
48
|
-
threadTs: string,
|
|
49
|
-
userId: string,
|
|
50
|
-
text: string,
|
|
51
|
-
opts?: MessageOptions,
|
|
52
|
-
): Promise<void> {
|
|
53
|
-
await this.web.chat.postEphemeral({
|
|
54
|
-
channel: channelId,
|
|
55
|
-
thread_ts: threadTs,
|
|
56
|
-
user: userId,
|
|
57
|
-
text,
|
|
58
|
-
...(opts?.blocks ? { blocks: opts.blocks } : {}),
|
|
59
|
-
});
|
|
60
|
-
}
|
|
61
|
-
|
|
62
|
-
async openModal(triggerId: string, view: ModalView): Promise<void> {
|
|
63
|
-
await this.web.views.open({ trigger_id: triggerId, view });
|
|
64
|
-
}
|
|
65
|
-
|
|
66
|
-
async uploadFile(
|
|
67
|
-
channelId: string,
|
|
68
|
-
threadTs: string,
|
|
69
|
-
content: Buffer,
|
|
70
|
-
filename: string,
|
|
71
|
-
title: string,
|
|
72
|
-
): Promise<void> {
|
|
73
|
-
const upload = await this.web.files.getUploadURLExternal({
|
|
74
|
-
filename,
|
|
75
|
-
length: content.byteLength,
|
|
76
|
-
});
|
|
77
|
-
if (!upload.upload_url || !upload.file_id) return;
|
|
78
|
-
await fetch(upload.upload_url, { method: 'POST', body: new Uint8Array(content) });
|
|
79
|
-
await this.web.files.completeUploadExternal({
|
|
80
|
-
files: [{ id: upload.file_id, title }],
|
|
81
|
-
channel_id: channelId,
|
|
82
|
-
thread_ts: threadTs,
|
|
83
|
-
});
|
|
84
|
-
}
|
|
85
|
-
|
|
86
|
-
startStream(opts: StreamStartOptions): StreamHandle {
|
|
87
|
-
return this.web.chatStream({
|
|
88
|
-
channel: opts.channelId,
|
|
89
|
-
thread_ts: opts.threadTs,
|
|
90
|
-
recipient_team_id: opts.teamId,
|
|
91
|
-
recipient_user_id: opts.userId,
|
|
92
|
-
task_display_mode: 'timeline',
|
|
93
|
-
buffer_size: 128,
|
|
94
|
-
});
|
|
95
|
-
}
|
|
96
|
-
|
|
97
|
-
async setStatus(channelId: string, threadTs: string, status: string): Promise<void> {
|
|
98
|
-
await this.web.assistant.threads.setStatus({
|
|
99
|
-
channel_id: channelId,
|
|
100
|
-
thread_ts: threadTs,
|
|
101
|
-
status,
|
|
102
|
-
});
|
|
103
|
-
}
|
|
104
|
-
|
|
105
|
-
async setTitle(channelId: string, threadTs: string, title: string): Promise<void> {
|
|
106
|
-
await this.web.assistant.threads.setTitle({
|
|
107
|
-
channel_id: channelId,
|
|
108
|
-
thread_ts: threadTs,
|
|
109
|
-
title,
|
|
110
|
-
});
|
|
111
|
-
}
|
|
112
|
-
|
|
113
|
-
async setSuggestedPrompts(
|
|
114
|
-
channelId: string,
|
|
115
|
-
threadTs: string,
|
|
116
|
-
prompts: Array<{ title: string; message: string }>,
|
|
117
|
-
): Promise<void> {
|
|
118
|
-
await this.web.assistant.threads.setSuggestedPrompts({
|
|
119
|
-
channel_id: channelId,
|
|
120
|
-
thread_ts: threadTs,
|
|
121
|
-
prompts,
|
|
122
|
-
});
|
|
123
|
-
}
|
|
124
|
-
}
|
|
125
|
-
|
|
126
|
-
const IGNORED_SUBTYPES = new Set([
|
|
127
|
-
'bot_message',
|
|
128
|
-
'message_changed',
|
|
129
|
-
'message_deleted',
|
|
130
|
-
'message_replied',
|
|
131
|
-
'channel_join',
|
|
132
|
-
'channel_leave',
|
|
133
|
-
'channel_topic',
|
|
134
|
-
'channel_purpose',
|
|
135
|
-
'channel_name',
|
|
136
|
-
'channel_archive',
|
|
137
|
-
'channel_unarchive',
|
|
138
|
-
'file_share',
|
|
139
|
-
'pinned_item',
|
|
140
|
-
'unpinned_item',
|
|
141
|
-
]);
|
|
142
|
-
|
|
143
|
-
const DEDUP_TTL_MS = 30_000;
|
|
144
|
-
const DEDUP_CLEANUP_MS = 60_000;
|
|
145
|
-
|
|
146
|
-
function logDispatchError(err: unknown): void {
|
|
147
|
-
logger.error(`dispatch error: ${err instanceof Error ? err.message : String(err)}`);
|
|
148
|
-
}
|
|
149
|
-
|
|
150
|
-
interface SocketModeEvent {
|
|
151
|
-
ack: (response?: unknown) => Promise<void>;
|
|
152
|
-
envelope_id: string;
|
|
153
|
-
body: Record<string, unknown>;
|
|
154
|
-
event?: Record<string, unknown>;
|
|
155
|
-
retry_num?: number;
|
|
156
|
-
retry_reason?: string;
|
|
157
|
-
}
|
|
158
|
-
|
|
159
|
-
export function createSlackChannel(config: Config, signal: AbortSignal): ChannelHandle {
|
|
160
|
-
const recentEvents = new Map<string, number>();
|
|
161
|
-
let botUserId = '';
|
|
162
|
-
let teamId = '';
|
|
163
|
-
|
|
164
|
-
const socketClient = new SocketModeClient({
|
|
165
|
-
appToken: config.slack.appToken,
|
|
166
|
-
logLevel: LogLevel.WARN,
|
|
167
|
-
clientPingTimeout: 15_000,
|
|
168
|
-
serverPingTimeout: 60_000,
|
|
169
|
-
});
|
|
170
|
-
|
|
171
|
-
const webClient = new WebClient(config.slack.botToken);
|
|
172
|
-
const channelClient = new SlackChannelClient(webClient);
|
|
173
|
-
const handler = createHandler(channelClient, config, signal, () => teamId);
|
|
174
|
-
const coalescer = createCoalescer(config.slack.coalesce, (dispatch) => {
|
|
175
|
-
handler.handle(dispatch).catch(logDispatchError);
|
|
176
|
-
});
|
|
177
|
-
|
|
178
|
-
const dedupTimer = setInterval(() => {
|
|
179
|
-
const cutoff = Date.now() - DEDUP_TTL_MS;
|
|
180
|
-
for (const [key, ts] of recentEvents) {
|
|
181
|
-
if (ts < cutoff) recentEvents.delete(key);
|
|
182
|
-
}
|
|
183
|
-
}, DEDUP_CLEANUP_MS);
|
|
184
|
-
|
|
185
|
-
signal.addEventListener('abort', () => {
|
|
186
|
-
clearInterval(dedupTimer);
|
|
187
|
-
});
|
|
188
|
-
|
|
189
|
-
function isDuplicate(body: Record<string, unknown>): boolean {
|
|
190
|
-
const eventId = body['event_id'] as string | undefined;
|
|
191
|
-
if (!eventId) return false;
|
|
192
|
-
if (recentEvents.has(eventId)) return true;
|
|
193
|
-
recentEvents.set(eventId, Date.now());
|
|
194
|
-
return false;
|
|
195
|
-
}
|
|
196
|
-
|
|
197
|
-
socketClient.on('message', async ({ ack, body, event }: SocketModeEvent) => {
|
|
198
|
-
await ack();
|
|
199
|
-
if (!event) return;
|
|
200
|
-
if (event['user'] === botUserId) return;
|
|
201
|
-
|
|
202
|
-
const text = event['text'] as string | undefined;
|
|
203
|
-
if (!text) return;
|
|
204
|
-
|
|
205
|
-
const subtype = event['subtype'] as string | undefined;
|
|
206
|
-
if (subtype && IGNORED_SUBTYPES.has(subtype)) return;
|
|
207
|
-
if (event['bot_id']) return;
|
|
208
|
-
if (isDuplicate(body)) return;
|
|
209
|
-
|
|
210
|
-
const channelType = event['channel_type'] as string | undefined;
|
|
211
|
-
const isDm = channelType === 'im';
|
|
212
|
-
|
|
213
|
-
if (!isDm && config.slack.requireMention) return;
|
|
214
|
-
|
|
215
|
-
const channelId = event['channel'] as string;
|
|
216
|
-
if (!isChannelAllowed(channelId, config.slack.channels)) return;
|
|
217
|
-
if (!isDm && isQuietHours(config.slack.quietHours)) return;
|
|
218
|
-
|
|
219
|
-
const threadTs = (event['thread_ts'] as string | undefined) ?? (event['ts'] as string);
|
|
220
|
-
const userId = (event['user'] as string) ?? 'unknown';
|
|
221
|
-
const messageTs = event['ts'] as string;
|
|
222
|
-
const cleanText = stripMention(text);
|
|
223
|
-
|
|
224
|
-
if (!cleanText.trim()) return;
|
|
225
|
-
|
|
226
|
-
if (isDm && config.slack.coalesce.bypassDms) {
|
|
227
|
-
handler
|
|
228
|
-
.handle({
|
|
229
|
-
channelId,
|
|
230
|
-
threadTs,
|
|
231
|
-
messages: [{ userId, text: cleanText, timestamp: messageTs }],
|
|
232
|
-
})
|
|
233
|
-
.catch(logDispatchError);
|
|
234
|
-
} else {
|
|
235
|
-
coalescer.push(channelId, threadTs, userId, cleanText, messageTs);
|
|
236
|
-
}
|
|
237
|
-
});
|
|
238
|
-
|
|
239
|
-
if (config.slack.requireMention) {
|
|
240
|
-
socketClient.on('app_mention', async ({ ack, body, event }: SocketModeEvent) => {
|
|
241
|
-
await ack();
|
|
242
|
-
if (!event) return;
|
|
243
|
-
if (isDuplicate(body)) return;
|
|
244
|
-
|
|
245
|
-
const channelId = event['channel'] as string;
|
|
246
|
-
if (!isChannelAllowed(channelId, config.slack.channels)) return;
|
|
247
|
-
if (isQuietHours(config.slack.quietHours)) return;
|
|
248
|
-
|
|
249
|
-
const messageTs = event['ts'] as string | undefined;
|
|
250
|
-
if (!messageTs) return;
|
|
251
|
-
|
|
252
|
-
const threadTs = (event['thread_ts'] as string | undefined) ?? messageTs;
|
|
253
|
-
const userId = (event['user'] as string) ?? 'unknown';
|
|
254
|
-
const text = stripMention((event['text'] as string) ?? '');
|
|
255
|
-
|
|
256
|
-
if (!text.trim()) return;
|
|
257
|
-
|
|
258
|
-
coalescer.push(channelId, threadTs, userId, text, messageTs);
|
|
259
|
-
});
|
|
260
|
-
}
|
|
261
|
-
|
|
262
|
-
socketClient.on('assistant_thread_started', async ({ ack, event }: SocketModeEvent) => {
|
|
263
|
-
await ack();
|
|
264
|
-
if (!event) return;
|
|
265
|
-
const threadInfo = event['assistant_thread'] as Record<string, unknown> | undefined;
|
|
266
|
-
if (!threadInfo) return;
|
|
267
|
-
|
|
268
|
-
const channelId = threadInfo['channel_id'] as string | undefined;
|
|
269
|
-
const threadTs = threadInfo['thread_ts'] as string | undefined;
|
|
270
|
-
if (!channelId || !threadTs) return;
|
|
271
|
-
|
|
272
|
-
channelClient
|
|
273
|
-
.setSuggestedPrompts(channelId, threadTs, [
|
|
274
|
-
{ title: 'Browse a website', message: 'Browse https://example.com and summarize it' },
|
|
275
|
-
{ title: 'Run a command', message: 'List files in the current directory' },
|
|
276
|
-
{ title: 'Help me code', message: 'Help me write a function that...' },
|
|
277
|
-
])
|
|
278
|
-
.catch(() => {});
|
|
279
|
-
});
|
|
280
|
-
|
|
281
|
-
socketClient.on('assistant_thread_context_changed', async ({ ack }: SocketModeEvent) => {
|
|
282
|
-
await ack();
|
|
283
|
-
});
|
|
284
|
-
|
|
285
|
-
let connected = false;
|
|
286
|
-
const statusProvider = {
|
|
287
|
-
slackConnected: () => connected,
|
|
288
|
-
activeCount: () => handler.activeCount(),
|
|
289
|
-
};
|
|
290
|
-
|
|
291
|
-
socketClient.on('slash_commands', async ({ ack, body }: SocketModeEvent) => {
|
|
292
|
-
await ack();
|
|
293
|
-
const commandBody = body as unknown as SlashCommandBody;
|
|
294
|
-
if (commandBody.command !== '/bird') return;
|
|
295
|
-
|
|
296
|
-
try {
|
|
297
|
-
await handleSlashCommand(commandBody, webClient, channelClient, config, statusProvider);
|
|
298
|
-
} catch (err) {
|
|
299
|
-
logger.error(`/bird command error: ${err instanceof Error ? err.message : String(err)}`);
|
|
300
|
-
}
|
|
301
|
-
});
|
|
302
|
-
|
|
303
|
-
socketClient.on('interactive', async ({ ack, body }: SocketModeEvent) => {
|
|
304
|
-
await ack();
|
|
305
|
-
const interactionType = body['type'] as string | undefined;
|
|
306
|
-
|
|
307
|
-
if (interactionType === 'view_submission') {
|
|
308
|
-
const view = body['view'] as Record<string, unknown> | undefined;
|
|
309
|
-
if (view?.['callback_id'] === 'bird_create') {
|
|
310
|
-
await handleBirdCreateSubmission(view, webClient, config.timezone);
|
|
311
|
-
}
|
|
312
|
-
}
|
|
313
|
-
|
|
314
|
-
if (interactionType === 'block_actions') {
|
|
315
|
-
const actionsArr = body['actions'] as Array<Record<string, unknown>> | undefined;
|
|
316
|
-
const channel = (body['channel'] as Record<string, unknown> | undefined)?.['id'] as
|
|
317
|
-
| string
|
|
318
|
-
| undefined;
|
|
319
|
-
const user = (body['user'] as Record<string, unknown> | undefined)?.['id'] as
|
|
320
|
-
| string
|
|
321
|
-
| undefined;
|
|
322
|
-
if (!actionsArr || !channel) return;
|
|
323
|
-
|
|
324
|
-
for (const action of actionsArr) {
|
|
325
|
-
if (action['action_id'] !== 'session_error_overflow') continue;
|
|
326
|
-
const selected = (action['selected_option'] as Record<string, unknown> | undefined)?.[
|
|
327
|
-
'value'
|
|
328
|
-
] as string | undefined;
|
|
329
|
-
if (!selected) continue;
|
|
330
|
-
|
|
331
|
-
if (selected.startsWith('retry:')) {
|
|
332
|
-
const sessionUid = selected.slice('retry:'.length);
|
|
333
|
-
if (!sessionUid) continue;
|
|
334
|
-
await handleSessionRetry(sessionUid, channel, user ?? 'unknown', config, handler);
|
|
335
|
-
}
|
|
336
|
-
}
|
|
337
|
-
}
|
|
338
|
-
});
|
|
339
|
-
|
|
340
|
-
const MAX_CONSECUTIVE_FAILURES = 5;
|
|
341
|
-
|
|
342
|
-
socketClient.on('connected', () => {
|
|
343
|
-
connected = true;
|
|
344
|
-
logger.success('slack connected');
|
|
345
|
-
});
|
|
346
|
-
|
|
347
|
-
socketClient.on('reconnecting', () => {
|
|
348
|
-
connected = false;
|
|
349
|
-
logger.info('slack reconnecting...');
|
|
350
|
-
});
|
|
351
|
-
|
|
352
|
-
socketClient.on('disconnected', () => {
|
|
353
|
-
connected = false;
|
|
354
|
-
logger.warn('slack disconnected');
|
|
355
|
-
});
|
|
356
|
-
|
|
357
|
-
socketClient.on('close' as string, () => {
|
|
358
|
-
const failures = (socketClient as unknown as Record<string, unknown>)[
|
|
359
|
-
'numOfConsecutiveReconnectionFailures'
|
|
360
|
-
] as number | undefined;
|
|
361
|
-
if (failures != null && failures > MAX_CONSECUTIVE_FAILURES) {
|
|
362
|
-
(socketClient as unknown as Record<string, number>)['numOfConsecutiveReconnectionFailures'] =
|
|
363
|
-
1;
|
|
364
|
-
logger.info('reset reconnection back-off counter');
|
|
365
|
-
}
|
|
366
|
-
});
|
|
367
|
-
|
|
368
|
-
async function resolveChannelNames(): Promise<void> {
|
|
369
|
-
const namesToResolve = new Set<string>();
|
|
370
|
-
|
|
371
|
-
function collectNames(channels: string[]): void {
|
|
372
|
-
for (const ch of channels) {
|
|
373
|
-
if (ch !== '*' && !ch.startsWith('C') && !ch.startsWith('D') && !ch.startsWith('G')) {
|
|
374
|
-
namesToResolve.add(ch);
|
|
375
|
-
}
|
|
376
|
-
}
|
|
377
|
-
}
|
|
378
|
-
|
|
379
|
-
collectNames(config.slack.channels);
|
|
380
|
-
for (const agent of config.agents) {
|
|
381
|
-
collectNames(agent.channels);
|
|
382
|
-
}
|
|
383
|
-
if (namesToResolve.size === 0) return;
|
|
384
|
-
|
|
385
|
-
const nameToId = new Map<string, string>();
|
|
386
|
-
try {
|
|
387
|
-
let cursor: string | undefined;
|
|
388
|
-
do {
|
|
389
|
-
const result = await webClient.conversations.list({
|
|
390
|
-
types: 'public_channel,private_channel',
|
|
391
|
-
limit: 200,
|
|
392
|
-
exclude_archived: true,
|
|
393
|
-
cursor,
|
|
394
|
-
});
|
|
395
|
-
for (const ch of result.channels ?? []) {
|
|
396
|
-
if (ch.name && ch.id && namesToResolve.has(ch.name)) {
|
|
397
|
-
nameToId.set(ch.name, ch.id);
|
|
398
|
-
}
|
|
399
|
-
}
|
|
400
|
-
cursor = result.response_metadata?.next_cursor || undefined;
|
|
401
|
-
} while (cursor);
|
|
402
|
-
} catch (err) {
|
|
403
|
-
logger.warn(
|
|
404
|
-
`failed to resolve channel names: ${err instanceof Error ? err.message : String(err)}`,
|
|
405
|
-
);
|
|
406
|
-
return;
|
|
407
|
-
}
|
|
408
|
-
|
|
409
|
-
function resolveList(channels: string[], label: string): string[] {
|
|
410
|
-
return channels.map((ch) => {
|
|
411
|
-
const resolved = nameToId.get(ch);
|
|
412
|
-
if (resolved) {
|
|
413
|
-
logger.info(`${label}: resolved channel "${ch}" -> ${resolved}`);
|
|
414
|
-
return resolved;
|
|
415
|
-
}
|
|
416
|
-
if (namesToResolve.has(ch)) {
|
|
417
|
-
logger.warn(`${label}: channel "${ch}" not found in workspace`);
|
|
418
|
-
}
|
|
419
|
-
return ch;
|
|
420
|
-
});
|
|
421
|
-
}
|
|
422
|
-
|
|
423
|
-
config.slack.channels = resolveList(config.slack.channels, 'slack');
|
|
424
|
-
for (const agent of config.agents) {
|
|
425
|
-
agent.channels = resolveList(agent.channels, `agent "${agent.id}"`);
|
|
426
|
-
}
|
|
427
|
-
}
|
|
428
|
-
|
|
429
|
-
async function start(): Promise<void> {
|
|
430
|
-
const authResult = await webClient.auth.test();
|
|
431
|
-
botUserId = (authResult.user_id as string) ?? '';
|
|
432
|
-
teamId = (authResult.team_id as string) ?? '';
|
|
433
|
-
logger.info(`authenticated as ${authResult.user} (team: ${teamId})`);
|
|
434
|
-
|
|
435
|
-
await resolveChannelNames();
|
|
436
|
-
|
|
437
|
-
await socketClient.start();
|
|
438
|
-
connected = true;
|
|
439
|
-
}
|
|
440
|
-
|
|
441
|
-
async function stop(): Promise<void> {
|
|
442
|
-
connected = false;
|
|
443
|
-
coalescer.destroy();
|
|
444
|
-
handler.killAll();
|
|
445
|
-
clearInterval(dedupTimer);
|
|
446
|
-
await socketClient.disconnect();
|
|
447
|
-
logger.info('slack stopped');
|
|
448
|
-
}
|
|
449
|
-
|
|
450
|
-
function isConnected(): boolean {
|
|
451
|
-
return connected;
|
|
452
|
-
}
|
|
453
|
-
|
|
454
|
-
async function postMessage(channel: string, text: string, opts?: MessageOptions): Promise<void> {
|
|
455
|
-
await webClient.chat.postMessage({
|
|
456
|
-
channel,
|
|
457
|
-
text,
|
|
458
|
-
...(opts?.blocks ? { blocks: opts.blocks } : {}),
|
|
459
|
-
});
|
|
460
|
-
}
|
|
461
|
-
|
|
462
|
-
return { start, stop, isConnected, activeCount: () => handler.activeCount(), postMessage };
|
|
463
|
-
}
|
|
464
|
-
|
|
465
|
-
async function handleBirdCreateSubmission(
|
|
466
|
-
view: Record<string, unknown>,
|
|
467
|
-
webClient: WebClient,
|
|
468
|
-
defaultTimezone: string,
|
|
469
|
-
): Promise<void> {
|
|
470
|
-
try {
|
|
471
|
-
const values = view['state'] as Record<string, unknown> | undefined;
|
|
472
|
-
const stateValues = (values?.['values'] ?? {}) as Record<
|
|
473
|
-
string,
|
|
474
|
-
Record<string, Record<string, unknown>>
|
|
475
|
-
>;
|
|
476
|
-
|
|
477
|
-
const name = (stateValues['bird_name']?.['name_input']?.['value'] as string | undefined) ?? '';
|
|
478
|
-
const schedule = (
|
|
479
|
-
stateValues['bird_schedule']?.['schedule_select']?.['selected_option'] as
|
|
480
|
-
| Record<string, unknown>
|
|
481
|
-
| undefined
|
|
482
|
-
)?.['value'] as string | undefined;
|
|
483
|
-
const prompt =
|
|
484
|
-
(stateValues['bird_prompt']?.['prompt_input']?.['value'] as string | undefined) ?? '';
|
|
485
|
-
const channelId =
|
|
486
|
-
(stateValues['bird_channel']?.['channel_select']?.['selected_conversation'] as
|
|
487
|
-
| string
|
|
488
|
-
| undefined) ?? '';
|
|
489
|
-
const enabledValue = (
|
|
490
|
-
stateValues['bird_enabled']?.['enabled_radio']?.['selected_option'] as
|
|
491
|
-
| Record<string, unknown>
|
|
492
|
-
| undefined
|
|
493
|
-
)?.['value'] as string | undefined;
|
|
494
|
-
|
|
495
|
-
if (!name || !schedule || !prompt) {
|
|
496
|
-
logger.warn('bird_create submission missing required fields');
|
|
497
|
-
return;
|
|
498
|
-
}
|
|
499
|
-
|
|
500
|
-
const { createCronJob, setCronJobEnabled } = await import('../db/index.ts');
|
|
501
|
-
const bird = createCronJob(
|
|
502
|
-
name,
|
|
503
|
-
schedule,
|
|
504
|
-
prompt,
|
|
505
|
-
channelId || undefined,
|
|
506
|
-
'default',
|
|
507
|
-
defaultTimezone,
|
|
508
|
-
);
|
|
509
|
-
if (enabledValue !== 'enabled') {
|
|
510
|
-
setCronJobEnabled(bird.uid, false);
|
|
511
|
-
}
|
|
512
|
-
|
|
513
|
-
await webClient.chat.postMessage({
|
|
514
|
-
channel: channelId || 'general',
|
|
515
|
-
text: `Bird *${name}* created. Schedule: \`${schedule}\``,
|
|
516
|
-
});
|
|
517
|
-
|
|
518
|
-
logger.info(`bird created via modal: ${name}`);
|
|
519
|
-
} catch (err) {
|
|
520
|
-
logger.error(
|
|
521
|
-
`bird_create submission error: ${err instanceof Error ? err.message : String(err)}`,
|
|
522
|
-
);
|
|
523
|
-
}
|
|
524
|
-
}
|
|
525
|
-
|
|
526
|
-
async function handleSessionRetry(
|
|
527
|
-
sessionUid: string,
|
|
528
|
-
channelId: string,
|
|
529
|
-
userId: string,
|
|
530
|
-
config: Config,
|
|
531
|
-
handler: Handler,
|
|
532
|
-
): Promise<void> {
|
|
533
|
-
try {
|
|
534
|
-
const { getSession, getLastInboundMessage } = await import('../db/index.ts');
|
|
535
|
-
const session = getSession(sessionUid);
|
|
536
|
-
if (!session) {
|
|
537
|
-
logger.warn(`retry: session ${sessionUid} not found`);
|
|
538
|
-
return;
|
|
539
|
-
}
|
|
540
|
-
|
|
541
|
-
const lastMsg = getLastInboundMessage(session.channel_id, session.thread_id);
|
|
542
|
-
if (!lastMsg) {
|
|
543
|
-
logger.warn(`retry: no inbound message for session ${sessionUid}`);
|
|
544
|
-
return;
|
|
545
|
-
}
|
|
546
|
-
|
|
547
|
-
handler
|
|
548
|
-
.handle({
|
|
549
|
-
channelId: session.channel_id,
|
|
550
|
-
threadTs: session.thread_id ?? lastMsg.timestamp,
|
|
551
|
-
messages: [{ userId, text: lastMsg.content, timestamp: lastMsg.timestamp }],
|
|
552
|
-
})
|
|
553
|
-
.catch(logDispatchError);
|
|
554
|
-
|
|
555
|
-
logger.info(`retry: session ${sessionUid} re-dispatched by ${userId}`);
|
|
556
|
-
} catch (err) {
|
|
557
|
-
logger.error(`retry error: ${err instanceof Error ? err.message : String(err)}`);
|
|
558
|
-
}
|
|
559
|
-
}
|
|
560
|
-
|
|
561
|
-
function isChannelAllowed(channelId: string, channels: string[]): boolean {
|
|
562
|
-
if (channels.includes('*')) return true;
|
|
563
|
-
return channels.includes(channelId);
|
|
564
|
-
}
|
|
565
|
-
|
|
566
|
-
function isQuietHours(quietHours: SlackConfig['quietHours']): boolean {
|
|
567
|
-
if (!quietHours.enabled) return false;
|
|
568
|
-
return isWithinTimeRange(quietHours.start, quietHours.end, new Date(), quietHours.timezone);
|
|
569
|
-
}
|
|
570
|
-
|
|
571
|
-
function stripMention(text: string): string {
|
|
572
|
-
return text.replace(/^\s*<@[A-Z0-9]+>\s*/i, '').trim();
|
|
573
|
-
}
|
package/src/channel/types.ts
DELETED
|
@@ -1,59 +0,0 @@
|
|
|
1
|
-
/** @fileoverview Platform-agnostic channel client interface. */
|
|
2
|
-
|
|
3
|
-
import type { Block, ModalView } from './blocks.ts';
|
|
4
|
-
|
|
5
|
-
export interface MessageOptions {
|
|
6
|
-
blocks?: Block[];
|
|
7
|
-
}
|
|
8
|
-
|
|
9
|
-
export interface StreamHandle {
|
|
10
|
-
append(args: { markdown_text?: string }): Promise<unknown>;
|
|
11
|
-
stop(args?: { blocks?: Block[] }): Promise<unknown>;
|
|
12
|
-
}
|
|
13
|
-
|
|
14
|
-
export interface StreamStartOptions {
|
|
15
|
-
channelId: string;
|
|
16
|
-
threadTs: string;
|
|
17
|
-
teamId: string;
|
|
18
|
-
userId: string;
|
|
19
|
-
}
|
|
20
|
-
|
|
21
|
-
export interface ChannelClient {
|
|
22
|
-
postMessage(
|
|
23
|
-
channelId: string,
|
|
24
|
-
threadTs: string,
|
|
25
|
-
text: string,
|
|
26
|
-
opts?: MessageOptions,
|
|
27
|
-
): Promise<string>;
|
|
28
|
-
postEphemeral(
|
|
29
|
-
channelId: string,
|
|
30
|
-
threadTs: string,
|
|
31
|
-
userId: string,
|
|
32
|
-
text: string,
|
|
33
|
-
opts?: MessageOptions,
|
|
34
|
-
): Promise<void>;
|
|
35
|
-
uploadFile(
|
|
36
|
-
channelId: string,
|
|
37
|
-
threadTs: string,
|
|
38
|
-
content: Buffer,
|
|
39
|
-
filename: string,
|
|
40
|
-
title: string,
|
|
41
|
-
): Promise<void>;
|
|
42
|
-
startStream(opts: StreamStartOptions): StreamHandle;
|
|
43
|
-
openModal?(triggerId: string, view: ModalView): Promise<void>;
|
|
44
|
-
setStatus?(channelId: string, threadTs: string, status: string): Promise<void>;
|
|
45
|
-
setTitle?(channelId: string, threadTs: string, title: string): Promise<void>;
|
|
46
|
-
setSuggestedPrompts?(
|
|
47
|
-
channelId: string,
|
|
48
|
-
threadTs: string,
|
|
49
|
-
prompts: Array<{ title: string; message: string }>,
|
|
50
|
-
): Promise<void>;
|
|
51
|
-
}
|
|
52
|
-
|
|
53
|
-
export interface ChannelHandle {
|
|
54
|
-
start(): Promise<void>;
|
|
55
|
-
stop(): Promise<void>;
|
|
56
|
-
isConnected(): boolean;
|
|
57
|
-
activeCount(): number;
|
|
58
|
-
postMessage(channel: string, text: string, opts?: MessageOptions): Promise<void>;
|
|
59
|
-
}
|
package/src/cli/banner.ts
DELETED
|
@@ -1,10 +0,0 @@
|
|
|
1
|
-
/** @fileoverview ASCII banner displayed on daemon startup and in help text. */
|
|
2
|
-
|
|
3
|
-
import { createRequire } from 'node:module';
|
|
4
|
-
|
|
5
|
-
const require = createRequire(import.meta.url);
|
|
6
|
-
export const VERSION: string = (require('../../package.json') as { version: string }).version;
|
|
7
|
-
|
|
8
|
-
const BIRD = [' .__.', ' ( ^>', ' / )\\', ' <_/_/', ' " "'].join('\n');
|
|
9
|
-
|
|
10
|
-
export const BANNER = BIRD;
|