@gitterm/slack-bot 0.1.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/index.js ADDED
@@ -0,0 +1,573 @@
1
+ // src/index.ts
2
+ import { createBot, withSavedConfig as loadSaved } from "@gitterm/bot";
3
+
4
+ // src/adapter.ts
5
+ import { downloadImage } from "@gitterm/bot";
6
+ import {
7
+ App,
8
+ LogLevel,
9
+ webApi
10
+ } from "@slack/bolt";
11
+
12
+ // src/blocks.ts
13
+ import { splitMessage, tablesToCode } from "@gitterm/bot";
14
+ var ACTION = {
15
+ option: /^gitterm_option_\d+$/,
16
+ toggle: "gitterm_toggle",
17
+ submit: "gitterm_submit",
18
+ custom: "gitterm_custom",
19
+ permission: /^gitterm_permission_(once|always|reject)$/
20
+ };
21
+ var CUSTOM_MODAL = "gitterm_custom_modal";
22
+ var CUSTOM_INPUT = "gitterm_custom_text";
23
+ var BLOCK_PREFIX = "gitterm";
24
+ var BUTTONS_PER_ROW = 5;
25
+ var CHECKBOX_LIMIT = 10;
26
+ var LABEL_LIMIT = 75;
27
+ var SECTION_LIMIT = 2900;
28
+ var SECTIONS_PER_MESSAGE = 10;
29
+ var OPEN_COLOR = "#E8A33D";
30
+ var SETTLED_COLOR = "#B8BFC7";
31
+ var truncate = (value, limit) => value.length > limit ? `${value.slice(0, limit - 1)}…` : value;
32
+ var plain = (text, limit = LABEL_LIMIT) => ({
33
+ type: "plain_text",
34
+ text: truncate(text, limit)
35
+ });
36
+ var context = (text) => ({
37
+ type: "context",
38
+ elements: [{ type: "mrkdwn", text }]
39
+ });
40
+ var section = (text) => ({ type: "section", text: { type: "mrkdwn", text } });
41
+ var blockId = (promptId, row = 0) => `${BLOCK_PREFIX}|${promptId}|${row}`;
42
+ var promptIdOf = (id) => {
43
+ const [prefix, promptId] = id.split("|");
44
+ return prefix === BLOCK_PREFIX && promptId ? promptId : null;
45
+ };
46
+ function toMrkdwn(markdown) {
47
+ let inCode = false;
48
+ return markdown.split(`
49
+ `).map((line) => {
50
+ if (line.trimStart().startsWith("```")) {
51
+ inCode = !inCode;
52
+ return line;
53
+ }
54
+ if (inCode)
55
+ return line;
56
+ return line.replace(/^#{1,6}\s+(.+)$/, "*$1*").replace(/\*\*(.+?)\*\*/g, "*$1*").replace(/~~(.+?)~~/g, "~$1~").replace(/\[([^\]]+)]\((https?:\/\/[^)]+)\)/g, "<$2|$1>");
57
+ }).join(`
58
+ `);
59
+ }
60
+ function answerMessages(markdown, footer) {
61
+ const quiet = markdown.replace(/<!(channel|here|everyone)[^>]*>/g, "@$1");
62
+ const sections = splitMessage(toMrkdwn(tablesToCode(quiet)), SECTION_LIMIT);
63
+ const messages = [];
64
+ for (let index = 0;index < sections.length; index += SECTIONS_PER_MESSAGE) {
65
+ const group = sections.slice(index, index + SECTIONS_PER_MESSAGE);
66
+ const last = index + SECTIONS_PER_MESSAGE >= sections.length;
67
+ messages.push({
68
+ text: group.join(`
69
+
70
+ `),
71
+ blocks: [...group.map(section), ...last ? [{ type: "divider" }, context(footer)] : []]
72
+ });
73
+ }
74
+ return messages;
75
+ }
76
+ function leadIn(prompt, mention) {
77
+ const who = mention ? ` for <@${prompt.requester.id}>` : "";
78
+ if (prompt.kind === "permission")
79
+ return context(`*Approval needed${who}*`);
80
+ const counter = prompt.total > 1 ? `Question ${prompt.index + 1} of ${prompt.total}` : "Question";
81
+ return context(`*${counter}${who}* · ${prompt.question.header}`);
82
+ }
83
+ function body(prompt) {
84
+ if (prompt.kind === "permission") {
85
+ const { request } = prompt;
86
+ const patterns = request.patterns.filter(Boolean);
87
+ return [
88
+ section(`The agent wants to run *${request.title}*`),
89
+ ...patterns.length ? [section(`\`\`\`${patterns.join(`
90
+ `).slice(0, 2800)}\`\`\``)] : []
91
+ ];
92
+ }
93
+ const { question } = prompt;
94
+ const options = question.options.map((option, i) => `${i + 1}. *${option.label}*${option.description ? ` — ${option.description}` : ""}`).join(`
95
+ `);
96
+ return [section(`*${question.question}*`), ...options ? [section(options)] : []];
97
+ }
98
+ var customButton = {
99
+ type: "button",
100
+ action_id: ACTION.custom,
101
+ text: plain("Write your own answer"),
102
+ value: "custom"
103
+ };
104
+ function controls(prompt) {
105
+ if (prompt.kind === "permission") {
106
+ return [
107
+ {
108
+ type: "actions",
109
+ block_id: blockId(prompt.id),
110
+ elements: [
111
+ { label: "Allow once", value: "once", style: "primary" },
112
+ { label: "Always allow", value: "always" },
113
+ { label: "Deny", value: "reject", style: "danger" }
114
+ ].map((button) => ({
115
+ type: "button",
116
+ action_id: `gitterm_permission_${button.value}`,
117
+ text: plain(button.label),
118
+ value: button.value,
119
+ ...button.style ? { style: button.style } : {}
120
+ }))
121
+ },
122
+ context("Or reply *yes*, *always*, or *no* in this thread.")
123
+ ];
124
+ }
125
+ const { question } = prompt;
126
+ const rows = [];
127
+ if (question.multiple && question.options.length > 0) {
128
+ rows.push({
129
+ type: "actions",
130
+ block_id: blockId(prompt.id),
131
+ elements: [
132
+ {
133
+ type: "checkboxes",
134
+ action_id: ACTION.toggle,
135
+ options: question.options.slice(0, CHECKBOX_LIMIT).map((option, i) => ({
136
+ text: plain(option.label),
137
+ ...option.description ? { description: plain(option.description) } : {},
138
+ value: String(i)
139
+ }))
140
+ },
141
+ {
142
+ type: "button",
143
+ action_id: ACTION.submit,
144
+ style: "primary",
145
+ text: plain("Submit"),
146
+ value: "submit"
147
+ },
148
+ ...question.custom ? [customButton] : []
149
+ ]
150
+ });
151
+ } else {
152
+ for (let start = 0;start < question.options.length; start += BUTTONS_PER_ROW) {
153
+ rows.push({
154
+ type: "actions",
155
+ block_id: blockId(prompt.id, rows.length),
156
+ elements: question.options.slice(start, start + BUTTONS_PER_ROW).map((option, i) => ({
157
+ type: "button",
158
+ action_id: `gitterm_option_${start + i}`,
159
+ text: plain(option.label),
160
+ value: String(start + i)
161
+ }))
162
+ });
163
+ }
164
+ if (question.custom) {
165
+ rows.push({
166
+ type: "actions",
167
+ block_id: blockId(prompt.id, rows.length),
168
+ elements: [customButton]
169
+ });
170
+ }
171
+ }
172
+ const hint = question.options.length > 0 ? question.multiple ? "Tick options and press *Submit*, or reply with their numbers (e.g. `1, 3`)." : "Press a button or reply with its number." : "Reply in this thread with your answer.";
173
+ return [...rows, context(hint)];
174
+ }
175
+ function promptAttachment(prompt) {
176
+ const fallback = prompt.kind === "permission" ? `<@${prompt.requester.id}> the agent needs approval: ${prompt.request.title}` : `<@${prompt.requester.id}> the agent has a question: ${prompt.question.header}`;
177
+ return {
178
+ color: OPEN_COLOR,
179
+ fallback,
180
+ blocks: [leadIn(prompt, true), ...body(prompt), ...controls(prompt)]
181
+ };
182
+ }
183
+ function settledAttachment(prompt, outcome) {
184
+ return {
185
+ color: SETTLED_COLOR,
186
+ fallback: outcome,
187
+ blocks: [leadIn(prompt, false), ...body(prompt), context(outcome)]
188
+ };
189
+ }
190
+ function customAnswerView(prompt) {
191
+ return {
192
+ type: "modal",
193
+ callback_id: CUSTOM_MODAL,
194
+ private_metadata: prompt.id,
195
+ title: plain(prompt.question.header || "Your answer", 24),
196
+ submit: plain("Send to the agent"),
197
+ close: plain("Cancel"),
198
+ blocks: [
199
+ section(prompt.question.question),
200
+ {
201
+ type: "input",
202
+ block_id: "answer",
203
+ label: plain("Your answer"),
204
+ element: { type: "plain_text_input", action_id: CUSTOM_INPUT, multiline: true }
205
+ }
206
+ ]
207
+ };
208
+ }
209
+
210
+ // src/adapter.ts
211
+ var HISTORY_LIMIT = 200;
212
+ function required(value, name) {
213
+ if (!value?.trim())
214
+ throw new Error(`${name} is required`);
215
+ return value.trim();
216
+ }
217
+ function createSlackAdapter(options = {}) {
218
+ const botToken = required(options.botToken ?? options.app?.client.token ?? process.env.SLACK_BOT_TOKEN, "SLACK_BOT_TOKEN");
219
+ const owned = !options.app;
220
+ const app = options.app ?? new App({
221
+ token: botToken,
222
+ appToken: required(options.appToken ?? process.env.SLACK_APP_TOKEN, "SLACK_APP_TOKEN"),
223
+ socketMode: true,
224
+ logLevel: LogLevel.WARN
225
+ });
226
+ const client = owned ? app.client : new webApi.WebClient(botToken);
227
+ const users = new Map;
228
+ const ticked = new Map;
229
+ let indicatorAvailable = true;
230
+ let teamId = "";
231
+ let botUserId = "";
232
+ const stripMention = (text) => text.replaceAll(`<@${botUserId}>`, "").trim();
233
+ const user = async (id) => {
234
+ if (!id)
235
+ return { id: "unknown", name: "someone", guest: true };
236
+ const cached = users.get(id);
237
+ if (cached && Date.now() - cached.at < 60000)
238
+ return cached.user;
239
+ const known = await client.users.info({ user: id }).then(({ user: profile }) => ({
240
+ id,
241
+ name: profile?.profile?.display_name || profile?.real_name || profile?.name || id,
242
+ guest: Boolean(!profile || profile.deleted || profile?.is_restricted || profile?.is_ultra_restricted || profile?.team_id && teamId && profile.team_id !== teamId)
243
+ })).catch(() => ({ id, name: id, guest: true }));
244
+ users.delete(id);
245
+ users.set(id, { user: known, at: Date.now() });
246
+ if (users.size > 2000)
247
+ users.delete(users.keys().next().value);
248
+ return known;
249
+ };
250
+ const file = (entry) => ({
251
+ id: entry.id,
252
+ name: entry.name ?? entry.id,
253
+ mime: entry.mimetype ?? "application/octet-stream",
254
+ size: entry.size ?? -1,
255
+ async download(signal) {
256
+ return downloadImage(entry.url_private_download ?? "", {
257
+ hosts: ["files.slack.com", "files-pri.slack.com"],
258
+ signal,
259
+ headers: { Authorization: `Bearer ${botToken}` }
260
+ }).catch((error) => {
261
+ throw new Error(`Could not download “${entry.name ?? entry.id}” from Slack; the app needs the files:read scope (${error instanceof Error ? error.message : error})`);
262
+ });
263
+ }
264
+ });
265
+ const fromBot = (message) => Boolean(message.bot_id) || message.subtype === "bot_message" || message.user === botUserId;
266
+ return {
267
+ platform: "slack",
268
+ displayName: "Slack",
269
+ async start(events) {
270
+ const sameTeam = (id) => !id || id === teamId;
271
+ app.event("app_mention", async ({ event, context }) => {
272
+ if (!sameTeam(context.teamId) || event.bot_id || !event.user)
273
+ return;
274
+ if (event.channel.startsWith("D"))
275
+ return;
276
+ events.message({
277
+ id: event.ts,
278
+ thread: { channel: event.channel, thread: event.thread_ts ?? event.ts },
279
+ author: await user(event.user),
280
+ text: stripMention(event.text),
281
+ files: (event.files ?? []).map(file),
282
+ mentioned: true,
283
+ inThread: Boolean(event.thread_ts)
284
+ });
285
+ });
286
+ app.message(async ({ message, context }) => {
287
+ const reply = message;
288
+ if (!sameTeam(context.teamId) || fromBot(reply) || !reply.user || !reply.ts)
289
+ return;
290
+ if (reply.subtype && reply.subtype !== "file_share")
291
+ return;
292
+ if (reply.channel_type === "im") {
293
+ events.message({
294
+ id: reply.ts,
295
+ thread: { channel: reply.channel, thread: reply.thread_ts ?? reply.ts },
296
+ author: await user(reply.user),
297
+ text: stripMention(reply.text ?? ""),
298
+ files: (reply.files ?? []).map(file),
299
+ mentioned: true,
300
+ inThread: Boolean(reply.thread_ts),
301
+ direct: true
302
+ });
303
+ return;
304
+ }
305
+ if (!reply.thread_ts || reply.thread_ts === reply.ts)
306
+ return;
307
+ if (reply.text?.includes(`<@${botUserId}>`))
308
+ return;
309
+ events.message({
310
+ id: reply.ts,
311
+ thread: { channel: reply.channel, thread: reply.thread_ts },
312
+ author: await user(reply.user),
313
+ text: reply.text?.trim() ?? "",
314
+ files: (reply.files ?? []).map(file),
315
+ mentioned: false,
316
+ inThread: true
317
+ });
318
+ });
319
+ const answer = async (body, promptId, value) => promptId === null ? "stale" : events.answer(promptId, value, await user(body.user.id));
320
+ const notice = (text) => ({
321
+ response_type: "ephemeral",
322
+ replace_original: false,
323
+ text
324
+ });
325
+ const stale = notice("This was already answered.");
326
+ const outcomeNotice = (outcome) => outcome === "forbidden" ? notice("Only people allowed to use this bot can answer.") : outcome === "stale" ? stale : undefined;
327
+ app.action(ACTION.option, async ({ ack, body, action, respond }) => {
328
+ await ack();
329
+ const promptId = promptIdOf(action.block_id);
330
+ const prompt = promptId ? events.prompt(promptId) : undefined;
331
+ const label = prompt?.kind === "question" ? prompt.question.options[Number(action.value)]?.label : undefined;
332
+ const reply = label ? outcomeNotice(await answer(body, promptId, { kind: "question", labels: [label] })) : stale;
333
+ if (reply)
334
+ await respond(reply);
335
+ });
336
+ app.action(ACTION.toggle, async ({ ack, action, body }) => {
337
+ await ack();
338
+ const promptId = promptIdOf(action.block_id);
339
+ if (promptId && events.canAnswer && !events.canAnswer(promptId, await user(body.user.id)))
340
+ return;
341
+ if (promptId)
342
+ ticked.set(promptId, action.selected_options.map((option) => Number(option.value)));
343
+ });
344
+ app.action(ACTION.submit, async ({ ack, body, action, respond }) => {
345
+ await ack();
346
+ const promptId = promptIdOf(action.block_id);
347
+ const prompt = promptId ? events.prompt(promptId) : undefined;
348
+ if (prompt?.kind !== "question" || !promptId) {
349
+ await respond(stale);
350
+ return;
351
+ }
352
+ const labels = (ticked.get(promptId) ?? []).map((index) => prompt.question.options[index]?.label).filter((label) => Boolean(label));
353
+ if (labels.length === 0) {
354
+ await respond({ ...stale, text: "Tick at least one option before pressing Submit." });
355
+ return;
356
+ }
357
+ const reply = outcomeNotice(await answer(body, promptId, { kind: "question", labels }));
358
+ if (reply)
359
+ await respond(reply);
360
+ });
361
+ app.action(ACTION.permission, async ({ ack, body, action, respond }) => {
362
+ await ack();
363
+ const response = action.value;
364
+ const reply = outcomeNotice(await answer(body, promptIdOf(action.block_id), { kind: "permission", response }));
365
+ if (reply)
366
+ await respond(reply);
367
+ });
368
+ app.action(ACTION.custom, async ({ ack, body, action, client: web, respond }) => {
369
+ await ack();
370
+ const promptId = promptIdOf(action.block_id);
371
+ const prompt = promptId ? events.prompt(promptId) : undefined;
372
+ if (prompt?.kind !== "question") {
373
+ await respond(stale);
374
+ return;
375
+ }
376
+ await web.views.open({ trigger_id: body.trigger_id, view: customAnswerView(prompt) });
377
+ });
378
+ app.view(CUSTOM_MODAL, async ({ ack, body, view }) => {
379
+ const promptId = view.private_metadata;
380
+ const prompt = events.prompt(promptId);
381
+ const text = view.state.values.answer?.[CUSTOM_INPUT]?.value?.trim() ?? "";
382
+ const labels = prompt?.kind === "question" && prompt.question.multiple ? (ticked.get(promptId) ?? []).map((index) => prompt.question.options[index]?.label ?? "") : [];
383
+ const outcome = text ? events.answer(promptId, { kind: "question", labels: [...new Set([...labels.filter(Boolean), text])] }, await user(body.user.id)) : "empty";
384
+ if (outcome === "answered")
385
+ return ack();
386
+ await ack({
387
+ response_action: "errors",
388
+ errors: {
389
+ answer: outcome === "empty" ? "Write an answer first." : outcome === "forbidden" ? "Only people allowed to use this bot can answer." : "This question was already answered."
390
+ }
391
+ });
392
+ });
393
+ if (owned) {
394
+ app.error(async (error) => {
395
+ console.error("Slack error", error);
396
+ });
397
+ await app.start();
398
+ }
399
+ const identity = await client.auth.test();
400
+ teamId = identity.team_id ?? "";
401
+ botUserId = identity.user_id ?? "";
402
+ return { scope: teamId };
403
+ },
404
+ async stop() {
405
+ if (owned)
406
+ await app.stop();
407
+ },
408
+ async history(thread, after) {
409
+ const messages = [];
410
+ let cursor;
411
+ for (let page = 0;page < 5; page++) {
412
+ const replies = await client.conversations.replies({
413
+ channel: thread.channel,
414
+ ts: thread.thread,
415
+ limit: HISTORY_LIMIT,
416
+ ...after ? { oldest: after, inclusive: false } : {},
417
+ ...cursor ? { cursor } : {}
418
+ });
419
+ messages.push(...replies.messages ?? []);
420
+ cursor = replies.response_metadata?.next_cursor?.trim();
421
+ if (!cursor)
422
+ break;
423
+ }
424
+ const result = [];
425
+ for (const message of messages) {
426
+ if (!message.ts || after && Number(message.ts) <= Number(after))
427
+ continue;
428
+ const bot = fromBot(message);
429
+ result.push({
430
+ id: message.ts,
431
+ author: bot ? { id: message.user ?? "bot", name: "assistant" } : await user(message.user),
432
+ fromBot: bot,
433
+ text: stripMention(message.text ?? ""),
434
+ files: (message.files ?? []).map(file)
435
+ });
436
+ }
437
+ return result;
438
+ },
439
+ async post(thread, text) {
440
+ const posted = await client.chat.postMessage({
441
+ channel: thread.channel,
442
+ thread_ts: thread.thread,
443
+ text
444
+ });
445
+ if (!posted.ts)
446
+ throw new Error("Slack did not return a message timestamp");
447
+ return posted.ts;
448
+ },
449
+ async edit(thread, messageId, text) {
450
+ await client.chat.update({ channel: thread.channel, ts: messageId, text });
451
+ },
452
+ async remove(thread, messageId) {
453
+ await client.chat.delete({ channel: thread.channel, ts: messageId });
454
+ },
455
+ async reply(thread, markdown, footer, delivery) {
456
+ const messages = answerMessages(markdown, footer);
457
+ for (let index = delivery?.sent ?? 0;index < messages.length; index++) {
458
+ await client.chat.postMessage({
459
+ channel: thread.channel,
460
+ thread_ts: thread.thread,
461
+ ...messages[index]
462
+ });
463
+ await delivery?.recordSent(index + 1);
464
+ }
465
+ },
466
+ async ask(thread, prompt) {
467
+ const posted = await client.chat.postMessage({
468
+ channel: thread.channel,
469
+ thread_ts: thread.thread,
470
+ attachments: [promptAttachment(prompt)]
471
+ });
472
+ if (!posted.ts)
473
+ throw new Error("Slack did not return a message timestamp");
474
+ return posted.ts;
475
+ },
476
+ async settle(thread, messageId, prompt, outcome) {
477
+ ticked.delete(prompt.id);
478
+ await client.chat.update({
479
+ channel: thread.channel,
480
+ ts: messageId,
481
+ text: "",
482
+ attachments: [settledAttachment(prompt, outcome)]
483
+ });
484
+ },
485
+ async indicate(thread, status) {
486
+ if (!indicatorAvailable)
487
+ return false;
488
+ try {
489
+ await client.assistant.threads.setStatus({
490
+ channel_id: thread.channel,
491
+ thread_ts: thread.thread,
492
+ status
493
+ });
494
+ return true;
495
+ } catch (error) {
496
+ if (error.code === "slack_webapi_platform_error") {
497
+ indicatorAvailable = false;
498
+ console.warn("Slack refused the native status indicator; using status messages instead", error);
499
+ }
500
+ return false;
501
+ }
502
+ },
503
+ async mark(message, state) {
504
+ await client.reactions.add({
505
+ channel: message.thread.channel,
506
+ timestamp: message.id,
507
+ name: { seen: "eyes", done: "white_check_mark", failed: "x" }[state]
508
+ });
509
+ }
510
+ };
511
+ }
512
+ // src/manifest.ts
513
+ function slackManifest(name = "GitTerm Agent") {
514
+ return {
515
+ display_information: {
516
+ name,
517
+ description: "A coding agent for your repository, running in a GitTerm sandbox"
518
+ },
519
+ features: {
520
+ bot_user: { display_name: name, always_online: true },
521
+ app_home: {
522
+ home_tab_enabled: false,
523
+ messages_tab_enabled: true,
524
+ messages_tab_read_only_enabled: false
525
+ }
526
+ },
527
+ oauth_config: {
528
+ scopes: {
529
+ bot: [
530
+ "app_mentions:read",
531
+ "channels:history",
532
+ "groups:history",
533
+ "im:history",
534
+ "chat:write",
535
+ "files:read",
536
+ "reactions:write",
537
+ "users:read"
538
+ ]
539
+ }
540
+ },
541
+ settings: {
542
+ event_subscriptions: {
543
+ bot_events: ["app_mention", "message.channels", "message.groups", "message.im"]
544
+ },
545
+ interactivity: { is_enabled: true },
546
+ org_deploy_enabled: false,
547
+ socket_mode_enabled: true,
548
+ token_rotation_enabled: false
549
+ }
550
+ };
551
+ }
552
+
553
+ // src/index.ts
554
+ function createSlackBot(options) {
555
+ const { botToken, appToken, app, ...bot } = options;
556
+ return createBot({
557
+ ...bot,
558
+ adapter: createSlackAdapter({
559
+ ...botToken ? { botToken } : {},
560
+ ...appToken ? { appToken } : {},
561
+ ...app ? { app } : {}
562
+ })
563
+ });
564
+ }
565
+ async function withSavedConfig(options = {}) {
566
+ return { ...options, ...await loadSaved(options, "slack", options.logger) };
567
+ }
568
+ export {
569
+ createSlackAdapter,
570
+ createSlackBot,
571
+ slackManifest,
572
+ withSavedConfig
573
+ };
@@ -0,0 +1,37 @@
1
+ /**
2
+ * A Slack app manifest with exactly what the bot uses. Paste it at api.slack.com/apps →
3
+ * Create New App → From a manifest.
4
+ */
5
+ export declare function slackManifest(name?: string): {
6
+ display_information: {
7
+ name: string;
8
+ description: string;
9
+ };
10
+ features: {
11
+ bot_user: {
12
+ display_name: string;
13
+ always_online: boolean;
14
+ };
15
+ app_home: {
16
+ home_tab_enabled: boolean;
17
+ messages_tab_enabled: boolean;
18
+ messages_tab_read_only_enabled: boolean;
19
+ };
20
+ };
21
+ oauth_config: {
22
+ scopes: {
23
+ bot: string[];
24
+ };
25
+ };
26
+ settings: {
27
+ event_subscriptions: {
28
+ bot_events: string[];
29
+ };
30
+ interactivity: {
31
+ is_enabled: boolean;
32
+ };
33
+ org_deploy_enabled: boolean;
34
+ socket_mode_enabled: boolean;
35
+ token_rotation_enabled: boolean;
36
+ };
37
+ };
@@ -0,0 +1,43 @@
1
+ // src/manifest.ts
2
+ function slackManifest(name = "GitTerm Agent") {
3
+ return {
4
+ display_information: {
5
+ name,
6
+ description: "A coding agent for your repository, running in a GitTerm sandbox"
7
+ },
8
+ features: {
9
+ bot_user: { display_name: name, always_online: true },
10
+ app_home: {
11
+ home_tab_enabled: false,
12
+ messages_tab_enabled: true,
13
+ messages_tab_read_only_enabled: false
14
+ }
15
+ },
16
+ oauth_config: {
17
+ scopes: {
18
+ bot: [
19
+ "app_mentions:read",
20
+ "channels:history",
21
+ "groups:history",
22
+ "im:history",
23
+ "chat:write",
24
+ "files:read",
25
+ "reactions:write",
26
+ "users:read"
27
+ ]
28
+ }
29
+ },
30
+ settings: {
31
+ event_subscriptions: {
32
+ bot_events: ["app_mention", "message.channels", "message.groups", "message.im"]
33
+ },
34
+ interactivity: { is_enabled: true },
35
+ org_deploy_enabled: false,
36
+ socket_mode_enabled: true,
37
+ token_rotation_enabled: false
38
+ }
39
+ };
40
+ }
41
+ export {
42
+ slackManifest
43
+ };