mattermost-claude-code 0.3.3 → 0.4.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.
@@ -87,6 +87,8 @@ export declare class SessionManager {
87
87
  sendFollowUp(threadId: string, message: string): Promise<void>;
88
88
  /** Kill a specific session */
89
89
  killSession(threadId: string): void;
90
+ /** Cancel a session with user feedback */
91
+ cancelSession(threadId: string, username: string): Promise<void>;
90
92
  /** Kill all active sessions (for graceful shutdown) */
91
93
  killAllSessions(): void;
92
94
  /** Cleanup idle sessions that have exceeded timeout */
@@ -129,6 +129,7 @@ export class SessionManager {
129
129
  };
130
130
  // Register session
131
131
  this.sessions.set(actualThreadId, session);
132
+ this.registerPost(post.id, actualThreadId); // For cancel reactions on session start post
132
133
  const shortId = actualThreadId.substring(0, 8);
133
134
  console.log(` ▶ Session #${this.sessions.size} started (${shortId}…) by @${username}`);
134
135
  // Start typing indicator immediately so user sees activity
@@ -388,6 +389,11 @@ export class SessionManager {
388
389
  const session = this.getSessionByPost(postId);
389
390
  if (!session)
390
391
  return;
392
+ // Handle cancel reactions (❌ or 🛑) on any post in the session
393
+ if (emojiName === 'x' || emojiName === 'octagonal_sign' || emojiName === 'stop_sign') {
394
+ await this.cancelSession(session.threadId, username);
395
+ return;
396
+ }
391
397
  // Handle approval reactions
392
398
  if (session.pendingApproval && session.pendingApproval.postId === postId) {
393
399
  await this.handleApprovalReaction(session, emojiName, username);
@@ -708,6 +714,16 @@ export class SessionManager {
708
714
  const shortId = threadId.substring(0, 8);
709
715
  console.log(` ✖ Session killed (${shortId}…) — ${this.sessions.size} active`);
710
716
  }
717
+ /** Cancel a session with user feedback */
718
+ async cancelSession(threadId, username) {
719
+ const session = this.sessions.get(threadId);
720
+ if (!session)
721
+ return;
722
+ const shortId = threadId.substring(0, 8);
723
+ console.log(` 🛑 Session (${shortId}…) cancelled by @${username}`);
724
+ await this.mattermost.createPost(`🛑 **Session cancelled** by @${username}`, threadId);
725
+ this.killSession(threadId);
726
+ }
711
727
  /** Kill all active sessions (for graceful shutdown) */
712
728
  killAllSessions() {
713
729
  const count = this.sessions.size;
package/dist/config.d.ts CHANGED
@@ -1,3 +1,14 @@
1
+ /** Check if any .env config file exists */
2
+ export declare function configExists(): boolean;
3
+ /** CLI arguments that can override config */
4
+ export interface CliArgs {
5
+ url?: string;
6
+ token?: string;
7
+ channel?: string;
8
+ botName?: string;
9
+ allowedUsers?: string;
10
+ skipPermissions?: boolean;
11
+ }
1
12
  export interface Config {
2
13
  mattermost: {
3
14
  url: string;
@@ -8,4 +19,4 @@ export interface Config {
8
19
  allowedUsers: string[];
9
20
  skipPermissions: boolean;
10
21
  }
11
- export declare function loadConfig(): Config;
22
+ export declare function loadConfig(cliArgs?: CliArgs): Config;
package/dist/config.js CHANGED
@@ -3,17 +3,17 @@ import { resolve } from 'path';
3
3
  import { existsSync } from 'fs';
4
4
  import { homedir } from 'os';
5
5
  let envLoaded = false;
6
+ // Paths to search for .env files (in order of priority)
7
+ const ENV_PATHS = [
8
+ resolve(process.cwd(), '.env'), // Current directory
9
+ resolve(homedir(), '.config', 'mm-claude', '.env'), // ~/.config/mm-claude/.env
10
+ resolve(homedir(), '.mm-claude.env'), // ~/.mm-claude.env
11
+ ];
6
12
  function loadEnv() {
7
13
  if (envLoaded)
8
14
  return;
9
15
  envLoaded = true;
10
- // Load .env file from multiple locations (in order of priority)
11
- const envPaths = [
12
- resolve(process.cwd(), '.env'), // Current directory
13
- resolve(homedir(), '.config', 'mm-claude', '.env'), // ~/.config/mm-claude/.env
14
- resolve(homedir(), '.mm-claude.env'), // ~/.mm-claude.env
15
- ];
16
- for (const envPath of envPaths) {
16
+ for (const envPath of ENV_PATHS) {
17
17
  if (existsSync(envPath)) {
18
18
  if (process.env.DEBUG === '1' || process.argv.includes('--debug')) {
19
19
  console.log(` [config] Loading from: ${envPath}`);
@@ -23,28 +23,41 @@ function loadEnv() {
23
23
  }
24
24
  }
25
25
  }
26
- function requireEnv(name) {
27
- const value = process.env[name];
26
+ /** Check if any .env config file exists */
27
+ export function configExists() {
28
+ return ENV_PATHS.some(p => existsSync(p));
29
+ }
30
+ function getRequired(cliValue, envName, name) {
31
+ const value = cliValue || process.env[envName];
28
32
  if (!value) {
29
- throw new Error(`Missing required environment variable: ${name}`);
33
+ throw new Error(`Missing required config: ${name}. Set ${envName} in .env or use --${name.toLowerCase().replace(/ /g, '-')} flag.`);
30
34
  }
31
35
  return value;
32
36
  }
33
- export function loadConfig() {
37
+ export function loadConfig(cliArgs) {
34
38
  loadEnv();
39
+ // CLI args take priority over env vars
40
+ const url = getRequired(cliArgs?.url, 'MATTERMOST_URL', 'url');
41
+ const token = getRequired(cliArgs?.token, 'MATTERMOST_TOKEN', 'token');
42
+ const channelId = getRequired(cliArgs?.channel, 'MATTERMOST_CHANNEL_ID', 'channel');
43
+ const botName = cliArgs?.botName || process.env.MATTERMOST_BOT_NAME || 'claude-code';
44
+ const allowedUsersStr = cliArgs?.allowedUsers || process.env.ALLOWED_USERS || '';
45
+ const allowedUsers = allowedUsersStr
46
+ .split(',')
47
+ .map(u => u.trim())
48
+ .filter(u => u.length > 0);
49
+ // CLI --skip-permissions, env SKIP_PERMISSIONS=true, or legacy flag
50
+ const skipPermissions = cliArgs?.skipPermissions ||
51
+ process.env.SKIP_PERMISSIONS === 'true' ||
52
+ process.argv.includes('--dangerously-skip-permissions');
35
53
  return {
36
54
  mattermost: {
37
- url: requireEnv('MATTERMOST_URL').replace(/\/$/, ''), // Remove trailing slash
38
- token: requireEnv('MATTERMOST_TOKEN'),
39
- channelId: requireEnv('MATTERMOST_CHANNEL_ID'),
40
- botName: process.env.MATTERMOST_BOT_NAME || 'claude-code',
55
+ url: url.replace(/\/$/, ''), // Remove trailing slash
56
+ token,
57
+ channelId,
58
+ botName,
41
59
  },
42
- allowedUsers: (process.env.ALLOWED_USERS || '')
43
- .split(',')
44
- .map(u => u.trim())
45
- .filter(u => u.length > 0),
46
- // SKIP_PERMISSIONS=true or --dangerously-skip-permissions flag
47
- skipPermissions: process.env.SKIP_PERMISSIONS === 'true' ||
48
- process.argv.includes('--dangerously-skip-permissions'),
60
+ allowedUsers,
61
+ skipPermissions,
49
62
  };
50
63
  }
package/dist/index.js CHANGED
@@ -1,5 +1,7 @@
1
1
  #!/usr/bin/env node
2
- import { loadConfig } from './config.js';
2
+ import { program } from 'commander';
3
+ import { loadConfig, configExists } from './config.js';
4
+ import { runOnboarding } from './onboarding.js';
3
5
  import { MattermostClient } from './mattermost/client.js';
4
6
  import { SessionManager } from './claude/session.js';
5
7
  import { readFileSync } from 'fs';
@@ -10,19 +12,44 @@ const pkg = JSON.parse(readFileSync(resolve(__dirname, '..', 'package.json'), 'u
10
12
  const dim = (s) => `\x1b[2m${s}\x1b[0m`;
11
13
  const bold = (s) => `\x1b[1m${s}\x1b[0m`;
12
14
  const cyan = (s) => `\x1b[36m${s}\x1b[0m`;
15
+ // Define CLI options
16
+ program
17
+ .name('mm-claude')
18
+ .version(pkg.version)
19
+ .description('Share Claude Code sessions in Mattermost')
20
+ .option('--url <url>', 'Mattermost server URL')
21
+ .option('--token <token>', 'Mattermost bot token')
22
+ .option('--channel <id>', 'Mattermost channel ID')
23
+ .option('--bot-name <name>', 'Bot mention name (default: claude-code)')
24
+ .option('--allowed-users <users>', 'Comma-separated allowed usernames')
25
+ .option('--skip-permissions', 'Skip interactive permission prompts')
26
+ .option('--debug', 'Enable debug logging')
27
+ .parse();
28
+ const opts = program.opts();
29
+ // Check if required args are provided via CLI
30
+ function hasRequiredCliArgs(args) {
31
+ return !!(args.url && args.token && args.channel);
32
+ }
13
33
  async function main() {
14
- if (process.argv.includes('--version') || process.argv.includes('-v')) {
15
- console.log(pkg.version);
16
- process.exit(0);
34
+ // Set debug mode from CLI flag
35
+ if (opts.debug) {
36
+ process.env.DEBUG = '1';
17
37
  }
18
- if (process.argv.includes('--help') || process.argv.includes('-h')) {
19
- console.log(`mm-claude v${pkg.version} - Share Claude Code sessions in Mattermost
20
-
21
- Usage: cd /your/project && mm-claude`);
22
- process.exit(0);
38
+ // Build CLI args object
39
+ const cliArgs = {
40
+ url: opts.url,
41
+ token: opts.token,
42
+ channel: opts.channel,
43
+ botName: opts.botName,
44
+ allowedUsers: opts.allowedUsers,
45
+ skipPermissions: opts.skipPermissions,
46
+ };
47
+ // Check if we need onboarding
48
+ if (!configExists() && !hasRequiredCliArgs(opts)) {
49
+ await runOnboarding();
23
50
  }
24
51
  const workingDir = process.cwd();
25
- const config = loadConfig();
52
+ const config = loadConfig(cliArgs);
26
53
  // Nice startup banner
27
54
  console.log('');
28
55
  console.log(bold(` 🤖 mm-claude v${pkg.version}`));
@@ -50,6 +77,13 @@ Usage: cd /your/project && mm-claude`);
50
77
  const content = mattermost.isBotMentioned(message)
51
78
  ? mattermost.extractPrompt(message)
52
79
  : message.trim();
80
+ // Check for stop/cancel commands
81
+ const lowerContent = content.toLowerCase();
82
+ if (lowerContent === '/stop' || lowerContent === 'stop' ||
83
+ lowerContent === '/cancel' || lowerContent === 'cancel') {
84
+ await session.cancelSession(threadRoot, username);
85
+ return;
86
+ }
53
87
  if (content)
54
88
  await session.sendFollowUp(threadRoot, content);
55
89
  return;
@@ -0,0 +1 @@
1
+ export declare function runOnboarding(): Promise<void>;
@@ -0,0 +1,117 @@
1
+ import prompts from 'prompts';
2
+ import { writeFileSync, mkdirSync } from 'fs';
3
+ import { homedir } from 'os';
4
+ import { resolve } from 'path';
5
+ const bold = (s) => `\x1b[1m${s}\x1b[0m`;
6
+ const dim = (s) => `\x1b[2m${s}\x1b[0m`;
7
+ const cyan = (s) => `\x1b[36m${s}\x1b[0m`;
8
+ const green = (s) => `\x1b[32m${s}\x1b[0m`;
9
+ export async function runOnboarding() {
10
+ console.log('');
11
+ console.log(bold(' Welcome to mm-claude!'));
12
+ console.log(dim(' ─────────────────────────────────'));
13
+ console.log('');
14
+ console.log(' No configuration found. Let\'s set things up.');
15
+ console.log('');
16
+ console.log(dim(' You\'ll need:'));
17
+ console.log(dim(' • A Mattermost bot account with a token'));
18
+ console.log(dim(' • A channel ID where the bot will listen'));
19
+ console.log('');
20
+ // Handle Ctrl+C gracefully
21
+ prompts.override({});
22
+ const onCancel = () => {
23
+ console.log('');
24
+ console.log(dim(' Setup cancelled.'));
25
+ process.exit(0);
26
+ };
27
+ const response = await prompts([
28
+ {
29
+ type: 'text',
30
+ name: 'url',
31
+ message: 'Mattermost URL',
32
+ initial: 'https://your-mattermost-server.com',
33
+ validate: (v) => v.startsWith('http') ? true : 'URL must start with http:// or https://',
34
+ },
35
+ {
36
+ type: 'password',
37
+ name: 'token',
38
+ message: 'Bot token',
39
+ hint: 'Create at: Integrations > Bot Accounts > Add Bot Account',
40
+ validate: (v) => v.length > 0 ? true : 'Token is required',
41
+ },
42
+ {
43
+ type: 'text',
44
+ name: 'channelId',
45
+ message: 'Channel ID',
46
+ hint: 'Click channel name > View Info > copy ID from URL',
47
+ validate: (v) => v.length > 0 ? true : 'Channel ID is required',
48
+ },
49
+ {
50
+ type: 'text',
51
+ name: 'botName',
52
+ message: 'Bot mention name',
53
+ initial: 'claude-code',
54
+ hint: 'Users will @mention this name',
55
+ },
56
+ {
57
+ type: 'text',
58
+ name: 'allowedUsers',
59
+ message: 'Allowed usernames',
60
+ initial: '',
61
+ hint: 'Comma-separated, or empty for all users',
62
+ },
63
+ {
64
+ type: 'confirm',
65
+ name: 'skipPermissions',
66
+ message: 'Skip permission prompts?',
67
+ initial: true,
68
+ hint: 'If no, you\'ll approve each action via emoji reactions',
69
+ },
70
+ ], { onCancel });
71
+ // Check if user cancelled
72
+ if (!response.url || !response.token || !response.channelId) {
73
+ console.log('');
74
+ console.log(dim(' Setup incomplete. Run mm-claude again to retry.'));
75
+ process.exit(1);
76
+ }
77
+ // Build .env content
78
+ const envContent = `# mm-claude configuration
79
+ # Generated by mm-claude onboarding
80
+
81
+ # Mattermost server URL
82
+ MATTERMOST_URL=${response.url}
83
+
84
+ # Bot token (from Integrations > Bot Accounts)
85
+ MATTERMOST_TOKEN=${response.token}
86
+
87
+ # Channel ID where the bot listens
88
+ MATTERMOST_CHANNEL_ID=${response.channelId}
89
+
90
+ # Bot mention name (users @mention this)
91
+ MATTERMOST_BOT_NAME=${response.botName || 'claude-code'}
92
+
93
+ # Allowed usernames (comma-separated, empty = all users)
94
+ ALLOWED_USERS=${response.allowedUsers || ''}
95
+
96
+ # Skip permission prompts (true = auto-approve, false = require emoji approval)
97
+ SKIP_PERMISSIONS=${response.skipPermissions ? 'true' : 'false'}
98
+ `;
99
+ // Save to ~/.config/mm-claude/.env
100
+ const configDir = resolve(homedir(), '.config', 'mm-claude');
101
+ const envPath = resolve(configDir, '.env');
102
+ try {
103
+ mkdirSync(configDir, { recursive: true });
104
+ writeFileSync(envPath, envContent, { mode: 0o600 }); // Secure permissions
105
+ }
106
+ catch (err) {
107
+ console.error('');
108
+ console.error(` Failed to save config: ${err}`);
109
+ process.exit(1);
110
+ }
111
+ console.log('');
112
+ console.log(green(' ✓ Configuration saved!'));
113
+ console.log(dim(` ${envPath}`));
114
+ console.log('');
115
+ console.log(dim(' Starting mm-claude...'));
116
+ console.log('');
117
+ }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "mattermost-claude-code",
3
- "version": "0.3.3",
3
+ "version": "0.4.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",
@@ -41,12 +41,15 @@
41
41
  ],
42
42
  "dependencies": {
43
43
  "@modelcontextprotocol/sdk": "^1.25.1",
44
+ "commander": "^14.0.2",
44
45
  "dotenv": "^16.4.7",
46
+ "prompts": "^2.4.2",
45
47
  "ws": "^8.18.0",
46
48
  "zod": "^4.2.1"
47
49
  },
48
50
  "devDependencies": {
49
51
  "@types/node": "^22.10.2",
52
+ "@types/prompts": "^2.4.9",
50
53
  "@types/ws": "^8.5.13",
51
54
  "tsx": "^4.19.2",
52
55
  "typescript": "^5.7.2"