mattermost-claude-code 0.2.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.
@@ -0,0 +1,582 @@
1
+ import { ClaudeCli } from './cli.js';
2
+ const REACTION_EMOJIS = ['one', 'two', 'three', 'four'];
3
+ const EMOJI_TO_INDEX = {
4
+ 'one': 0, '1️⃣': 0,
5
+ 'two': 1, '2️⃣': 1,
6
+ 'three': 2, '3️⃣': 2,
7
+ 'four': 3, '4️⃣': 3,
8
+ };
9
+ export class SessionManager {
10
+ claude = null;
11
+ mattermost;
12
+ workingDir;
13
+ skipPermissions;
14
+ session = null;
15
+ updateTimer = null;
16
+ typingTimer = null;
17
+ pendingQuestionSet = null;
18
+ pendingApproval = null;
19
+ planApproved = false; // Track if we already approved this session
20
+ tasksPostId = null; // Track the tasks display post
21
+ activeSubagents = new Map(); // taskId -> postId for subagent status
22
+ debug = process.env.DEBUG === '1' || process.argv.includes('--debug');
23
+ constructor(mattermost, workingDir, skipPermissions = false) {
24
+ this.mattermost = mattermost;
25
+ this.workingDir = workingDir;
26
+ this.skipPermissions = skipPermissions;
27
+ // Listen for reactions to answer questions
28
+ this.mattermost.on('reaction', (reaction, user) => {
29
+ this.handleReaction(reaction.post_id, reaction.emoji_name, user?.username || 'unknown');
30
+ });
31
+ }
32
+ async startSession(options, username, replyToPostId) {
33
+ // Start Claude if not running
34
+ if (!this.claude?.isRunning()) {
35
+ const msg = `🚀 **Session started**\n> Working directory: \`${this.workingDir}\``;
36
+ const post = await this.mattermost.createPost(msg, replyToPostId);
37
+ const threadId = replyToPostId || post.id;
38
+ this.session = { threadId, postId: null, content: '' };
39
+ this.planApproved = false; // Reset for new session
40
+ this.tasksPostId = null; // Reset tasks display
41
+ this.activeSubagents.clear(); // Clear subagent tracking
42
+ // Create Claude CLI with options (including threadId for permissions)
43
+ const cliOptions = {
44
+ workingDir: this.workingDir,
45
+ threadId: threadId,
46
+ skipPermissions: this.skipPermissions,
47
+ };
48
+ this.claude = new ClaudeCli(cliOptions);
49
+ this.claude.on('event', (e) => this.handleEvent(e));
50
+ this.claude.on('exit', (code) => this.handleExit(code));
51
+ try {
52
+ this.claude.start();
53
+ }
54
+ catch (err) {
55
+ console.error('[Session] Start error:', err);
56
+ await this.mattermost.createPost(`❌ ${err}`, threadId);
57
+ this.session = null;
58
+ return;
59
+ }
60
+ }
61
+ // Send the message and start typing indicator
62
+ this.claude.sendMessage(options.prompt);
63
+ this.startTyping();
64
+ }
65
+ handleEvent(event) {
66
+ // Check for special tool uses that need custom handling
67
+ if (event.type === 'assistant') {
68
+ const msg = event.message;
69
+ let hasSpecialTool = false;
70
+ for (const block of msg?.content || []) {
71
+ if (block.type === 'tool_use') {
72
+ if (block.name === 'ExitPlanMode') {
73
+ this.handleExitPlanMode();
74
+ hasSpecialTool = true;
75
+ }
76
+ else if (block.name === 'TodoWrite') {
77
+ this.handleTodoWrite(block.input);
78
+ // Don't set hasSpecialTool - let other content through
79
+ }
80
+ else if (block.name === 'Task') {
81
+ this.handleTaskStart(block.id, block.input);
82
+ // Don't set hasSpecialTool - let other content through
83
+ }
84
+ else if (block.name === 'AskUserQuestion') {
85
+ this.handleAskUserQuestion(block.id, block.input);
86
+ hasSpecialTool = true;
87
+ }
88
+ }
89
+ }
90
+ // Skip normal output if we handled a special tool (we post it ourselves)
91
+ if (hasSpecialTool)
92
+ return;
93
+ }
94
+ // Check for tool_result to update subagent status
95
+ if (event.type === 'user') {
96
+ const msg = event.message;
97
+ for (const block of msg?.content || []) {
98
+ if (block.type === 'tool_result' && block.tool_use_id) {
99
+ const postId = this.activeSubagents.get(block.tool_use_id);
100
+ if (postId) {
101
+ this.handleTaskComplete(block.tool_use_id, postId);
102
+ }
103
+ }
104
+ }
105
+ }
106
+ const formatted = this.formatEvent(event);
107
+ if (this.debug) {
108
+ console.log(`[DEBUG] handleEvent: ${event.type} -> ${formatted ? formatted.substring(0, 100) : '(null)'}`);
109
+ }
110
+ if (formatted)
111
+ this.appendContent(formatted);
112
+ }
113
+ async handleTaskComplete(toolUseId, postId) {
114
+ try {
115
+ await this.mattermost.updatePost(postId, this.activeSubagents.has(toolUseId)
116
+ ? `🤖 **Subagent** ✅ *completed*`
117
+ : `🤖 **Subagent** ✅`);
118
+ this.activeSubagents.delete(toolUseId);
119
+ }
120
+ catch (err) {
121
+ console.error('[Session] Failed to update subagent completion:', err);
122
+ }
123
+ }
124
+ async handleExitPlanMode() {
125
+ if (!this.session)
126
+ return;
127
+ // If already approved in this session, auto-continue
128
+ if (this.planApproved) {
129
+ console.log('[Session] Plan already approved, auto-continuing...');
130
+ if (this.claude?.isRunning()) {
131
+ this.claude.sendMessage('Continue with the implementation.');
132
+ this.startTyping();
133
+ }
134
+ return;
135
+ }
136
+ // If we already have a pending approval, don't post another one
137
+ if (this.pendingApproval && this.pendingApproval.type === 'plan') {
138
+ console.log('[Session] Plan approval already pending, waiting...');
139
+ return;
140
+ }
141
+ // Flush any pending content first
142
+ await this.flush();
143
+ this.session.postId = null;
144
+ this.session.content = '';
145
+ // Post approval message with reactions
146
+ const message = `✅ **Plan ready for approval**\n\n` +
147
+ `👍 Approve and start building\n` +
148
+ `👎 Request changes\n\n` +
149
+ `*React to respond*`;
150
+ const post = await this.mattermost.createPost(message, this.session.threadId);
151
+ // Add approval reactions
152
+ try {
153
+ await this.mattermost.addReaction(post.id, '+1');
154
+ await this.mattermost.addReaction(post.id, '-1');
155
+ }
156
+ catch (err) {
157
+ console.error('[Session] Failed to add approval reactions:', err);
158
+ }
159
+ // Track this for reaction handling
160
+ this.pendingApproval = { postId: post.id, type: 'plan' };
161
+ // Stop typing while waiting
162
+ this.stopTyping();
163
+ }
164
+ async handleTodoWrite(input) {
165
+ if (!this.session)
166
+ return;
167
+ const todos = input.todos;
168
+ if (!todos || todos.length === 0) {
169
+ // Clear tasks display if empty
170
+ if (this.tasksPostId) {
171
+ try {
172
+ await this.mattermost.updatePost(this.tasksPostId, '📋 ~~Tasks~~ *(completed)*');
173
+ }
174
+ catch (err) {
175
+ console.error('[Session] Failed to update tasks:', err);
176
+ }
177
+ }
178
+ return;
179
+ }
180
+ // Format tasks nicely
181
+ let message = '📋 **Tasks**\n\n';
182
+ for (const todo of todos) {
183
+ let icon;
184
+ let text;
185
+ switch (todo.status) {
186
+ case 'completed':
187
+ icon = '✅';
188
+ text = `~~${todo.content}~~`;
189
+ break;
190
+ case 'in_progress':
191
+ icon = '🔄';
192
+ text = `**${todo.activeForm}**`;
193
+ break;
194
+ default: // pending
195
+ icon = '⬜';
196
+ text = todo.content;
197
+ }
198
+ message += `${icon} ${text}\n`;
199
+ }
200
+ // Update or create tasks post
201
+ try {
202
+ if (this.tasksPostId) {
203
+ await this.mattermost.updatePost(this.tasksPostId, message);
204
+ }
205
+ else {
206
+ const post = await this.mattermost.createPost(message, this.session.threadId);
207
+ this.tasksPostId = post.id;
208
+ }
209
+ }
210
+ catch (err) {
211
+ console.error('[Session] Failed to update tasks:', err);
212
+ }
213
+ }
214
+ async handleTaskStart(toolUseId, input) {
215
+ if (!this.session)
216
+ return;
217
+ const description = input.description || 'Working...';
218
+ const subagentType = input.subagent_type || 'general';
219
+ // Post subagent status
220
+ const message = `🤖 **Subagent** *(${subagentType})*\n` +
221
+ `> ${description}\n` +
222
+ `⏳ Running...`;
223
+ try {
224
+ const post = await this.mattermost.createPost(message, this.session.threadId);
225
+ this.activeSubagents.set(toolUseId, post.id);
226
+ }
227
+ catch (err) {
228
+ console.error('[Session] Failed to post subagent status:', err);
229
+ }
230
+ }
231
+ async handleAskUserQuestion(toolUseId, input) {
232
+ if (!this.session)
233
+ return;
234
+ // If we already have pending questions, don't start another set
235
+ if (this.pendingQuestionSet) {
236
+ console.log('[Session] Questions already pending, waiting...');
237
+ return;
238
+ }
239
+ // Flush any pending content first
240
+ await this.flush();
241
+ this.session.postId = null;
242
+ this.session.content = '';
243
+ const questions = input.questions;
244
+ if (!questions || questions.length === 0)
245
+ return;
246
+ // Create a new question set - we'll ask one at a time
247
+ this.pendingQuestionSet = {
248
+ toolUseId,
249
+ currentIndex: 0,
250
+ currentPostId: null,
251
+ questions: questions.map(q => ({
252
+ header: q.header,
253
+ question: q.question,
254
+ options: q.options,
255
+ answer: null,
256
+ })),
257
+ };
258
+ // Post the first question
259
+ await this.postCurrentQuestion();
260
+ // Stop typing while waiting for answer
261
+ this.stopTyping();
262
+ }
263
+ async postCurrentQuestion() {
264
+ if (!this.session || !this.pendingQuestionSet)
265
+ return;
266
+ const { currentIndex, questions } = this.pendingQuestionSet;
267
+ if (currentIndex >= questions.length)
268
+ return;
269
+ const q = questions[currentIndex];
270
+ const total = questions.length;
271
+ // Format the question message - show "Question (1/3)" not the header
272
+ let message = `❓ **Question** *(${currentIndex + 1}/${total})*\n`;
273
+ message += `**${q.header}:** ${q.question}\n\n`;
274
+ for (let i = 0; i < q.options.length && i < 4; i++) {
275
+ const emoji = ['1️⃣', '2️⃣', '3️⃣', '4️⃣'][i];
276
+ message += `${emoji} **${q.options[i].label}**`;
277
+ if (q.options[i].description) {
278
+ message += ` - ${q.options[i].description}`;
279
+ }
280
+ message += '\n';
281
+ }
282
+ // Post the question
283
+ const post = await this.mattermost.createPost(message, this.session.threadId);
284
+ this.pendingQuestionSet.currentPostId = post.id;
285
+ // Add reaction emojis
286
+ for (let i = 0; i < q.options.length && i < 4; i++) {
287
+ try {
288
+ await this.mattermost.addReaction(post.id, REACTION_EMOJIS[i]);
289
+ }
290
+ catch (err) {
291
+ console.error(`[Session] Failed to add reaction ${REACTION_EMOJIS[i]}:`, err);
292
+ }
293
+ }
294
+ }
295
+ async handleReaction(postId, emojiName, username) {
296
+ // Check if user is allowed
297
+ if (!this.mattermost.isUserAllowed(username))
298
+ return;
299
+ // Handle approval reactions
300
+ if (this.pendingApproval && this.pendingApproval.postId === postId) {
301
+ await this.handleApprovalReaction(emojiName, username);
302
+ return;
303
+ }
304
+ // Handle question reactions - must be for current question
305
+ if (!this.pendingQuestionSet || this.pendingQuestionSet.currentPostId !== postId)
306
+ return;
307
+ const { currentIndex, questions } = this.pendingQuestionSet;
308
+ const question = questions[currentIndex];
309
+ if (!question)
310
+ return;
311
+ const optionIndex = EMOJI_TO_INDEX[emojiName];
312
+ if (optionIndex === undefined || optionIndex >= question.options.length)
313
+ return;
314
+ const selectedOption = question.options[optionIndex];
315
+ question.answer = selectedOption.label;
316
+ console.log(`[Session] User ${username} answered "${question.header}": ${selectedOption.label}`);
317
+ // Update the post to show answer
318
+ try {
319
+ await this.mattermost.updatePost(postId, `✅ **${question.header}**: ${selectedOption.label}`);
320
+ }
321
+ catch (err) {
322
+ console.error('[Session] Failed to update answered question:', err);
323
+ }
324
+ // Move to next question or finish
325
+ this.pendingQuestionSet.currentIndex++;
326
+ if (this.pendingQuestionSet.currentIndex < questions.length) {
327
+ // Post next question
328
+ await this.postCurrentQuestion();
329
+ }
330
+ else {
331
+ // All questions answered - send as follow-up message
332
+ // (CLI auto-responds with error to AskUserQuestion, so tool_result won't work)
333
+ let answersText = 'Here are my answers:\n';
334
+ for (const q of questions) {
335
+ answersText += `- **${q.header}**: ${q.answer}\n`;
336
+ }
337
+ console.log(`[Session] All questions answered, sending as message:`, answersText);
338
+ // Clear and send as regular message
339
+ this.pendingQuestionSet = null;
340
+ if (this.claude?.isRunning()) {
341
+ this.claude.sendMessage(answersText);
342
+ this.startTyping();
343
+ }
344
+ }
345
+ }
346
+ async handleApprovalReaction(emojiName, username) {
347
+ if (!this.pendingApproval)
348
+ return;
349
+ const isApprove = emojiName === '+1' || emojiName === 'thumbsup';
350
+ const isReject = emojiName === '-1' || emojiName === 'thumbsdown';
351
+ if (!isApprove && !isReject)
352
+ return;
353
+ const postId = this.pendingApproval.postId;
354
+ console.log(`[Session] User ${username} ${isApprove ? 'approved' : 'rejected'} the plan`);
355
+ // Update the post to show the decision
356
+ try {
357
+ const statusMessage = isApprove
358
+ ? `✅ **Plan approved** by @${username} - starting implementation...`
359
+ : `❌ **Changes requested** by @${username}`;
360
+ await this.mattermost.updatePost(postId, statusMessage);
361
+ }
362
+ catch (err) {
363
+ console.error('[Session] Failed to update approval post:', err);
364
+ }
365
+ // Clear pending approval and mark as approved
366
+ this.pendingApproval = null;
367
+ if (isApprove) {
368
+ this.planApproved = true;
369
+ }
370
+ // Send response to Claude
371
+ if (this.claude?.isRunning()) {
372
+ const response = isApprove
373
+ ? 'Approved. Please proceed with the implementation.'
374
+ : 'Please revise the plan. I would like some changes.';
375
+ this.claude.sendMessage(response);
376
+ this.startTyping();
377
+ }
378
+ }
379
+ formatEvent(e) {
380
+ switch (e.type) {
381
+ case 'assistant': {
382
+ const msg = e.message;
383
+ const parts = [];
384
+ for (const block of msg?.content || []) {
385
+ if (block.type === 'text' && block.text) {
386
+ parts.push(block.text);
387
+ }
388
+ else if (block.type === 'tool_use' && block.name) {
389
+ const formatted = this.formatToolUse(block.name, block.input || {});
390
+ if (formatted)
391
+ parts.push(formatted);
392
+ }
393
+ else if (block.type === 'thinking' && block.thinking) {
394
+ // Extended thinking - show abbreviated version
395
+ const thinking = block.thinking;
396
+ const preview = thinking.length > 100 ? thinking.substring(0, 100) + '...' : thinking;
397
+ parts.push(`💭 *Thinking: ${preview}*`);
398
+ }
399
+ else if (block.type === 'server_tool_use' && block.name) {
400
+ // Server-managed tools like web search
401
+ parts.push(`🌐 **${block.name}** ${block.input ? JSON.stringify(block.input).substring(0, 50) : ''}`);
402
+ }
403
+ }
404
+ return parts.length > 0 ? parts.join('\n') : null;
405
+ }
406
+ case 'tool_use': {
407
+ const tool = e.tool_use;
408
+ return this.formatToolUse(tool.name, tool.input || {}) || null;
409
+ }
410
+ case 'tool_result': {
411
+ const result = e.tool_result;
412
+ if (result.is_error)
413
+ return ` ↳ ❌ Error`;
414
+ return null;
415
+ }
416
+ case 'result': {
417
+ // Response complete - stop typing and start new post for next message
418
+ this.stopTyping();
419
+ if (this.session) {
420
+ this.flush();
421
+ this.session.postId = null;
422
+ this.session.content = '';
423
+ }
424
+ return null;
425
+ }
426
+ case 'system':
427
+ if (e.subtype === 'error')
428
+ return `❌ ${e.error}`;
429
+ return null;
430
+ default:
431
+ return null;
432
+ }
433
+ }
434
+ formatToolUse(name, input) {
435
+ const short = (p) => {
436
+ const home = process.env.HOME || '';
437
+ return p?.startsWith(home) ? '~' + p.slice(home.length) : p;
438
+ };
439
+ switch (name) {
440
+ case 'Read': return `📄 **Read** \`${short(input.file_path)}\``;
441
+ case 'Edit': {
442
+ const filePath = short(input.file_path);
443
+ const oldStr = (input.old_string || '').trim();
444
+ const newStr = (input.new_string || '').trim();
445
+ // Show diff if we have old/new strings
446
+ if (oldStr || newStr) {
447
+ const maxLines = 8;
448
+ const oldLines = oldStr.split('\n').slice(0, maxLines);
449
+ const newLines = newStr.split('\n').slice(0, maxLines);
450
+ let diff = `✏️ **Edit** \`${filePath}\`\n\`\`\`diff\n`;
451
+ for (const line of oldLines) {
452
+ diff += `- ${line}\n`;
453
+ }
454
+ if (oldStr.split('\n').length > maxLines)
455
+ diff += `- ... (${oldStr.split('\n').length - maxLines} more lines)\n`;
456
+ for (const line of newLines) {
457
+ diff += `+ ${line}\n`;
458
+ }
459
+ if (newStr.split('\n').length > maxLines)
460
+ diff += `+ ... (${newStr.split('\n').length - maxLines} more lines)\n`;
461
+ diff += '```';
462
+ return diff;
463
+ }
464
+ return `✏️ **Edit** \`${filePath}\``;
465
+ }
466
+ case 'Write': {
467
+ const filePath = short(input.file_path);
468
+ const content = input.content || '';
469
+ const lines = content.split('\n');
470
+ const lineCount = lines.length;
471
+ // Show preview of content
472
+ if (content && lineCount > 0) {
473
+ const maxLines = 6;
474
+ const previewLines = lines.slice(0, maxLines);
475
+ let preview = `📝 **Write** \`${filePath}\` *(${lineCount} lines)*\n\`\`\`\n`;
476
+ preview += previewLines.join('\n');
477
+ if (lineCount > maxLines)
478
+ preview += `\n... (${lineCount - maxLines} more lines)`;
479
+ preview += '\n```';
480
+ return preview;
481
+ }
482
+ return `📝 **Write** \`${filePath}\``;
483
+ }
484
+ case 'Bash': {
485
+ const cmd = (input.command || '').substring(0, 50);
486
+ return `💻 **Bash** \`${cmd}${cmd.length >= 50 ? '...' : ''}\``;
487
+ }
488
+ case 'Glob': return `🔍 **Glob** \`${input.pattern}\``;
489
+ case 'Grep': return `🔎 **Grep** \`${input.pattern}\``;
490
+ case 'Task': return null; // Handled specially with subagent display
491
+ case 'EnterPlanMode': return `📋 **Planning...**`;
492
+ case 'ExitPlanMode': return null; // Handled specially with approval buttons
493
+ case 'AskUserQuestion': return null; // Don't show, the question text follows
494
+ case 'TodoWrite': return null; // Handled specially with task list display
495
+ case 'WebFetch': return `🌐 **Fetching** \`${(input.url || '').substring(0, 40)}\``;
496
+ case 'WebSearch': return `🔍 **Searching** \`${input.query}\``;
497
+ default: {
498
+ // Handle MCP tools: mcp__server__tool -> 🔌 tool (server)
499
+ if (name.startsWith('mcp__')) {
500
+ const parts = name.split('__');
501
+ if (parts.length >= 3) {
502
+ const server = parts[1];
503
+ const tool = parts.slice(2).join('__');
504
+ return `🔌 **${tool}** *(${server})*`;
505
+ }
506
+ }
507
+ return `● **${name}**`;
508
+ }
509
+ }
510
+ }
511
+ appendContent(text) {
512
+ if (!this.session || !text)
513
+ return;
514
+ this.session.content += text + '\n';
515
+ this.scheduleUpdate();
516
+ }
517
+ scheduleUpdate() {
518
+ if (this.updateTimer)
519
+ return;
520
+ this.updateTimer = setTimeout(() => {
521
+ this.updateTimer = null;
522
+ this.flush();
523
+ }, 500);
524
+ }
525
+ startTyping() {
526
+ if (this.typingTimer)
527
+ return;
528
+ // Send typing immediately, then every 3 seconds
529
+ this.mattermost.sendTyping(this.session?.threadId);
530
+ this.typingTimer = setInterval(() => {
531
+ this.mattermost.sendTyping(this.session?.threadId);
532
+ }, 3000);
533
+ }
534
+ stopTyping() {
535
+ if (this.typingTimer) {
536
+ clearInterval(this.typingTimer);
537
+ this.typingTimer = null;
538
+ }
539
+ }
540
+ async flush() {
541
+ if (!this.session || !this.session.content.trim())
542
+ return;
543
+ const content = this.session.content.replace(/\n{3,}/g, '\n\n').trim();
544
+ if (this.session.postId) {
545
+ await this.mattermost.updatePost(this.session.postId, content);
546
+ }
547
+ else {
548
+ const post = await this.mattermost.createPost(content, this.session.threadId);
549
+ this.session.postId = post.id;
550
+ }
551
+ }
552
+ async handleExit(code) {
553
+ this.stopTyping();
554
+ if (this.updateTimer) {
555
+ clearTimeout(this.updateTimer);
556
+ this.updateTimer = null;
557
+ }
558
+ await this.flush();
559
+ if (code !== 0 && this.session) {
560
+ await this.mattermost.createPost(`**[Exited: ${code}]**`, this.session.threadId);
561
+ }
562
+ this.session = null;
563
+ }
564
+ isSessionActive() {
565
+ return this.session !== null;
566
+ }
567
+ isInCurrentSessionThread(threadRoot) {
568
+ return this.session?.threadId === threadRoot;
569
+ }
570
+ async sendFollowUp(message) {
571
+ if (!this.claude?.isRunning() || !this.session)
572
+ return;
573
+ this.claude.sendMessage(message);
574
+ this.startTyping();
575
+ }
576
+ killSession() {
577
+ this.stopTyping();
578
+ this.claude?.kill();
579
+ this.claude = null;
580
+ this.session = null;
581
+ }
582
+ }
@@ -0,0 +1,76 @@
1
+ export type ClaudeStreamEvent = SystemEvent | AssistantEvent | UserEvent | ToolUseEvent | ToolResultEvent | ResultEvent;
2
+ export interface SystemEvent {
3
+ type: 'system';
4
+ subtype: 'init' | 'error';
5
+ session_id?: string;
6
+ message?: string;
7
+ error?: string;
8
+ }
9
+ export interface AssistantEvent {
10
+ type: 'assistant';
11
+ message: {
12
+ id: string;
13
+ type: 'message';
14
+ role: 'assistant';
15
+ content: ContentBlock[];
16
+ model: string;
17
+ stop_reason: string | null;
18
+ stop_sequence: string | null;
19
+ };
20
+ session_id: string;
21
+ }
22
+ export interface UserEvent {
23
+ type: 'user';
24
+ message: {
25
+ role: 'user';
26
+ content: string;
27
+ };
28
+ session_id: string;
29
+ }
30
+ export interface ToolUseEvent {
31
+ type: 'tool_use';
32
+ tool_use: {
33
+ id: string;
34
+ name: string;
35
+ input: Record<string, unknown>;
36
+ };
37
+ session_id: string;
38
+ }
39
+ export interface ToolResultEvent {
40
+ type: 'tool_result';
41
+ tool_result: {
42
+ tool_use_id: string;
43
+ content: string | ContentBlock[];
44
+ is_error?: boolean;
45
+ };
46
+ session_id: string;
47
+ }
48
+ export interface ResultEvent {
49
+ type: 'result';
50
+ result: string;
51
+ session_id: string;
52
+ cost_usd?: number;
53
+ duration_ms?: number;
54
+ is_error?: boolean;
55
+ }
56
+ export type ContentBlock = TextBlock | ToolUseBlock | ToolResultBlock;
57
+ export interface TextBlock {
58
+ type: 'text';
59
+ text: string;
60
+ }
61
+ export interface ToolUseBlock {
62
+ type: 'tool_use';
63
+ id: string;
64
+ name: string;
65
+ input: Record<string, unknown>;
66
+ }
67
+ export interface ToolResultBlock {
68
+ type: 'tool_result';
69
+ tool_use_id: string;
70
+ content: string;
71
+ is_error?: boolean;
72
+ }
73
+ export interface ClaudeUserInput {
74
+ type: 'user';
75
+ content: string;
76
+ }
@@ -0,0 +1,3 @@
1
+ // Claude Code stream-json output types
2
+ // Based on https://code.claude.com/docs/en/cli-reference
3
+ export {};
@@ -0,0 +1,11 @@
1
+ export interface Config {
2
+ mattermost: {
3
+ url: string;
4
+ token: string;
5
+ channelId: string;
6
+ botName: string;
7
+ };
8
+ allowedUsers: string[];
9
+ skipPermissions: boolean;
10
+ }
11
+ export declare function loadConfig(): Config;