gibi-bot 1.0.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.
Files changed (43) hide show
  1. package/.context.json +185 -0
  2. package/.env.example +4 -0
  3. package/DISTRIBUTION.md +55 -0
  4. package/GEMINI.md +20 -0
  5. package/LICENSE +21 -0
  6. package/README.md +192 -0
  7. package/assets/gibi_avatar.png +0 -0
  8. package/conductor/code_styleguides/general.md +23 -0
  9. package/conductor/code_styleguides/ts.md +52 -0
  10. package/conductor/product-guidelines.md +28 -0
  11. package/conductor/product.md +20 -0
  12. package/conductor/setup_state.json +1 -0
  13. package/conductor/tech-stack.md +15 -0
  14. package/conductor/tracks/slack_bot_20260107/metadata.json +8 -0
  15. package/conductor/tracks/slack_bot_20260107/plan.md +26 -0
  16. package/conductor/tracks/slack_bot_20260107/spec.md +18 -0
  17. package/conductor/tracks.md +8 -0
  18. package/conductor/workflow.md +338 -0
  19. package/dist/agents.js +90 -0
  20. package/dist/agents.test.js +65 -0
  21. package/dist/app.js +740 -0
  22. package/dist/config.js +102 -0
  23. package/dist/context.js +146 -0
  24. package/dist/context.test.js +95 -0
  25. package/dist/prompts.js +20 -0
  26. package/jest.config.js +11 -0
  27. package/nodemon.json +10 -0
  28. package/package.json +44 -0
  29. package/src/agents.test.ts +85 -0
  30. package/src/agents.ts +112 -0
  31. package/src/app.d.ts +2 -0
  32. package/src/app.d.ts.map +1 -0
  33. package/src/app.js +55 -0
  34. package/src/app.js.map +1 -0
  35. package/src/app.ts +809 -0
  36. package/src/config.ts +72 -0
  37. package/src/context.test.ts +75 -0
  38. package/src/context.ts +130 -0
  39. package/src/prompts.ts +17 -0
  40. package/test_gemini.js +23 -0
  41. package/test_gemini_approval.js +24 -0
  42. package/test_gemini_write.js +23 -0
  43. package/tsconfig.json +13 -0
package/dist/app.js ADDED
@@ -0,0 +1,740 @@
1
+ #!/usr/bin/env node
2
+ "use strict";
3
+ var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
4
+ if (k2 === undefined) k2 = k;
5
+ var desc = Object.getOwnPropertyDescriptor(m, k);
6
+ if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
7
+ desc = { enumerable: true, get: function() { return m[k]; } };
8
+ }
9
+ Object.defineProperty(o, k2, desc);
10
+ }) : (function(o, m, k, k2) {
11
+ if (k2 === undefined) k2 = k;
12
+ o[k2] = m[k];
13
+ }));
14
+ var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
15
+ Object.defineProperty(o, "default", { enumerable: true, value: v });
16
+ }) : function(o, v) {
17
+ o["default"] = v;
18
+ });
19
+ var __importStar = (this && this.__importStar) || (function () {
20
+ var ownKeys = function(o) {
21
+ ownKeys = Object.getOwnPropertyNames || function (o) {
22
+ var ar = [];
23
+ for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k;
24
+ return ar;
25
+ };
26
+ return ownKeys(o);
27
+ };
28
+ return function (mod) {
29
+ if (mod && mod.__esModule) return mod;
30
+ var result = {};
31
+ if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]);
32
+ __setModuleDefault(result, mod);
33
+ return result;
34
+ };
35
+ })();
36
+ Object.defineProperty(exports, "__esModule", { value: true });
37
+ const bolt_1 = require("@slack/bolt");
38
+ const config_1 = require("./config");
39
+ const context_1 = require("./context");
40
+ const fs = __importStar(require("fs"));
41
+ const path = __importStar(require("path"));
42
+ const agents_1 = require("./agents");
43
+ const AVAILABLE_MODELS = [
44
+ { id: 'gemini-2.5-pro', desc: 'Complex reasoning, coding (Access required)' },
45
+ { id: 'gemini-2.5-flash', desc: 'Fast, lightweight (Access required)' },
46
+ { id: 'gemini-1.5-pro', desc: 'General purpose, good reasoning' },
47
+ { id: 'gemini-1.5-flash', desc: 'High speed, lower latency' },
48
+ { id: 'gemini-1.0-pro', desc: 'Standard model' },
49
+ ];
50
+ let isRestarting = false;
51
+ const start = async () => {
52
+ try {
53
+ await (0, config_1.loadConfig)();
54
+ let BOT_USER_ID = '';
55
+ let BOT_ID = '';
56
+ const app = new bolt_1.App({
57
+ token: process.env.SLACK_BOT_TOKEN || '',
58
+ signingSecret: process.env.SLACK_SIGNING_SECRET || '',
59
+ socketMode: true,
60
+ appToken: process.env.SLACK_APP_TOKEN || ''
61
+ });
62
+ // Fetch Bot User ID on startup
63
+ (async () => {
64
+ try {
65
+ const authResult = await app.client.auth.test();
66
+ BOT_USER_ID = authResult.user_id;
67
+ BOT_ID = authResult.bot_id;
68
+ }
69
+ catch (e) {
70
+ console.error('Failed to fetch Bot User ID:', e);
71
+ }
72
+ })();
73
+ const contextManager = new context_1.ContextManager();
74
+ // Handle bot being added to a channel
75
+ app.event('member_joined_channel', async ({ event, client, say }) => {
76
+ // Use cached ID or fetch if not ready
77
+ if (!BOT_USER_ID) {
78
+ const authResult = await client.auth.test();
79
+ BOT_USER_ID = authResult.user_id;
80
+ BOT_ID = authResult.bot_id;
81
+ }
82
+ if (event.user === BOT_USER_ID) {
83
+ await say(`Hello! I'm Gibi. I'm ready to help. Mention me (@Gibi) to start a new thread!`);
84
+ }
85
+ });
86
+ // Generic message handler for Agent Chat
87
+ // Helper to handle Agent turns
88
+ const processAgentTurn = async (contextId, channelId, threadTs, userText, say, client, existingMessageTs) => {
89
+ const context = contextManager.getContext(contextId);
90
+ // Append User Message if provided
91
+ if (userText) {
92
+ context.messages.push({ role: 'user', content: userText });
93
+ contextManager.setContextData(contextId, { messages: context.messages });
94
+ }
95
+ let processingMsgTs = existingMessageTs;
96
+ try {
97
+ const agentName = context.data.agent || 'gemini';
98
+ // Send or Update ephemeral "Thinking..." message
99
+ const thinkingBlocks = [
100
+ {
101
+ type: 'section',
102
+ text: {
103
+ type: 'mrkdwn',
104
+ text: `_Thinking..._ (using \`${agentName}\`)`
105
+ }
106
+ }
107
+ ];
108
+ if (processingMsgTs) {
109
+ await client.chat.update({
110
+ channel: channelId,
111
+ ts: processingMsgTs,
112
+ text: 'Thinking...',
113
+ blocks: thinkingBlocks
114
+ });
115
+ }
116
+ else {
117
+ const processingMsg = await say({
118
+ blocks: thinkingBlocks,
119
+ text: 'Thinking...',
120
+ thread_ts: threadTs
121
+ });
122
+ processingMsgTs = processingMsg.ts;
123
+ }
124
+ // Determine Agent
125
+ const agent = agents_1.agentRegistry[agentName];
126
+ if (!agent) {
127
+ throw new Error(`Unknown agent: ${agentName}`);
128
+ }
129
+ const responseText = await agent.run(context.messages, {
130
+ model: context.data.model,
131
+ mode: context.data.mode
132
+ });
133
+ if (responseText) {
134
+ // Append Assistant Response
135
+ context.messages.push({ role: 'assistant', content: responseText });
136
+ contextManager.setContextData(contextId, { messages: context.messages });
137
+ const responseBlocks = [
138
+ {
139
+ type: 'section',
140
+ text: {
141
+ type: 'mrkdwn',
142
+ text: responseText
143
+ }
144
+ },
145
+ {
146
+ type: 'context',
147
+ elements: [
148
+ {
149
+ type: 'mrkdwn',
150
+ text: `šŸ¤– *Agent:* \`${agentName}\` | 🧠 *Model:* \`${context.data.model || 'default'}\` | šŸ“‚ *CWD:* \`${context.data.cwd || 'root'}\``
151
+ }
152
+ ]
153
+ },
154
+ {
155
+ type: 'actions',
156
+ elements: [
157
+ {
158
+ type: 'button',
159
+ text: { type: 'plain_text', text: 'šŸ”„ Retry', emoji: true },
160
+ action_id: 'retry_turn'
161
+ },
162
+ {
163
+ type: 'button',
164
+ text: { type: 'plain_text', text: 'ā†©ļø Revert', emoji: true },
165
+ action_id: 'revert_turn'
166
+ }
167
+ ]
168
+ }
169
+ ];
170
+ // Update the processing message with the actual response
171
+ if (processingMsgTs) {
172
+ await client.chat.update({
173
+ channel: channelId,
174
+ ts: processingMsgTs,
175
+ text: responseText,
176
+ blocks: responseBlocks
177
+ });
178
+ }
179
+ else {
180
+ await say({
181
+ text: responseText,
182
+ blocks: responseBlocks,
183
+ thread_ts: threadTs
184
+ });
185
+ }
186
+ }
187
+ else if (processingMsgTs) {
188
+ // Handle empty response case
189
+ await client.chat.update({
190
+ channel: channelId,
191
+ ts: processingMsgTs,
192
+ text: `Received empty response from ${agentName}.`
193
+ });
194
+ }
195
+ }
196
+ catch (error) {
197
+ console.error('Error calling Agent:', error);
198
+ const errorMessage = `params: Error calling Agent: ${error.message}`;
199
+ if (processingMsgTs) {
200
+ await client.chat.update({
201
+ channel: channelId,
202
+ ts: processingMsgTs,
203
+ text: errorMessage
204
+ });
205
+ }
206
+ else {
207
+ await say({
208
+ text: errorMessage,
209
+ thread_ts: threadTs
210
+ });
211
+ }
212
+ }
213
+ };
214
+ // Handle generic messages
215
+ app.message(async ({ message, say, client }) => {
216
+ const msg = message;
217
+ if (msg.bot_id || msg.subtype === 'bot_message' || !msg.text)
218
+ return;
219
+ // Skip if it looks like the cd command
220
+ if (/^cd\s+/.test(msg.text.trim()))
221
+ return;
222
+ // Check if this message mentions the bot
223
+ let isMention = false;
224
+ if (BOT_USER_ID && msg.text.includes(`<@${BOT_USER_ID}>`))
225
+ isMention = true;
226
+ if (BOT_ID && msg.text.includes(`<@${BOT_ID}>`))
227
+ isMention = true;
228
+ const isDm = msg.channel_type === 'im';
229
+ const isThread = !!msg.thread_ts;
230
+ const contextId = msg.thread_ts || msg.channel;
231
+ if (isDm) {
232
+ // Process
233
+ }
234
+ else if (isThread) {
235
+ if (!isMention && !contextManager.hasContext(contextId))
236
+ return;
237
+ }
238
+ else {
239
+ if (!isMention)
240
+ return;
241
+ }
242
+ // Clean up text (remove mention)
243
+ let text = msg.text;
244
+ if (BOT_USER_ID)
245
+ text = text.replace(new RegExp(`<@${BOT_USER_ID}>`, 'g'), '').trim();
246
+ if (BOT_ID)
247
+ text = text.replace(new RegExp(`<@${BOT_ID}>`, 'g'), '').trim();
248
+ const replyThreadTs = msg.thread_ts || msg.ts;
249
+ const targetContextId = msg.thread_ts || msg.ts;
250
+ await processAgentTurn(targetContextId, msg.channel, replyThreadTs, text, say, client);
251
+ });
252
+ // Helper for CD command (shared between Slash command and Message)
253
+ const executeChangeDirectory = async (contextId, targetPath) => {
254
+ const context = contextManager.getContext(contextId);
255
+ // 1. Determine current context CWD
256
+ const currentCwd = context.data.cwd || process.cwd();
257
+ // 2. Resolve new path
258
+ let newPath = currentCwd;
259
+ if (targetPath) {
260
+ newPath = path.resolve(currentCwd, targetPath);
261
+ }
262
+ try {
263
+ // 3. Validate path
264
+ await fs.promises.access(newPath, fs.constants.F_OK);
265
+ const stats = await fs.promises.stat(newPath);
266
+ if (!stats.isDirectory()) {
267
+ return { text: `āŒ Path is not a directory: \`${newPath}\`` };
268
+ }
269
+ // 4. Update Context
270
+ contextManager.setContextData(contextId, { cwd: newPath });
271
+ // 5. List files
272
+ const files = await fs.promises.readdir(newPath, { withFileTypes: true });
273
+ // Sort: Directories first, then files
274
+ files.sort((a, b) => {
275
+ if (a.isDirectory() && !b.isDirectory())
276
+ return -1;
277
+ if (!a.isDirectory() && b.isDirectory())
278
+ return 1;
279
+ return a.name.localeCompare(b.name);
280
+ });
281
+ // Build Blocks
282
+ const blocks = [
283
+ {
284
+ type: 'section',
285
+ text: {
286
+ type: 'mrkdwn',
287
+ text: `šŸ“‚ *Directory:* \`${newPath}\``
288
+ }
289
+ }
290
+ ];
291
+ // Add "Up" button if not root
292
+ const parentPath = path.dirname(newPath);
293
+ if (newPath !== parentPath) {
294
+ blocks.push({
295
+ type: 'actions',
296
+ elements: [
297
+ {
298
+ type: 'button',
299
+ text: {
300
+ type: 'plain_text',
301
+ text: 'ā¬†ļø Up one level',
302
+ emoji: true
303
+ },
304
+ value: parentPath,
305
+ action_id: 'navigate_dir'
306
+ }
307
+ ]
308
+ });
309
+ }
310
+ const dirButtons = [];
311
+ const fileNames = [];
312
+ files.forEach(file => {
313
+ if (file.isDirectory()) {
314
+ if (dirButtons.length < 10) { // Limit buttons to avoid clutter
315
+ dirButtons.push({
316
+ type: 'button',
317
+ text: {
318
+ type: 'plain_text',
319
+ text: `šŸ“ ${file.name}`,
320
+ emoji: true
321
+ },
322
+ value: path.join(newPath, file.name),
323
+ action_id: 'navigate_dir'
324
+ });
325
+ }
326
+ else {
327
+ fileNames.push(`šŸ“ ${file.name}`);
328
+ }
329
+ }
330
+ else {
331
+ fileNames.push(`šŸ“„ ${file.name}`);
332
+ }
333
+ });
334
+ if (dirButtons.length > 0) {
335
+ // Slack allows max 5 buttons per action block
336
+ for (let i = 0; i < dirButtons.length; i += 5) {
337
+ blocks.push({
338
+ type: 'actions',
339
+ elements: dirButtons.slice(i, i + 5)
340
+ });
341
+ }
342
+ }
343
+ if (fileNames.length > 0) {
344
+ let fileListText = fileNames.join('\n');
345
+ if (fileListText.length > 2500) {
346
+ fileListText = fileListText.substring(0, 2500) + '\n...(truncated)';
347
+ }
348
+ blocks.push({
349
+ type: 'section',
350
+ text: {
351
+ type: 'mrkdwn',
352
+ text: fileListText
353
+ }
354
+ });
355
+ }
356
+ return {
357
+ text: `Directory: ${newPath}`,
358
+ blocks: blocks
359
+ };
360
+ }
361
+ catch (error) {
362
+ return { text: `āŒ Error accessing directory: ${error.message}` };
363
+ }
364
+ };
365
+ // Handle navigate_dir action
366
+ app.action('navigate_dir', async ({ ack, body, respond }) => {
367
+ await ack();
368
+ const action = body.actions[0];
369
+ const contextId = body.container.thread_ts || body.container.channel_id;
370
+ const output = await executeChangeDirectory(contextId, action.value);
371
+ await respond({
372
+ ...output,
373
+ replace_original: true
374
+ });
375
+ });
376
+ // Handle /plan command
377
+ app.command('/plan', async ({ command, ack, say, client }) => {
378
+ await ack();
379
+ const contextId = command.channel_id;
380
+ const goal = command.text.trim();
381
+ // Switch to Plan Mode
382
+ contextManager.setContextData(contextId, { mode: 'plan' });
383
+ // Notify initiation
384
+ await say({
385
+ text: `šŸ“ *Entering Plan Mode*\nTarget: ${goal || "No specific goal provided. What would you like to plan?"}`,
386
+ channel: command.channel_id
387
+ });
388
+ // If goal provided, process it as the first message
389
+ if (goal) {
390
+ await processAgentTurn(contextId, command.channel_id, undefined, goal, say, client);
391
+ }
392
+ });
393
+ // Handle /cd command
394
+ app.command('/cd', async ({ command, ack, respond }) => {
395
+ await ack();
396
+ const output = await executeChangeDirectory(command.channel_id, command.text.trim());
397
+ await respond(output);
398
+ });
399
+ // Handle "cd [path]" messages (works in threads)
400
+ app.message(/^cd\s+(.+)/, async ({ message, context, say }) => {
401
+ // Need to cast message to access optional properties like thread_ts
402
+ const msg = message;
403
+ const targetPath = context.matches[1].trim();
404
+ // Use thread_ts if available, otherwise channel
405
+ const contextId = msg.thread_ts || msg.channel;
406
+ const output = await executeChangeDirectory(contextId, targetPath);
407
+ await say({
408
+ text: output,
409
+ thread_ts: msg.thread_ts // Reply in thread if it was a thread message
410
+ });
411
+ });
412
+ // Handle /model command
413
+ app.command('/model', async ({ command, ack, client, respond }) => {
414
+ await ack();
415
+ const input = command.text.trim();
416
+ const contextId = command.channel_id;
417
+ const context = contextManager.getContext(contextId);
418
+ // Show modal if no input
419
+ if (!input) {
420
+ await client.views.open({
421
+ trigger_id: command.trigger_id,
422
+ view: {
423
+ type: 'modal',
424
+ callback_id: 'model_select_modal',
425
+ private_metadata: contextId,
426
+ title: { type: 'plain_text', text: 'Select Model' },
427
+ blocks: [
428
+ {
429
+ type: 'input',
430
+ block_id: 'model_block',
431
+ label: { type: 'plain_text', text: 'Choose a Gemini model' },
432
+ element: {
433
+ type: 'static_select',
434
+ action_id: 'model_select',
435
+ initial_option: AVAILABLE_MODELS.find(m => m.id === (context.data.model || 'gemini-1.5-pro')) ? {
436
+ text: { type: 'plain_text', text: context.data.model || 'gemini-1.5-pro' },
437
+ value: context.data.model || 'gemini-1.5-pro'
438
+ } : undefined,
439
+ options: AVAILABLE_MODELS.map(m => ({
440
+ text: { type: 'plain_text', text: m.id },
441
+ value: m.id,
442
+ description: { type: 'plain_text', text: m.desc.substring(0, 75) }
443
+ }))
444
+ }
445
+ }
446
+ ],
447
+ submit: { type: 'plain_text', text: 'Update' }
448
+ }
449
+ });
450
+ return;
451
+ }
452
+ if (input === 'list' || input === 'help') {
453
+ const currentModel = context.data.model || 'default';
454
+ const modelList = AVAILABLE_MODELS.map(m => `• \`${m.id}\`: ${m.desc}`).join('\n');
455
+ await respond(`Current model: \`${currentModel}\`\n\n` +
456
+ `*Available Models:*\n${modelList}\n\n` +
457
+ `To switch, use \`/model <model_name>\``);
458
+ return;
459
+ }
460
+ contextManager.setContextData(contextId, { model: input });
461
+ await respond(`Switched model to: \`${input}\``);
462
+ });
463
+ // Handle /agent command
464
+ app.command('/agent', async ({ command, ack, client, respond }) => {
465
+ await ack();
466
+ const input = command.text.trim();
467
+ const contextId = command.channel_id;
468
+ const context = contextManager.getContext(contextId);
469
+ if (!input) {
470
+ await client.views.open({
471
+ trigger_id: command.trigger_id,
472
+ view: {
473
+ type: 'modal',
474
+ callback_id: 'agent_select_modal',
475
+ private_metadata: contextId,
476
+ title: { type: 'plain_text', text: 'Select Agent' },
477
+ blocks: [
478
+ {
479
+ type: 'input',
480
+ block_id: 'agent_block',
481
+ label: { type: 'plain_text', text: 'Choose a backing agent' },
482
+ element: {
483
+ type: 'static_select',
484
+ action_id: 'agent_select',
485
+ initial_option: {
486
+ text: { type: 'plain_text', text: context.data.agent || 'gemini' },
487
+ value: context.data.agent || 'gemini'
488
+ },
489
+ options: Object.keys(agents_1.agentRegistry).map(k => ({
490
+ text: { type: 'plain_text', text: k },
491
+ value: k
492
+ }))
493
+ }
494
+ }
495
+ ],
496
+ submit: { type: 'plain_text', text: 'Update' }
497
+ }
498
+ });
499
+ return;
500
+ }
501
+ if (input === 'list' || input === 'help') {
502
+ const currentAgent = context.data.agent || 'gemini';
503
+ const agentList = Object.keys(agents_1.agentRegistry).map(k => `• \`${k}\``).join('\n');
504
+ await respond(`Current agent: \`${currentAgent}\`\n\n` +
505
+ `*Available Agents:*\n${agentList}\n\n` +
506
+ `To switch, use \`/agent <agent_name>\``);
507
+ return;
508
+ }
509
+ const agentName = input.toLowerCase();
510
+ if (!agents_1.agentRegistry[agentName]) {
511
+ await respond(`āŒ Unknown agent: \`${agentName}\`. Available agents: ${Object.keys(agents_1.agentRegistry).join(', ')}`);
512
+ return;
513
+ }
514
+ contextManager.setContextData(contextId, { agent: agentName });
515
+ await respond(`Switched agent to: \`${agentName}\``);
516
+ });
517
+ // Handle Modal Submissions
518
+ app.view('model_select_modal', async ({ ack, view, client }) => {
519
+ await ack();
520
+ const contextId = view.private_metadata;
521
+ const selectedModel = view.state.values.model_block.model_select.selected_option?.value;
522
+ if (selectedModel) {
523
+ contextManager.setContextData(contextId, { model: selectedModel });
524
+ await client.chat.postMessage({
525
+ channel: contextId,
526
+ text: `šŸ”„ Model updated to \`${selectedModel}\``
527
+ });
528
+ }
529
+ });
530
+ app.view('agent_select_modal', async ({ ack, view, client }) => {
531
+ await ack();
532
+ const contextId = view.private_metadata;
533
+ const selectedAgent = view.state.values.agent_block.agent_select.selected_option?.value;
534
+ if (selectedAgent) {
535
+ contextManager.setContextData(contextId, { agent: selectedAgent });
536
+ await client.chat.postMessage({
537
+ channel: contextId,
538
+ text: `šŸ”„ Agent updated to \`${selectedAgent}\``
539
+ });
540
+ }
541
+ });
542
+ // Handle Retry
543
+ app.action('retry_turn', async ({ ack, body, client }) => {
544
+ await ack();
545
+ const action = body;
546
+ const contextId = action.container.thread_ts || action.container.channel_id;
547
+ const channelId = action.container.channel_id;
548
+ const messageTs = action.container.message_ts;
549
+ const threadTs = action.container.thread_ts;
550
+ const context = contextManager.getContext(contextId);
551
+ // Remove last assistant message
552
+ if (context.messages.length > 0 && context.messages[context.messages.length - 1].role === 'assistant') {
553
+ context.messages.pop();
554
+ }
555
+ // Add a note to the last user message if not already present
556
+ if (context.messages.length > 0 && context.messages[context.messages.length - 1].role === 'user') {
557
+ const lastMsg = context.messages[context.messages.length - 1];
558
+ if (!lastMsg.content.includes('[System: The user requested a retry')) {
559
+ lastMsg.content += '\n\n[System: The user requested a retry of this instruction.]';
560
+ }
561
+ }
562
+ contextManager.setContextData(contextId, { messages: context.messages });
563
+ // Polyfill say using client
564
+ const say = async (args) => {
565
+ const msg = typeof args === 'string' ? { text: args } : args;
566
+ return await client.chat.postMessage({
567
+ channel: channelId,
568
+ ...msg
569
+ });
570
+ };
571
+ // Re-run agent turn (without new user text, using existing message to update)
572
+ await processAgentTurn(contextId, channelId, threadTs, undefined, say, client, messageTs);
573
+ });
574
+ // Handle Revert
575
+ app.action('revert_turn', async ({ ack, body, client }) => {
576
+ await ack();
577
+ const action = body;
578
+ const contextId = action.container.thread_ts || action.container.channel_id;
579
+ const channelId = action.container.channel_id;
580
+ const threadTs = action.container.thread_ts;
581
+ const context = contextManager.getContext(contextId);
582
+ // Find the last user message to include in the revert prompt
583
+ let lastUserText = '';
584
+ for (let i = context.messages.length - 1; i >= 0; i--) {
585
+ if (context.messages[i].role === 'user') {
586
+ lastUserText = context.messages[i].content;
587
+ break;
588
+ }
589
+ }
590
+ // Polyfill say using client
591
+ const say = async (args) => {
592
+ const msg = typeof args === 'string' ? { text: args } : args;
593
+ return await client.chat.postMessage({
594
+ channel: channelId,
595
+ ...msg
596
+ });
597
+ };
598
+ const revertPrompt = `The user wants to revert the actions taken for the following request: "${lastUserText}". Please undo any file modifications or state changes made in that turn.`;
599
+ // Trigger a new turn with instructions to revert
600
+ // We do NOT pop messages here, we want the agent to see what it did and undo it.
601
+ await processAgentTurn(contextId, channelId, threadTs, revertPrompt, say, client);
602
+ });
603
+ // Handle /help command
604
+ app.command('/help', async ({ ack, respond }) => {
605
+ await ack();
606
+ const helpMessage = `
607
+ *Available Commands:*
608
+
609
+ - \`/plan [goal]\`: Enter plan mode to generate and execute coding plans.
610
+ - \`/cd [path]\`: Change the current working directory for the conversation.
611
+ - \`/model [name]\`: Switch the Gemini model (e.g., \`gemini-pro\`, \`gemini-1.5-pro\`).
612
+ - \`/agent [name]\`: Switch the backing agent (e.g., \`gemini\`, \`claude\`, \`cursor\`).
613
+ - \`/help\`: Show this help message.
614
+ `;
615
+ await respond(helpMessage);
616
+ });
617
+ // Handle App Home Tab
618
+ app.event('app_home_opened', async ({ event, client, logger }) => {
619
+ try {
620
+ const contexts = contextManager.getAllContexts();
621
+ // Sort by last active
622
+ contexts.sort((a, b) => b.lastActiveAt.getTime() - a.lastActiveAt.getTime());
623
+ const contextBlocks = contexts.slice(0, 5).map(ctx => ({
624
+ type: 'section',
625
+ text: {
626
+ type: 'mrkdwn',
627
+ text: `*Context:* \`${ctx.id}\`\n*Last Active:* ${ctx.lastActiveAt.toLocaleString()}\n*Agent:* \`${ctx.data.agent || 'gemini'}\` | *Model:* \`${ctx.data.model || 'default'}\` | *CWD:* \`${ctx.data.cwd || 'root'}\``
628
+ }
629
+ }));
630
+ const blocks = [
631
+ {
632
+ type: 'header',
633
+ text: {
634
+ type: 'plain_text',
635
+ text: 'šŸ  Gibi Dashboard',
636
+ emoji: true
637
+ }
638
+ },
639
+ {
640
+ type: 'section',
641
+ text: {
642
+ type: 'mrkdwn',
643
+ text: `Welcome, <@${event.user}>! Here is a summary of your Gibi instance status.`
644
+ }
645
+ },
646
+ {
647
+ type: 'divider'
648
+ },
649
+ {
650
+ type: 'section',
651
+ fields: [
652
+ {
653
+ type: 'mrkdwn',
654
+ text: `*Default Agent:*\n\`gemini\``
655
+ },
656
+ {
657
+ type: 'mrkdwn',
658
+ text: `*Available Agents:*\n${Object.keys(agents_1.agentRegistry).map(k => `\`${k}\``).join(', ')}`
659
+ }
660
+ ]
661
+ },
662
+ {
663
+ type: 'divider'
664
+ },
665
+ {
666
+ type: 'header',
667
+ text: {
668
+ type: 'plain_text',
669
+ text: 'šŸ•’ Recent Activity',
670
+ emoji: true
671
+ }
672
+ }
673
+ ];
674
+ if (contextBlocks.length > 0) {
675
+ blocks.push(...contextBlocks.flatMap(b => [b, { type: 'divider' }]));
676
+ }
677
+ else {
678
+ blocks.push({
679
+ type: 'section',
680
+ text: {
681
+ type: 'mrkdwn',
682
+ text: '_No active contexts found. Send a message to Gibi in any channel to start._'
683
+ }
684
+ });
685
+ }
686
+ blocks.push({
687
+ type: 'context',
688
+ elements: [
689
+ {
690
+ type: 'mrkdwn',
691
+ text: `Last updated: ${new Date().toLocaleString()}`
692
+ }
693
+ ]
694
+ });
695
+ await client.views.publish({
696
+ user_id: event.user,
697
+ view: {
698
+ type: 'home',
699
+ blocks: blocks
700
+ }
701
+ });
702
+ }
703
+ catch (error) {
704
+ logger.error('Error publishing Home tab:', error);
705
+ }
706
+ });
707
+ // Start your app
708
+ await app.start(process.env.PORT || 3000);
709
+ console.log('šŸ’ Gibi is running!');
710
+ }
711
+ catch (error) {
712
+ await handleAuthError(error);
713
+ }
714
+ };
715
+ const handleAuthError = async (error) => {
716
+ if (isRestarting)
717
+ return;
718
+ if (error?.data?.error === 'invalid_auth') {
719
+ isRestarting = true;
720
+ console.error('\nāŒ Authentication failed: The stored Slack tokens seem to be invalid.');
721
+ console.error(' Clearing stored configuration and restarting setup...');
722
+ await (0, config_1.clearConfig)(['SLACK_BOT_TOKEN']);
723
+ console.log('\nšŸ”„ Restarting app setup...\n');
724
+ isRestarting = false;
725
+ await start(); // Recursively restart
726
+ }
727
+ else {
728
+ console.error('Failed to start app:', error);
729
+ process.exit(1);
730
+ }
731
+ };
732
+ // Start the application
733
+ start();
734
+ // Global error handlers to catch any unhandled promise rejections
735
+ process.on('unhandledRejection', async (reason) => {
736
+ await handleAuthError(reason);
737
+ });
738
+ process.on('uncaughtException', async (error) => {
739
+ await handleAuthError(error);
740
+ });