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 +21 -0
- package/README.md +170 -0
- package/dist/claude/cli.d.ts +24 -0
- package/dist/claude/cli.js +139 -0
- package/dist/claude/session.d.ts +41 -0
- package/dist/claude/session.js +582 -0
- package/dist/claude/types.d.ts +76 -0
- package/dist/claude/types.js +3 -0
- package/dist/config.d.ts +11 -0
- package/dist/config.js +41 -0
- package/dist/index.d.ts +2 -0
- package/dist/index.js +67 -0
- package/dist/mattermost/client.d.ts +35 -0
- package/dist/mattermost/client.js +226 -0
- package/dist/mattermost/message-formatter.d.ts +28 -0
- package/dist/mattermost/message-formatter.js +244 -0
- package/dist/mattermost/types.d.ts +62 -0
- package/dist/mattermost/types.js +1 -0
- package/dist/mcp/permission-server.d.ts +2 -0
- package/dist/mcp/permission-server.js +272 -0
- package/package.json +56 -0
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
|
+
[](https://www.npmjs.com/package/mattermost-claude-code)
|
|
4
|
+
[](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
|
+
}
|