mattermost-claude-code 0.2.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/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2024 Anne Schuth
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
package/README.md ADDED
@@ -0,0 +1,170 @@
1
+ # Mattermost Claude Code Bridge
2
+
3
+ [![npm version](https://img.shields.io/npm/v/mattermost-claude-code.svg)](https://www.npmjs.com/package/mattermost-claude-code)
4
+ [![License: MIT](https://img.shields.io/badge/License-MIT-yellow.svg)](https://opensource.org/licenses/MIT)
5
+
6
+ Share your Claude Code sessions live in a public Mattermost channel. Your colleagues can watch you work with Claude Code in real-time, and authorized users can even trigger sessions from Mattermost.
7
+
8
+ ## How it works
9
+
10
+ ```
11
+ ┌─────────────────────────────────────────────────────────────────┐
12
+ │ Your Local Machine │
13
+ │ ┌─────────────────┐ ┌─────────────────────────────┐ │
14
+ │ │ Claude Code CLI │◄───────►│ This service │ │
15
+ │ │ (subprocess) │ stdio │ (Node.js) │ │
16
+ │ └─────────────────┘ └──────────┬──────────────────┘ │
17
+ └─────────────────────────────────────────┼───────────────────────┘
18
+ │ WebSocket + REST API
19
+ ▼ (outbound only!)
20
+ ┌─────────────────────────────────────────────────────────────────┐
21
+ │ Mattermost Server │
22
+ │ ┌─────────────────┐ ┌─────────────────────────────┐ │
23
+ │ │ Bot Account │ │ Public Channel │ │
24
+ │ │ @claude-code │◄───────►│ #claude-code-sessions │ │
25
+ │ └─────────────────┘ └─────────────────────────────┘ │
26
+ └─────────────────────────────────────────────────────────────────┘
27
+ ```
28
+
29
+ This runs entirely on your local machine - it only makes **outbound** connections to Mattermost. No port forwarding or public IP needed!
30
+
31
+ ## Prerequisites
32
+
33
+ 1. **Claude Code CLI** installed and authenticated (`claude --version`)
34
+ 2. **Node.js 18+**
35
+ 3. **Mattermost bot account** with personal access token (ask your admin)
36
+
37
+ ## Installation
38
+
39
+ ### Option 1: npm (recommended)
40
+ ```bash
41
+ npm install -g mattermost-claude-code
42
+ ```
43
+
44
+ ### Option 2: From source
45
+ ```bash
46
+ git clone https://github.com/anneschuth/mattermost-claude-code.git
47
+ cd mattermost-claude-code
48
+ npm install
49
+ npm run build
50
+ npm link
51
+ ```
52
+
53
+ ## Configuration
54
+
55
+ Create a config file at `~/.config/mm-claude/.env`:
56
+
57
+ ```bash
58
+ mkdir -p ~/.config/mm-claude
59
+ cp .env.example ~/.config/mm-claude/.env
60
+ ```
61
+
62
+ Edit the config with your Mattermost details:
63
+ ```env
64
+ MATTERMOST_URL=https://your-mattermost.com
65
+ MATTERMOST_TOKEN=your-bot-token
66
+ MATTERMOST_CHANNEL_ID=your-channel-id
67
+ MATTERMOST_BOT_NAME=claude-code
68
+
69
+ ALLOWED_USERS=anne.schuth,colleague1
70
+
71
+ DEFAULT_WORKING_DIR=/path/to/your/project
72
+ ```
73
+
74
+ ## Running
75
+
76
+ Navigate to your project directory and run:
77
+ ```bash
78
+ cd /your/project
79
+ mm-claude
80
+ ```
81
+
82
+ With debug output:
83
+ ```bash
84
+ mm-claude --debug
85
+ ```
86
+
87
+ ## Usage
88
+
89
+ In your Mattermost channel, mention the bot to start a session:
90
+
91
+ ```
92
+ @claude-code help me fix the bug in src/auth.ts
93
+ ```
94
+
95
+ The bot will:
96
+ 1. Post a session start message
97
+ 2. Stream Claude Code's responses in real-time
98
+ 3. Show tool activity (file reads, edits, bash commands)
99
+ 4. Post a session end message when complete
100
+
101
+ ## Interactive Features
102
+
103
+ ### Typing Indicator
104
+ While Claude is thinking or working, you'll see the "is typing..." indicator in Mattermost.
105
+
106
+ ### Plan Mode Approval
107
+ When Claude enters plan mode and is ready to implement:
108
+ - Bot posts an approval message with 👍/👎 reactions
109
+ - React with 👍 to approve and start building
110
+ - React with 👎 to request changes
111
+ - Once approved, subsequent plan exits auto-continue
112
+
113
+ ### Questions with Emoji Reactions
114
+ When Claude needs to ask questions:
115
+ - Questions are posted one at a time (sequential flow)
116
+ - Each question shows numbered options: 1️⃣ 2️⃣ 3️⃣ 4️⃣
117
+ - React with the corresponding emoji to answer
118
+ - After all questions are answered, Claude continues
119
+
120
+ ### Task List Display
121
+ When Claude creates a todo list (TodoWrite):
122
+ - Tasks are shown with status icons: ⬜ pending, 🔄 in progress, ✅ completed
123
+ - The task list updates in place as Claude works
124
+ - In-progress tasks show the active description
125
+
126
+ ### Subagent Status
127
+ When Claude spawns subagents (Task tool):
128
+ - Shows subagent type and description
129
+ - Updates to ✅ completed when done
130
+
131
+ ### Permission Approval via Reactions
132
+ By default, Claude Code requests permission before executing tools. This service forwards these requests to Mattermost:
133
+ - Permission requests are posted with 👍/✅/👎 reactions
134
+ - 👍 **Allow this** - approve this specific tool use
135
+ - ✅ **Allow all** - approve all future tool uses in this session
136
+ - 👎 **Deny** - reject this tool use
137
+
138
+ To skip permission prompts (use with caution):
139
+ ```bash
140
+ mm-claude --dangerously-skip-permissions
141
+ # or set in .env:
142
+ SKIP_PERMISSIONS=true
143
+ ```
144
+
145
+ ### Code Diffs and Previews
146
+ - **Edit**: Shows actual diff with `-` old lines and `+` new lines
147
+ - **Write**: Shows first 6 lines of content with line count
148
+ - **Bash**: Shows the command being executed
149
+ - **Read**: Shows the file path being read
150
+ - **MCP tools**: Shows tool name and server (e.g., `🔌 get-library-docs *(context7)*`)
151
+
152
+ ## Access Control
153
+
154
+ - **ALLOWED_USERS**: Comma-separated list of Mattermost usernames that can trigger Claude Code
155
+ - If empty, anyone in the channel can use the bot (be careful!)
156
+ - Non-authorized users get a polite rejection message
157
+
158
+ ## Message to your Mattermost admin
159
+
160
+ > "Kun je een bot account voor me aanmaken om Claude Code sessies te delen in een publiek kanaal?
161
+ > Ik heb nodig: een bot account met posting rechten, een personal access token, en de bot toegevoegd aan [kanaal naam]."
162
+
163
+ Or in English:
164
+
165
+ > "Could you create a bot account for me to share Claude Code sessions in a public channel?
166
+ > I need: bot account with posting permissions, a personal access token, and the bot added to [channel name]."
167
+
168
+ ## License
169
+
170
+ MIT
@@ -0,0 +1,24 @@
1
+ import { EventEmitter } from 'events';
2
+ export interface ClaudeEvent {
3
+ type: string;
4
+ [key: string]: unknown;
5
+ }
6
+ export interface ClaudeCliOptions {
7
+ workingDir: string;
8
+ threadId?: string;
9
+ skipPermissions?: boolean;
10
+ }
11
+ export declare class ClaudeCli extends EventEmitter {
12
+ private process;
13
+ private options;
14
+ private buffer;
15
+ debug: boolean;
16
+ constructor(options: ClaudeCliOptions);
17
+ start(): void;
18
+ sendMessage(content: string): void;
19
+ sendToolResult(toolUseId: string, content: unknown): void;
20
+ private parseOutput;
21
+ isRunning(): boolean;
22
+ kill(): void;
23
+ private getMcpServerPath;
24
+ }
@@ -0,0 +1,139 @@
1
+ import { spawn } from 'child_process';
2
+ import { EventEmitter } from 'events';
3
+ import { resolve, dirname } from 'path';
4
+ import { fileURLToPath } from 'url';
5
+ export class ClaudeCli extends EventEmitter {
6
+ process = null;
7
+ options;
8
+ buffer = '';
9
+ debug = process.env.DEBUG === '1' || process.argv.includes('--debug');
10
+ constructor(options) {
11
+ super();
12
+ this.options = options;
13
+ }
14
+ start() {
15
+ if (this.process)
16
+ throw new Error('Already running');
17
+ const claudePath = process.env.CLAUDE_PATH || 'claude';
18
+ const args = [
19
+ '--input-format', 'stream-json',
20
+ '--output-format', 'stream-json',
21
+ '--verbose',
22
+ ];
23
+ // Either use skip permissions or the MCP-based permission system
24
+ if (this.options.skipPermissions) {
25
+ args.push('--dangerously-skip-permissions');
26
+ }
27
+ else {
28
+ // Configure the permission MCP server
29
+ const mcpServerPath = this.getMcpServerPath();
30
+ const mcpConfig = {
31
+ mcpServers: {
32
+ 'mm-claude-permissions': {
33
+ type: 'stdio',
34
+ command: 'node',
35
+ args: [mcpServerPath],
36
+ env: {
37
+ MM_THREAD_ID: this.options.threadId || '',
38
+ MATTERMOST_URL: process.env.MATTERMOST_URL || '',
39
+ MATTERMOST_TOKEN: process.env.MATTERMOST_TOKEN || '',
40
+ MATTERMOST_CHANNEL_ID: process.env.MATTERMOST_CHANNEL_ID || '',
41
+ ALLOWED_USERS: process.env.ALLOWED_USERS || '',
42
+ DEBUG: this.debug ? '1' : '',
43
+ },
44
+ },
45
+ },
46
+ };
47
+ args.push('--mcp-config', JSON.stringify(mcpConfig));
48
+ args.push('--permission-prompt-tool', 'mcp__mm-claude-permissions__permission_prompt');
49
+ }
50
+ console.log(`[Claude] Starting: ${claudePath} ${args.slice(0, 5).join(' ')}...`);
51
+ this.process = spawn(claudePath, args, {
52
+ cwd: this.options.workingDir,
53
+ env: process.env,
54
+ stdio: ['pipe', 'pipe', 'pipe'],
55
+ });
56
+ this.process.stdout?.on('data', (chunk) => {
57
+ this.parseOutput(chunk.toString());
58
+ });
59
+ this.process.stderr?.on('data', (chunk) => {
60
+ console.error(`[Claude stderr] ${chunk.toString().trim()}`);
61
+ });
62
+ this.process.on('error', (err) => {
63
+ console.error('[Claude] Error:', err);
64
+ this.emit('error', err);
65
+ });
66
+ this.process.on('exit', (code) => {
67
+ console.log(`[Claude] Exited ${code}`);
68
+ this.process = null;
69
+ this.buffer = '';
70
+ this.emit('exit', code);
71
+ });
72
+ }
73
+ // Send a user message via JSON stdin
74
+ sendMessage(content) {
75
+ if (!this.process?.stdin)
76
+ throw new Error('Not running');
77
+ const msg = JSON.stringify({
78
+ type: 'user',
79
+ message: { role: 'user', content }
80
+ }) + '\n';
81
+ console.log(`[Claude] Sending: ${content.substring(0, 50)}...`);
82
+ this.process.stdin.write(msg);
83
+ }
84
+ // Send a tool result response
85
+ sendToolResult(toolUseId, content) {
86
+ if (!this.process?.stdin)
87
+ throw new Error('Not running');
88
+ const msg = JSON.stringify({
89
+ type: 'user',
90
+ message: {
91
+ role: 'user',
92
+ content: [{
93
+ type: 'tool_result',
94
+ tool_use_id: toolUseId,
95
+ content: typeof content === 'string' ? content : JSON.stringify(content)
96
+ }]
97
+ }
98
+ }) + '\n';
99
+ console.log(`[Claude] Sending tool_result for ${toolUseId}`);
100
+ this.process.stdin.write(msg);
101
+ }
102
+ parseOutput(data) {
103
+ this.buffer += data;
104
+ const lines = this.buffer.split('\n');
105
+ this.buffer = lines.pop() || '';
106
+ for (const line of lines) {
107
+ const trimmed = line.trim();
108
+ if (!trimmed)
109
+ continue;
110
+ try {
111
+ const event = JSON.parse(trimmed);
112
+ if (this.debug) {
113
+ console.log(`[DEBUG] Event: ${event.type}`, JSON.stringify(event).substring(0, 200));
114
+ }
115
+ this.emit('event', event);
116
+ }
117
+ catch {
118
+ if (this.debug) {
119
+ console.log(`[DEBUG] Raw: ${trimmed.substring(0, 200)}`);
120
+ }
121
+ }
122
+ }
123
+ }
124
+ isRunning() {
125
+ return this.process !== null;
126
+ }
127
+ kill() {
128
+ this.process?.kill('SIGTERM');
129
+ this.process = null;
130
+ }
131
+ getMcpServerPath() {
132
+ // Get the path to the MCP permission server
133
+ // When running from source: src/mcp/permission-server.ts -> dist/mcp/permission-server.js
134
+ // When installed globally: the bin entry points to dist/mcp/permission-server.js
135
+ const __filename = fileURLToPath(import.meta.url);
136
+ const __dirname = dirname(__filename);
137
+ return resolve(__dirname, '..', 'mcp', 'permission-server.js');
138
+ }
139
+ }
@@ -0,0 +1,41 @@
1
+ import { MattermostClient } from '../mattermost/client.js';
2
+ export declare class SessionManager {
3
+ private claude;
4
+ private mattermost;
5
+ private workingDir;
6
+ private skipPermissions;
7
+ private session;
8
+ private updateTimer;
9
+ private typingTimer;
10
+ private pendingQuestionSet;
11
+ private pendingApproval;
12
+ private planApproved;
13
+ private tasksPostId;
14
+ private activeSubagents;
15
+ private debug;
16
+ constructor(mattermost: MattermostClient, workingDir: string, skipPermissions?: boolean);
17
+ startSession(options: {
18
+ prompt: string;
19
+ }, username: string, replyToPostId?: string): Promise<void>;
20
+ private handleEvent;
21
+ private handleTaskComplete;
22
+ private handleExitPlanMode;
23
+ private handleTodoWrite;
24
+ private handleTaskStart;
25
+ private handleAskUserQuestion;
26
+ private postCurrentQuestion;
27
+ private handleReaction;
28
+ private handleApprovalReaction;
29
+ private formatEvent;
30
+ private formatToolUse;
31
+ private appendContent;
32
+ private scheduleUpdate;
33
+ private startTyping;
34
+ private stopTyping;
35
+ private flush;
36
+ private handleExit;
37
+ isSessionActive(): boolean;
38
+ isInCurrentSessionThread(threadRoot: string): boolean;
39
+ sendFollowUp(message: string): Promise<void>;
40
+ killSession(): void;
41
+ }