mattermost-claude-code 0.3.0 → 0.3.2

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.
@@ -47,7 +47,9 @@ export class ClaudeCli extends EventEmitter {
47
47
  args.push('--mcp-config', JSON.stringify(mcpConfig));
48
48
  args.push('--permission-prompt-tool', 'mcp__mm-claude-permissions__permission_prompt');
49
49
  }
50
- console.log(`[Claude] Starting: ${claudePath} ${args.slice(0, 5).join(' ')}...`);
50
+ if (this.debug) {
51
+ console.log(` [claude] Starting: ${claudePath} ${args.slice(0, 5).join(' ')}...`);
52
+ }
51
53
  this.process = spawn(claudePath, args, {
52
54
  cwd: this.options.workingDir,
53
55
  env: process.env,
@@ -57,14 +59,18 @@ export class ClaudeCli extends EventEmitter {
57
59
  this.parseOutput(chunk.toString());
58
60
  });
59
61
  this.process.stderr?.on('data', (chunk) => {
60
- console.error(`[Claude stderr] ${chunk.toString().trim()}`);
62
+ if (this.debug) {
63
+ console.error(` [claude:err] ${chunk.toString().trim()}`);
64
+ }
61
65
  });
62
66
  this.process.on('error', (err) => {
63
- console.error('[Claude] Error:', err);
67
+ console.error('Claude error:', err);
64
68
  this.emit('error', err);
65
69
  });
66
70
  this.process.on('exit', (code) => {
67
- console.log(`[Claude] Exited ${code}`);
71
+ if (this.debug) {
72
+ console.log(` [claude] Exited ${code}`);
73
+ }
68
74
  this.process = null;
69
75
  this.buffer = '';
70
76
  this.emit('exit', code);
@@ -78,7 +84,9 @@ export class ClaudeCli extends EventEmitter {
78
84
  type: 'user',
79
85
  message: { role: 'user', content }
80
86
  }) + '\n';
81
- console.log(`[Claude] Sending: ${content.substring(0, 50)}...`);
87
+ if (this.debug) {
88
+ console.log(` [claude] Sending: ${content.substring(0, 50)}...`);
89
+ }
82
90
  this.process.stdin.write(msg);
83
91
  }
84
92
  // Send a tool result response
@@ -96,7 +104,9 @@ export class ClaudeCli extends EventEmitter {
96
104
  }]
97
105
  }
98
106
  }) + '\n';
99
- console.log(`[Claude] Sending tool_result for ${toolUseId}`);
107
+ if (this.debug) {
108
+ console.log(` [claude] Sending tool_result for ${toolUseId}`);
109
+ }
100
110
  this.process.stdin.write(msg);
101
111
  }
102
112
  parseOutput(data) {
@@ -1,4 +1,9 @@
1
1
  import { ClaudeCli } from './cli.js';
2
+ import { readFileSync } from 'fs';
3
+ import { dirname, resolve } from 'path';
4
+ import { fileURLToPath } from 'url';
5
+ const __dirname = dirname(fileURLToPath(import.meta.url));
6
+ const pkg = JSON.parse(readFileSync(resolve(__dirname, '..', '..', 'package.json'), 'utf-8'));
2
7
  const REACTION_EMOJIS = ['one', 'two', 'three', 'four'];
3
8
  const EMOJI_TO_INDEX = {
4
9
  'one': 0, '1️⃣': 0,
@@ -78,7 +83,24 @@ export class SessionManager {
78
83
  return;
79
84
  }
80
85
  // Post session start message
81
- const msg = `🚀 **Session started**\n> Working directory: \`${this.workingDir}\``;
86
+ const shortDir = this.workingDir.replace(process.env.HOME || '', '~');
87
+ const sessionNum = this.sessions.size + 1;
88
+ const permMode = this.skipPermissions ? '⚡ Auto' : '🔐 Interactive';
89
+ const promptPreview = options.prompt.length > 60
90
+ ? options.prompt.substring(0, 60) + '…'
91
+ : options.prompt;
92
+ const msg = [
93
+ `### 🤖 mm-claude \`v${pkg.version}\``,
94
+ ``,
95
+ `| | |`,
96
+ `|:--|:--|`,
97
+ `| 📂 **Directory** | \`${shortDir}\` |`,
98
+ `| 👤 **Started by** | @${username} |`,
99
+ `| 🔢 **Session** | #${sessionNum} of ${MAX_SESSIONS} max |`,
100
+ `| ${permMode.split(' ')[0]} **Permissions** | ${permMode.split(' ')[1]} |`,
101
+ ``,
102
+ `> ${promptPreview}`,
103
+ ].join('\n');
82
104
  const post = await this.mattermost.createPost(msg, replyToPostId);
83
105
  const actualThreadId = replyToPostId || post.id;
84
106
  // Create Claude CLI with options
@@ -107,7 +129,10 @@ export class SessionManager {
107
129
  };
108
130
  // Register session
109
131
  this.sessions.set(actualThreadId, session);
110
- console.log(`[Sessions] Started session for thread ${actualThreadId} by ${username} (active: ${this.sessions.size})`);
132
+ const shortId = actualThreadId.substring(0, 8);
133
+ console.log(` ▶ Session #${this.sessions.size} started (${shortId}…) by @${username}`);
134
+ // Start typing indicator immediately so user sees activity
135
+ this.startTyping(session);
111
136
  // Bind event handlers with closure over threadId
112
137
  claude.on('event', (e) => this.handleEvent(actualThreadId, e));
113
138
  claude.on('exit', (code) => this.handleExit(actualThreadId, code));
@@ -115,14 +140,14 @@ export class SessionManager {
115
140
  claude.start();
116
141
  }
117
142
  catch (err) {
118
- console.error('[Session] Start error:', err);
143
+ console.error(' Failed to start Claude:', err);
144
+ this.stopTyping(session);
119
145
  await this.mattermost.createPost(`❌ ${err}`, actualThreadId);
120
146
  this.sessions.delete(actualThreadId);
121
147
  return;
122
148
  }
123
- // Send the message and start typing indicator
149
+ // Send the message to Claude
124
150
  claude.sendMessage(options.prompt);
125
- this.startTyping(session);
126
151
  }
127
152
  handleEvent(threadId, event) {
128
153
  const session = this.sessions.get(threadId);
@@ -642,7 +667,8 @@ export class SessionManager {
642
667
  this.postIndex.delete(postId);
643
668
  }
644
669
  }
645
- console.log(`[Sessions] Session ended for thread ${threadId} (remaining: ${this.sessions.size})`);
670
+ const shortId = threadId.substring(0, 8);
671
+ console.log(` ■ Session ended (${shortId}…) — ${this.sessions.size} active`);
646
672
  }
647
673
  // ---------------------------------------------------------------------------
648
674
  // Public Session API
@@ -679,14 +705,15 @@ export class SessionManager {
679
705
  this.postIndex.delete(postId);
680
706
  }
681
707
  }
682
- console.log(`[Sessions] Session killed for thread ${threadId} (remaining: ${this.sessions.size})`);
708
+ const shortId = threadId.substring(0, 8);
709
+ console.log(` ✖ Session killed (${shortId}…) — ${this.sessions.size} active`);
683
710
  }
684
711
  /** Kill all active sessions (for graceful shutdown) */
685
712
  killAllSessions() {
686
- for (const [threadId, session] of this.sessions.entries()) {
713
+ const count = this.sessions.size;
714
+ for (const [, session] of this.sessions.entries()) {
687
715
  this.stopTyping(session);
688
716
  session.claude.kill();
689
- console.log(`[Sessions] Killed session for thread ${threadId}`);
690
717
  }
691
718
  this.sessions.clear();
692
719
  this.postIndex.clear();
@@ -694,7 +721,9 @@ export class SessionManager {
694
721
  clearInterval(this.cleanupTimer);
695
722
  this.cleanupTimer = null;
696
723
  }
697
- console.log(`[Sessions] All sessions killed`);
724
+ if (count > 0) {
725
+ console.log(` ✖ Killed ${count} session${count === 1 ? '' : 's'}`);
726
+ }
698
727
  }
699
728
  /** Cleanup idle sessions that have exceeded timeout */
700
729
  cleanupIdleSessions() {
@@ -702,8 +731,10 @@ export class SessionManager {
702
731
  for (const [threadId, session] of this.sessions.entries()) {
703
732
  const idleTime = now - session.lastActivityAt.getTime();
704
733
  if (idleTime > SESSION_TIMEOUT_MS) {
705
- console.log(`[Sessions] Session ${threadId} timed out after ${Math.round(idleTime / 60000)} minutes`);
706
- this.mattermost.createPost(`⏰ **Session timed out** - no activity for ${Math.round(idleTime / 60000)} minutes`, session.threadId).catch(err => console.error('[Sessions] Failed to post timeout message:', err));
734
+ const mins = Math.round(idleTime / 60000);
735
+ const shortId = threadId.substring(0, 8);
736
+ console.log(` ⏰ Session (${shortId}…) timed out after ${mins}m idle`);
737
+ this.mattermost.createPost(`⏰ **Session timed out** — no activity for ${mins} minutes`, session.threadId).catch(() => { });
707
738
  this.killSession(threadId);
708
739
  }
709
740
  }
package/dist/config.js CHANGED
@@ -15,7 +15,9 @@ function loadEnv() {
15
15
  ];
16
16
  for (const envPath of envPaths) {
17
17
  if (existsSync(envPath)) {
18
- console.log(`📄 Loading config from: ${envPath}`);
18
+ if (process.env.DEBUG === '1' || process.argv.includes('--debug')) {
19
+ console.log(` [config] Loading from: ${envPath}`);
20
+ }
19
21
  config({ path: envPath });
20
22
  break;
21
23
  }
package/dist/index.js CHANGED
@@ -7,6 +7,9 @@ import { dirname, resolve } from 'path';
7
7
  import { fileURLToPath } from 'url';
8
8
  const __dirname = dirname(fileURLToPath(import.meta.url));
9
9
  const pkg = JSON.parse(readFileSync(resolve(__dirname, '..', 'package.json'), 'utf-8'));
10
+ const dim = (s) => `\x1b[2m${s}\x1b[0m`;
11
+ const bold = (s) => `\x1b[1m${s}\x1b[0m`;
12
+ const cyan = (s) => `\x1b[36m${s}\x1b[0m`;
10
13
  async function main() {
11
14
  if (process.argv.includes('--version') || process.argv.includes('-v')) {
12
15
  console.log(pkg.version);
@@ -20,17 +23,22 @@ Usage: cd /your/project && mm-claude`);
20
23
  }
21
24
  const workingDir = process.cwd();
22
25
  const config = loadConfig();
23
- console.log(`🚀 mm-claude starting...`);
24
- console.log(`📂 ${workingDir}`);
25
- console.log(`📝 @${config.mattermost.botName} on ${config.mattermost.url}`);
26
- const mattermost = new MattermostClient(config);
27
- const session = new SessionManager(mattermost, workingDir, config.skipPermissions);
26
+ // Nice startup banner
27
+ console.log('');
28
+ console.log(bold(` 🤖 mm-claude v${pkg.version}`));
29
+ console.log(dim(' ─────────────────────────────────'));
30
+ console.log(` 📂 ${cyan(workingDir)}`);
31
+ console.log(` 💬 ${cyan('@' + config.mattermost.botName)}`);
32
+ console.log(` 🌐 ${dim(config.mattermost.url)}`);
28
33
  if (config.skipPermissions) {
29
- console.log('⚠️ Permissions skipped (--dangerously-skip-permissions)');
34
+ console.log(` ⚠️ ${dim('Permissions disabled')}`);
30
35
  }
31
36
  else {
32
- console.log('🔐 Interactive permissions enabled');
37
+ console.log(` 🔐 ${dim('Interactive permissions')}`);
33
38
  }
39
+ console.log('');
40
+ const mattermost = new MattermostClient(config);
41
+ const session = new SessionManager(mattermost, workingDir, config.skipPermissions);
34
42
  mattermost.on('message', async (post, user) => {
35
43
  const username = user?.username || 'unknown';
36
44
  const message = post.message;
@@ -60,12 +68,14 @@ Usage: cd /your/project && mm-claude`);
60
68
  }
61
69
  await session.startSession({ prompt }, username, threadRoot);
62
70
  });
63
- mattermost.on('connected', () => console.log('✅ Connected'));
64
- mattermost.on('error', (e) => console.error('❌', e));
71
+ mattermost.on('connected', () => { });
72
+ mattermost.on('error', (e) => console.error(' Error:', e));
65
73
  await mattermost.connect();
66
- console.log(`🎉 Ready! @${config.mattermost.botName}`);
74
+ console.log(` ✅ ${bold('Ready!')} Waiting for @${config.mattermost.botName} mentions...`);
75
+ console.log('');
67
76
  const shutdown = () => {
68
- console.log('\n👋 Bye');
77
+ console.log('');
78
+ console.log(` 👋 ${dim('Shutting down...')}`);
69
79
  session.killAllSessions();
70
80
  mattermost.disconnect();
71
81
  process.exit(0);
@@ -16,7 +16,9 @@ export declare class MattermostClient extends EventEmitter {
16
16
  private reconnectDelay;
17
17
  private userCache;
18
18
  private botUserId;
19
+ private debug;
19
20
  constructor(config: Config);
21
+ private log;
20
22
  private api;
21
23
  getBotUser(): Promise<MattermostUser>;
22
24
  getUser(userId: string): Promise<MattermostUser | null>;
@@ -8,10 +8,15 @@ export class MattermostClient extends EventEmitter {
8
8
  reconnectDelay = 1000;
9
9
  userCache = new Map();
10
10
  botUserId = null;
11
+ debug = process.env.DEBUG === '1' || process.argv.includes('--debug');
11
12
  constructor(config) {
12
13
  super();
13
14
  this.config = config;
14
15
  }
16
+ log(msg) {
17
+ if (this.debug)
18
+ console.log(` [ws] ${msg}`);
19
+ }
15
20
  // REST API helper
16
21
  async api(method, path, body) {
17
22
  const url = `${this.config.mattermost.url}/api/v4${path}`;
@@ -78,14 +83,14 @@ export class MattermostClient extends EventEmitter {
78
83
  async connect() {
79
84
  // Get bot user first
80
85
  await this.getBotUser();
81
- console.log(`[MM] Bot user ID: ${this.botUserId}`);
86
+ this.log(`Bot user ID: ${this.botUserId}`);
82
87
  const wsUrl = this.config.mattermost.url
83
88
  .replace(/^http/, 'ws')
84
89
  .concat('/api/v4/websocket');
85
90
  return new Promise((resolve, reject) => {
86
91
  this.ws = new WebSocket(wsUrl);
87
92
  this.ws.on('open', () => {
88
- console.log('[MM] WebSocket connected');
93
+ this.log('WebSocket connected');
89
94
  // Authenticate
90
95
  this.ws.send(JSON.stringify({
91
96
  seq: 1,
@@ -105,16 +110,16 @@ export class MattermostClient extends EventEmitter {
105
110
  }
106
111
  }
107
112
  catch (err) {
108
- console.error('[MM] Failed to parse WebSocket message:', err);
113
+ this.log(`Failed to parse message: ${err}`);
109
114
  }
110
115
  });
111
116
  this.ws.on('close', () => {
112
- console.log('[MM] WebSocket disconnected');
117
+ this.log('WebSocket disconnected');
113
118
  this.emit('disconnected');
114
119
  this.scheduleReconnect();
115
120
  });
116
121
  this.ws.on('error', (err) => {
117
- console.error('[MM] WebSocket error:', err);
122
+ this.log(`WebSocket error: ${err}`);
118
123
  this.emit('error', err);
119
124
  reject(err);
120
125
  });
@@ -140,7 +145,7 @@ export class MattermostClient extends EventEmitter {
140
145
  });
141
146
  }
142
147
  catch (err) {
143
- console.error('[MM] Failed to parse post:', err);
148
+ this.log(`Failed to parse post: ${err}`);
144
149
  }
145
150
  return;
146
151
  }
@@ -160,21 +165,21 @@ export class MattermostClient extends EventEmitter {
160
165
  });
161
166
  }
162
167
  catch (err) {
163
- console.error('[MM] Failed to parse reaction:', err);
168
+ this.log(`Failed to parse reaction: ${err}`);
164
169
  }
165
170
  }
166
171
  }
167
172
  scheduleReconnect() {
168
173
  if (this.reconnectAttempts >= this.maxReconnectAttempts) {
169
- console.error('[MM] Max reconnection attempts reached');
174
+ console.error(' ⚠️ Max reconnection attempts reached');
170
175
  return;
171
176
  }
172
177
  this.reconnectAttempts++;
173
178
  const delay = this.reconnectDelay * Math.pow(2, this.reconnectAttempts - 1);
174
- console.log(`[MM] Reconnecting in ${delay}ms (attempt ${this.reconnectAttempts})`);
179
+ console.log(` 🔄 Reconnecting... (attempt ${this.reconnectAttempts})`);
175
180
  setTimeout(() => {
176
181
  this.connect().catch((err) => {
177
- console.error('[MM] Reconnection failed:', err);
182
+ console.error(` ❌ Reconnection failed: ${err}`);
178
183
  });
179
184
  }, delay);
180
185
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "mattermost-claude-code",
3
- "version": "0.3.0",
3
+ "version": "0.3.2",
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",