mattermost-claude-code 0.7.2 → 0.8.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.
@@ -3,6 +3,19 @@ export interface ClaudeEvent {
3
3
  type: string;
4
4
  [key: string]: unknown;
5
5
  }
6
+ export interface TextContentBlock {
7
+ type: 'text';
8
+ text: string;
9
+ }
10
+ export interface ImageContentBlock {
11
+ type: 'image';
12
+ source: {
13
+ type: 'base64';
14
+ media_type: string;
15
+ data: string;
16
+ };
17
+ }
18
+ export type ContentBlock = TextContentBlock | ImageContentBlock;
6
19
  export interface ClaudeCliOptions {
7
20
  workingDir: string;
8
21
  threadId?: string;
@@ -15,7 +28,7 @@ export declare class ClaudeCli extends EventEmitter {
15
28
  debug: boolean;
16
29
  constructor(options: ClaudeCliOptions);
17
30
  start(): void;
18
- sendMessage(content: string): void;
31
+ sendMessage(content: string | ContentBlock[]): void;
19
32
  sendToolResult(toolUseId: string, content: unknown): void;
20
33
  private parseOutput;
21
34
  isRunning(): boolean;
@@ -77,6 +77,7 @@ export class ClaudeCli extends EventEmitter {
77
77
  });
78
78
  }
79
79
  // Send a user message via JSON stdin
80
+ // content can be a string or an array of content blocks (for images)
80
81
  sendMessage(content) {
81
82
  if (!this.process?.stdin)
82
83
  throw new Error('Not running');
@@ -85,7 +86,10 @@ export class ClaudeCli extends EventEmitter {
85
86
  message: { role: 'user', content }
86
87
  }) + '\n';
87
88
  if (this.debug) {
88
- console.log(` [claude] Sending: ${content.substring(0, 50)}...`);
89
+ const preview = typeof content === 'string'
90
+ ? content.substring(0, 50)
91
+ : `[${content.length} blocks]`;
92
+ console.log(` [claude] Sending: ${preview}...`);
89
93
  }
90
94
  this.process.stdin.write(msg);
91
95
  }
@@ -1,5 +1,6 @@
1
1
  import { ClaudeCli } from './cli.js';
2
2
  import { MattermostClient } from '../mattermost/client.js';
3
+ import { MattermostFile } from '../mattermost/types.js';
3
4
  interface QuestionOption {
4
5
  label: string;
5
6
  description: string;
@@ -81,6 +82,7 @@ export declare class SessionManager {
81
82
  isUserAllowedInSession(threadId: string, username: string): boolean;
82
83
  startSession(options: {
83
84
  prompt: string;
85
+ files?: MattermostFile[];
84
86
  }, username: string, replyToPostId?: string): Promise<void>;
85
87
  private handleEvent;
86
88
  private handleTaskComplete;
@@ -97,6 +99,11 @@ export declare class SessionManager {
97
99
  private formatToolUse;
98
100
  private appendContent;
99
101
  private scheduleUpdate;
102
+ /**
103
+ * Build message content for Claude, including images if present.
104
+ * Returns either a string or an array of content blocks.
105
+ */
106
+ private buildMessageContent;
100
107
  private startTyping;
101
108
  private stopTyping;
102
109
  private flush;
@@ -106,7 +113,7 @@ export declare class SessionManager {
106
113
  /** Check if a session exists for this thread */
107
114
  isInSessionThread(threadRoot: string): boolean;
108
115
  /** Send a follow-up message to an existing session */
109
- sendFollowUp(threadId: string, message: string): Promise<void>;
116
+ sendFollowUp(threadId: string, message: string, files?: MattermostFile[]): Promise<void>;
110
117
  /** Kill a specific session */
111
118
  killSession(threadId: string): void;
112
119
  /** Cancel a session with user feedback */
@@ -90,7 +90,7 @@ export class SessionManager {
90
90
  const existingSession = this.sessions.get(threadId);
91
91
  if (existingSession && existingSession.claude.isRunning()) {
92
92
  // Send as follow-up instead
93
- await this.sendFollowUp(threadId, options.prompt);
93
+ await this.sendFollowUp(threadId, options.prompt, options.files);
94
94
  return;
95
95
  }
96
96
  // Check max sessions limit
@@ -155,8 +155,9 @@ export class SessionManager {
155
155
  this.sessions.delete(actualThreadId);
156
156
  return;
157
157
  }
158
- // Send the message to Claude
159
- claude.sendMessage(options.prompt);
158
+ // Send the message to Claude (with images if present)
159
+ const content = await this.buildMessageContent(options.prompt, options.files);
160
+ claude.sendMessage(content);
160
161
  }
161
162
  handleEvent(threadId, event) {
162
163
  const session = this.sessions.get(threadId);
@@ -678,6 +679,50 @@ export class SessionManager {
678
679
  this.flush(session);
679
680
  }, 500);
680
681
  }
682
+ /**
683
+ * Build message content for Claude, including images if present.
684
+ * Returns either a string or an array of content blocks.
685
+ */
686
+ async buildMessageContent(text, files) {
687
+ // Filter to only image files
688
+ const imageFiles = files?.filter(f => f.mime_type.startsWith('image/') &&
689
+ ['image/jpeg', 'image/png', 'image/gif', 'image/webp'].includes(f.mime_type)) || [];
690
+ // If no images, return plain text
691
+ if (imageFiles.length === 0) {
692
+ return text;
693
+ }
694
+ // Build content blocks with images
695
+ const blocks = [];
696
+ // Download and add each image
697
+ for (const file of imageFiles) {
698
+ try {
699
+ const buffer = await this.mattermost.downloadFile(file.id);
700
+ const base64 = buffer.toString('base64');
701
+ blocks.push({
702
+ type: 'image',
703
+ source: {
704
+ type: 'base64',
705
+ media_type: file.mime_type,
706
+ data: base64,
707
+ },
708
+ });
709
+ if (this.debug) {
710
+ console.log(` 📷 Attached image: ${file.name} (${file.mime_type}, ${Math.round(buffer.length / 1024)}KB)`);
711
+ }
712
+ }
713
+ catch (err) {
714
+ console.error(` ⚠️ Failed to download image ${file.name}:`, err);
715
+ }
716
+ }
717
+ // Add the text message
718
+ if (text) {
719
+ blocks.push({
720
+ type: 'text',
721
+ text,
722
+ });
723
+ }
724
+ return blocks;
725
+ }
681
726
  startTyping(session) {
682
727
  if (session.typingTimer)
683
728
  return;
@@ -713,6 +758,7 @@ export class SessionManager {
713
758
  return;
714
759
  // If we're intentionally restarting (e.g., !cd), don't clean up or post exit message
715
760
  if (session.isRestarting) {
761
+ session.isRestarting = false; // Reset flag here, after the exit event fires
716
762
  return;
717
763
  }
718
764
  this.stopTyping(session);
@@ -748,11 +794,12 @@ export class SessionManager {
748
794
  return session !== undefined && session.claude.isRunning();
749
795
  }
750
796
  /** Send a follow-up message to an existing session */
751
- async sendFollowUp(threadId, message) {
797
+ async sendFollowUp(threadId, message, files) {
752
798
  const session = this.sessions.get(threadId);
753
799
  if (!session || !session.claude.isRunning())
754
800
  return;
755
- session.claude.sendMessage(message);
801
+ const content = await this.buildMessageContent(message, files);
802
+ session.claude.sendMessage(content);
756
803
  session.lastActivityAt = new Date();
757
804
  this.startTyping(session);
758
805
  }
@@ -836,10 +883,10 @@ export class SessionManager {
836
883
  // Start the new Claude CLI
837
884
  try {
838
885
  session.claude.start();
839
- session.isRestarting = false; // Successfully restarted
886
+ // Note: isRestarting is reset in handleExit when the old process exit event fires
840
887
  }
841
888
  catch (err) {
842
- session.isRestarting = false; // Reset flag even on failure
889
+ session.isRestarting = false; // Reset flag on failure since exit won't fire
843
890
  console.error(' ❌ Failed to restart Claude:', err);
844
891
  await this.mattermost.createPost(`❌ Failed to restart Claude: ${err}`, threadId);
845
892
  return;
package/dist/index.js CHANGED
@@ -149,8 +149,10 @@ async function main() {
149
149
  await session.requestMessageApproval(threadRoot, username, content);
150
150
  return;
151
151
  }
152
- if (content)
153
- await session.sendFollowUp(threadRoot, content);
152
+ // Get any attached files (images)
153
+ const files = post.metadata?.files;
154
+ if (content || files?.length)
155
+ await session.sendFollowUp(threadRoot, content, files);
154
156
  return;
155
157
  }
156
158
  // New session requires @mention
@@ -161,11 +163,12 @@ async function main() {
161
163
  return;
162
164
  }
163
165
  const prompt = mattermost.extractPrompt(message);
164
- if (!prompt) {
166
+ const files = post.metadata?.files;
167
+ if (!prompt && !files?.length) {
165
168
  await mattermost.createPost(`Mention me with your request`, threadRoot);
166
169
  return;
167
170
  }
168
- await session.startSession({ prompt }, username, threadRoot);
171
+ await session.startSession({ prompt, files }, username, threadRoot);
169
172
  });
170
173
  mattermost.on('connected', () => { });
171
174
  mattermost.on('error', (e) => console.error(' ❌ Error:', e));
@@ -30,6 +30,7 @@ export declare class MattermostClient extends EventEmitter {
30
30
  updatePost(postId: string, message: string): Promise<MattermostPost>;
31
31
  addReaction(postId: string, emojiName: string): Promise<void>;
32
32
  downloadFile(fileId: string): Promise<Buffer>;
33
+ getFileInfo(fileId: string): Promise<import('./types.js').MattermostFile>;
33
34
  connect(): Promise<void>;
34
35
  private handleEvent;
35
36
  private scheduleReconnect;
@@ -102,6 +102,10 @@ export class MattermostClient extends EventEmitter {
102
102
  const arrayBuffer = await response.arrayBuffer();
103
103
  return Buffer.from(arrayBuffer);
104
104
  }
105
+ // Get file info (metadata)
106
+ async getFileInfo(fileId) {
107
+ return this.api('GET', `/files/${fileId}/info`);
108
+ }
105
109
  // Connect to WebSocket
106
110
  async connect() {
107
111
  // Get bot user first
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "mattermost-claude-code",
3
- "version": "0.7.2",
3
+ "version": "0.8.0",
4
4
  "description": "Share Claude Code sessions live in a Mattermost channel with interactive features",
5
5
  "main": "dist/index.js",
6
6
  "type": "module",