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