mattermost-claude-code 0.6.0 → 0.7.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.
@@ -37,6 +37,7 @@ interface Session {
37
37
  startedAt: Date;
38
38
  lastActivityAt: Date;
39
39
  sessionNumber: number;
40
+ workingDir: string;
40
41
  claude: ClaudeCli;
41
42
  currentPostId: string | null;
42
43
  pendingContent: string;
@@ -109,6 +110,8 @@ export declare class SessionManager {
109
110
  killSession(threadId: string): void;
110
111
  /** Cancel a session with user feedback */
111
112
  cancelSession(threadId: string, username: string): Promise<void>;
113
+ /** Change working directory for a session (restarts Claude CLI) */
114
+ changeDirectory(threadId: string, newDir: string, username: string): Promise<void>;
112
115
  /** Invite a user to participate in a specific session */
113
116
  inviteUser(threadId: string, invitedUser: string, invitedBy: string): Promise<void>;
114
117
  /** Kick a user from a specific session */
@@ -115,6 +115,7 @@ export class SessionManager {
115
115
  startedAt: new Date(),
116
116
  lastActivityAt: new Date(),
117
117
  sessionNumber: this.sessions.size + 1,
118
+ workingDir: this.workingDir,
118
119
  claude,
119
120
  currentPostId: null,
120
121
  pendingContent: '',
@@ -215,13 +216,14 @@ export class SessionManager {
215
216
  session.activeSubagents.delete(toolUseId);
216
217
  }
217
218
  catch (err) {
218
- console.error('[Session] Failed to update subagent completion:', err);
219
+ console.error(' ⚠️ Failed to update subagent completion:', err);
219
220
  }
220
221
  }
221
222
  async handleExitPlanMode(session) {
222
223
  // If already approved in this session, auto-continue
223
224
  if (session.planApproved) {
224
- console.log('[Session] Plan already approved, auto-continuing...');
225
+ if (this.debug)
226
+ console.log(' ↪ Plan already approved, auto-continuing');
225
227
  if (session.claude.isRunning()) {
226
228
  session.claude.sendMessage('Continue with the implementation.');
227
229
  this.startTyping(session);
@@ -230,7 +232,8 @@ export class SessionManager {
230
232
  }
231
233
  // If we already have a pending approval, don't post another one
232
234
  if (session.pendingApproval && session.pendingApproval.type === 'plan') {
233
- console.log('[Session] Plan approval already pending, waiting...');
235
+ if (this.debug)
236
+ console.log(' ↪ Plan approval already pending, waiting');
234
237
  return;
235
238
  }
236
239
  // Flush any pending content first
@@ -251,7 +254,7 @@ export class SessionManager {
251
254
  await this.mattermost.addReaction(post.id, '-1');
252
255
  }
253
256
  catch (err) {
254
- console.error('[Session] Failed to add approval reactions:', err);
257
+ console.error(' ⚠️ Failed to add approval reactions:', err);
255
258
  }
256
259
  // Track this for reaction handling
257
260
  session.pendingApproval = { postId: post.id, type: 'plan' };
@@ -267,7 +270,7 @@ export class SessionManager {
267
270
  await this.mattermost.updatePost(session.tasksPostId, '📋 ~~Tasks~~ *(completed)*');
268
271
  }
269
272
  catch (err) {
270
- console.error('[Session] Failed to update tasks:', err);
273
+ console.error(' ⚠️ Failed to update tasks:', err);
271
274
  }
272
275
  }
273
276
  return;
@@ -303,7 +306,7 @@ export class SessionManager {
303
306
  }
304
307
  }
305
308
  catch (err) {
306
- console.error('[Session] Failed to update tasks:', err);
309
+ console.error(' ⚠️ Failed to update tasks:', err);
307
310
  }
308
311
  }
309
312
  async handleTaskStart(session, toolUseId, input) {
@@ -318,13 +321,14 @@ export class SessionManager {
318
321
  session.activeSubagents.set(toolUseId, post.id);
319
322
  }
320
323
  catch (err) {
321
- console.error('[Session] Failed to post subagent status:', err);
324
+ console.error(' ⚠️ Failed to post subagent status:', err);
322
325
  }
323
326
  }
324
327
  async handleAskUserQuestion(session, toolUseId, input) {
325
328
  // If we already have pending questions, don't start another set
326
329
  if (session.pendingQuestionSet) {
327
- console.log('[Session] Questions already pending, waiting...');
330
+ if (this.debug)
331
+ console.log(' ↪ Questions already pending, waiting');
328
332
  return;
329
333
  }
330
334
  // Flush any pending content first
@@ -381,7 +385,7 @@ export class SessionManager {
381
385
  await this.mattermost.addReaction(post.id, REACTION_EMOJIS[i]);
382
386
  }
383
387
  catch (err) {
384
- console.error(`[Session] Failed to add reaction ${REACTION_EMOJIS[i]}:`, err);
388
+ console.error(` ⚠️ Failed to add reaction ${REACTION_EMOJIS[i]}:`, err);
385
389
  }
386
390
  }
387
391
  }
@@ -429,13 +433,14 @@ export class SessionManager {
429
433
  return;
430
434
  const selectedOption = question.options[optionIndex];
431
435
  question.answer = selectedOption.label;
432
- console.log(`[Session] User ${username} answered "${question.header}": ${selectedOption.label}`);
436
+ if (this.debug)
437
+ console.log(` 💬 @${username} answered "${question.header}": ${selectedOption.label}`);
433
438
  // Update the post to show answer
434
439
  try {
435
440
  await this.mattermost.updatePost(postId, `✅ **${question.header}**: ${selectedOption.label}`);
436
441
  }
437
442
  catch (err) {
438
- console.error('[Session] Failed to update answered question:', err);
443
+ console.error(' ⚠️ Failed to update answered question:', err);
439
444
  }
440
445
  // Move to next question or finish
441
446
  session.pendingQuestionSet.currentIndex++;
@@ -449,7 +454,8 @@ export class SessionManager {
449
454
  for (const q of questions) {
450
455
  answersText += `- **${q.header}**: ${q.answer}\n`;
451
456
  }
452
- console.log(`[Session] All questions answered, sending as message:`, answersText);
457
+ if (this.debug)
458
+ console.log(' ✅ All questions answered');
453
459
  // Clear and send as regular message
454
460
  session.pendingQuestionSet = null;
455
461
  if (session.claude.isRunning()) {
@@ -466,7 +472,8 @@ export class SessionManager {
466
472
  if (!isApprove && !isReject)
467
473
  return;
468
474
  const postId = session.pendingApproval.postId;
469
- console.log(`[Session] User ${username} ${isApprove ? 'approved' : 'rejected'} the plan`);
475
+ const shortId = session.threadId.substring(0, 8);
476
+ console.log(` ${isApprove ? '✅' : '❌'} Plan ${isApprove ? 'approved' : 'rejected'} (${shortId}…) by @${username}`);
470
477
  // Update the post to show the decision
471
478
  try {
472
479
  const statusMessage = isApprove
@@ -475,7 +482,7 @@ export class SessionManager {
475
482
  await this.mattermost.updatePost(postId, statusMessage);
476
483
  }
477
484
  catch (err) {
478
- console.error('[Session] Failed to update approval post:', err);
485
+ console.error(' ⚠️ Failed to update approval post:', err);
479
486
  }
480
487
  // Clear pending approval and mark as approved
481
488
  session.pendingApproval = null;
@@ -774,6 +781,72 @@ export class SessionManager {
774
781
  await this.mattermost.createPost(`🛑 **Session cancelled** by @${username}`, threadId);
775
782
  this.killSession(threadId);
776
783
  }
784
+ /** Change working directory for a session (restarts Claude CLI) */
785
+ async changeDirectory(threadId, newDir, username) {
786
+ const session = this.sessions.get(threadId);
787
+ if (!session)
788
+ return;
789
+ // Only session owner or globally allowed users can change directory
790
+ if (session.startedBy !== username && !this.mattermost.isUserAllowed(username)) {
791
+ await this.mattermost.createPost(`⚠️ Only @${session.startedBy} or allowed users can change the working directory`, threadId);
792
+ return;
793
+ }
794
+ // Expand ~ to home directory
795
+ const expandedDir = newDir.startsWith('~')
796
+ ? newDir.replace('~', process.env.HOME || '')
797
+ : newDir;
798
+ // Resolve to absolute path
799
+ const { resolve } = await import('path');
800
+ const absoluteDir = resolve(expandedDir);
801
+ // Check if directory exists
802
+ const { existsSync, statSync } = await import('fs');
803
+ if (!existsSync(absoluteDir)) {
804
+ await this.mattermost.createPost(`❌ Directory does not exist: \`${newDir}\``, threadId);
805
+ return;
806
+ }
807
+ if (!statSync(absoluteDir).isDirectory()) {
808
+ await this.mattermost.createPost(`❌ Not a directory: \`${newDir}\``, threadId);
809
+ return;
810
+ }
811
+ const shortId = threadId.substring(0, 8);
812
+ const shortDir = absoluteDir.replace(process.env.HOME || '', '~');
813
+ console.log(` 📂 Session (${shortId}…) changing directory to ${shortDir}`);
814
+ // Stop the current Claude CLI
815
+ this.stopTyping(session);
816
+ session.claude.kill();
817
+ // Flush any pending content
818
+ await this.flush(session);
819
+ session.currentPostId = null;
820
+ session.pendingContent = '';
821
+ // Update session working directory
822
+ session.workingDir = absoluteDir;
823
+ // Create new Claude CLI with new working directory
824
+ const cliOptions = {
825
+ workingDir: absoluteDir,
826
+ threadId: threadId,
827
+ skipPermissions: this.skipPermissions || !session.forceInteractivePermissions,
828
+ };
829
+ session.claude = new ClaudeCli(cliOptions);
830
+ // Rebind event handlers
831
+ session.claude.on('event', (e) => this.handleEvent(threadId, e));
832
+ session.claude.on('exit', (code) => this.handleExit(threadId, code));
833
+ // Start the new Claude CLI
834
+ try {
835
+ session.claude.start();
836
+ }
837
+ catch (err) {
838
+ console.error(' ❌ Failed to restart Claude:', err);
839
+ await this.mattermost.createPost(`❌ Failed to restart Claude: ${err}`, threadId);
840
+ return;
841
+ }
842
+ // Update session header with new directory
843
+ await this.updateSessionHeader(session);
844
+ // Post confirmation
845
+ await this.mattermost.createPost(`📂 **Working directory changed** to \`${shortDir}\`\n*Claude Code restarted in new directory*`, threadId);
846
+ // Update activity
847
+ session.lastActivityAt = new Date();
848
+ session.timeoutWarningPosted = false;
849
+ }
777
850
  /** Invite a user to participate in a specific session */
778
851
  async inviteUser(threadId, invitedUser, invitedBy) {
779
852
  const session = this.sessions.get(threadId);
@@ -861,7 +934,8 @@ export class SessionManager {
861
934
  async updateSessionHeader(session) {
862
935
  if (!session.sessionStartPostId)
863
936
  return;
864
- const shortDir = this.workingDir.replace(process.env.HOME || '', '~');
937
+ // Use session's working directory (can be changed via !cd)
938
+ const shortDir = session.workingDir.replace(process.env.HOME || '', '~');
865
939
  // Check session-level permission override
866
940
  const isInteractive = !this.skipPermissions || session.forceInteractivePermissions;
867
941
  const permMode = isInteractive ? '🔐 Interactive' : '⚡ Auto';
@@ -895,7 +969,7 @@ export class SessionManager {
895
969
  await this.mattermost.updatePost(session.sessionStartPostId, msg);
896
970
  }
897
971
  catch (err) {
898
- console.error('[Session] Failed to update session header:', err);
972
+ console.error(' ⚠️ Failed to update session header:', err);
899
973
  }
900
974
  }
901
975
  /** Request approval for a message from an unauthorized user */
@@ -923,7 +997,7 @@ export class SessionManager {
923
997
  await this.mattermost.addReaction(post.id, '-1');
924
998
  }
925
999
  catch (err) {
926
- console.error('[Session] Failed to add message approval reactions:', err);
1000
+ console.error(' ⚠️ Failed to add message approval reactions:', err);
927
1001
  }
928
1002
  }
929
1003
  /** Kill all active sessions (for graceful shutdown) */
package/dist/index.js CHANGED
@@ -101,6 +101,7 @@ async function main() {
101
101
  `| Command | Description |\n` +
102
102
  `|:--------|:------------|\n` +
103
103
  `| \`!help\` | Show this help message |\n` +
104
+ `| \`!cd <path>\` | Change working directory (restarts Claude) |\n` +
104
105
  `| \`!invite @user\` | Invite a user to this session |\n` +
105
106
  `| \`!kick @user\` | Remove an invited user |\n` +
106
107
  `| \`!permissions interactive\` | Enable interactive permissions |\n` +
@@ -135,6 +136,12 @@ async function main() {
135
136
  }
136
137
  return;
137
138
  }
139
+ // Check for !cd command
140
+ const cdMatch = content.match(/^!cd\s+(.+)/i);
141
+ if (cdMatch) {
142
+ await session.changeDirectory(threadRoot, cdMatch[1].trim(), username);
143
+ return;
144
+ }
138
145
  // Check if user is allowed in this session
139
146
  if (!session.isUserAllowedInSession(threadRoot, username)) {
140
147
  // Request approval for their message
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "mattermost-claude-code",
3
- "version": "0.6.0",
3
+ "version": "0.7.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",