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.
- package/dist/claude/session.d.ts +2 -0
- package/dist/claude/session.js +16 -0
- package/dist/config.d.ts +12 -1
- package/dist/config.js +35 -22
- package/dist/index.js +44 -10
- package/dist/onboarding.d.ts +1 -0
- package/dist/onboarding.js +117 -0
- package/package.json +4 -1
package/dist/claude/session.d.ts
CHANGED
|
@@ -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 */
|
package/dist/claude/session.js
CHANGED
|
@@ -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
|
-
|
|
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
|
-
|
|
27
|
-
|
|
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
|
|
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:
|
|
38
|
-
token
|
|
39
|
-
channelId
|
|
40
|
-
botName
|
|
55
|
+
url: url.replace(/\/$/, ''), // Remove trailing slash
|
|
56
|
+
token,
|
|
57
|
+
channelId,
|
|
58
|
+
botName,
|
|
41
59
|
},
|
|
42
|
-
allowedUsers
|
|
43
|
-
|
|
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 {
|
|
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
|
-
|
|
15
|
-
|
|
16
|
-
process.
|
|
34
|
+
// Set debug mode from CLI flag
|
|
35
|
+
if (opts.debug) {
|
|
36
|
+
process.env.DEBUG = '1';
|
|
17
37
|
}
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
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
|
+
"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"
|