mattermost-claude-code 0.7.3 → 0.8.1
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/CHANGELOG.md +241 -0
- package/README.md +2 -0
- package/dist/changelog.d.ts +20 -0
- package/dist/changelog.js +134 -0
- package/dist/claude/cli.d.ts +14 -1
- package/dist/claude/cli.js +5 -1
- package/dist/claude/session.d.ts +8 -1
- package/dist/claude/session.js +57 -5
- package/dist/index.js +20 -4
- package/dist/mattermost/client.d.ts +1 -0
- package/dist/mattermost/client.js +4 -0
- package/package.json +2 -1
package/CHANGELOG.md
ADDED
|
@@ -0,0 +1,241 @@
|
|
|
1
|
+
# Changelog
|
|
2
|
+
|
|
3
|
+
All notable changes to this project will be documented in this file.
|
|
4
|
+
|
|
5
|
+
The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/),
|
|
6
|
+
and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
|
|
7
|
+
|
|
8
|
+
## [0.8.1] - 2025-12-28
|
|
9
|
+
|
|
10
|
+
### Added
|
|
11
|
+
- **`!release-notes` command** - Show release notes for the current version
|
|
12
|
+
- **"What's new" in session header** - Shows a brief summary of new features when starting a session
|
|
13
|
+
|
|
14
|
+
## [0.8.0] - 2025-12-28
|
|
15
|
+
|
|
16
|
+
### Added
|
|
17
|
+
- **Image attachment support** - Attach images to your messages and Claude Code will analyze them
|
|
18
|
+
- Supports JPEG, PNG, GIF, and WebP formats
|
|
19
|
+
- Images are downloaded from Mattermost and sent to Claude as base64-encoded content blocks
|
|
20
|
+
- Works for both new sessions and follow-up messages
|
|
21
|
+
- Debug logging shows attached image details (name, type, size)
|
|
22
|
+
|
|
23
|
+
## [0.7.3] - 2025-12-28
|
|
24
|
+
|
|
25
|
+
### Fixed
|
|
26
|
+
- Actually fix `!cd` showing "[Exited: null]" - reset flag in async exit handler, not synchronously
|
|
27
|
+
|
|
28
|
+
## [0.7.2] - 2025-12-28
|
|
29
|
+
|
|
30
|
+
### Fixed
|
|
31
|
+
- Fix `!cd` command showing "[Exited: null]" message - now properly suppresses exit message during intentional restart
|
|
32
|
+
|
|
33
|
+
## [0.7.1] - 2025-12-28
|
|
34
|
+
|
|
35
|
+
### Fixed
|
|
36
|
+
- Fix infinite loop when plan is approved - no longer sends "Continue" message on subsequent ExitPlanMode calls
|
|
37
|
+
|
|
38
|
+
## [0.7.0] - 2025-12-28
|
|
39
|
+
|
|
40
|
+
### Added
|
|
41
|
+
- **`!cd <path>` command** - Change working directory mid-session
|
|
42
|
+
- Restarts Claude Code in the new directory with fresh context
|
|
43
|
+
- Session header updates to show current working directory
|
|
44
|
+
- Validates directory exists before switching
|
|
45
|
+
|
|
46
|
+
## [0.6.1] - 2025-12-28
|
|
47
|
+
|
|
48
|
+
### Changed
|
|
49
|
+
- Cleaner console output: removed verbose `[Session]` prefixes from logs
|
|
50
|
+
- Debug-only logging for internal session state changes (plan approval, question handling)
|
|
51
|
+
- Consistent emoji formatting for all log messages
|
|
52
|
+
|
|
53
|
+
## [0.6.0] - 2025-12-28
|
|
54
|
+
|
|
55
|
+
### Added
|
|
56
|
+
- **Auto-update notifications** - shows banner in session header when new version is available
|
|
57
|
+
- Checks npm registry on startup for latest version
|
|
58
|
+
- Update notice includes install command: `npm install -g mattermost-claude-code`
|
|
59
|
+
|
|
60
|
+
## [0.5.9] - 2025-12-28
|
|
61
|
+
|
|
62
|
+
### Fixed
|
|
63
|
+
- Security fix: sanitize bot username in regex to prevent injection
|
|
64
|
+
|
|
65
|
+
## [0.5.8] - 2025-12-28
|
|
66
|
+
|
|
67
|
+
### Changed
|
|
68
|
+
- Commands now use `!` prefix instead of `/` to avoid Mattermost slash command conflicts
|
|
69
|
+
- `!help`, `!invite`, `!kick`, `!permissions`, `!stop` replace `/` versions
|
|
70
|
+
- Commands without prefix (`help`, `stop`, `cancel`) still work
|
|
71
|
+
|
|
72
|
+
## [0.5.7] - 2025-12-28
|
|
73
|
+
|
|
74
|
+
### Fixed
|
|
75
|
+
- Bot now recognizes mentions with hyphens in username (e.g., `@annes-minion`)
|
|
76
|
+
- Side conversation detection regex updated to handle full Mattermost usernames
|
|
77
|
+
|
|
78
|
+
## [0.5.6] - 2025-12-28
|
|
79
|
+
|
|
80
|
+
### Added
|
|
81
|
+
- Timeout warning 5 minutes before session expires
|
|
82
|
+
- Warning message tells user to send a message to keep session alive
|
|
83
|
+
- Warning resets if activity resumes
|
|
84
|
+
|
|
85
|
+
## [0.5.5] - 2025-12-28
|
|
86
|
+
|
|
87
|
+
### Added
|
|
88
|
+
- `/help` command to show available session commands
|
|
89
|
+
|
|
90
|
+
### Changed
|
|
91
|
+
- Replace ASCII diagram with Mermaid flowchart in README
|
|
92
|
+
|
|
93
|
+
## [0.5.4] - 2025-12-28 (not released)
|
|
94
|
+
|
|
95
|
+
### Added
|
|
96
|
+
- `/help` command to show available session commands
|
|
97
|
+
|
|
98
|
+
## [0.5.3] - 2025-12-28
|
|
99
|
+
|
|
100
|
+
### Added
|
|
101
|
+
- `/permissions interactive` command to enable interactive permissions for a session
|
|
102
|
+
- Can only downgrade permissions (auto → interactive), not upgrade
|
|
103
|
+
- Session header updates to show current permission mode
|
|
104
|
+
|
|
105
|
+
## [0.5.2] - 2025-12-28
|
|
106
|
+
|
|
107
|
+
### Changed
|
|
108
|
+
- Complete README rewrite with full documentation of all features
|
|
109
|
+
|
|
110
|
+
## [0.5.1] - 2025-12-28
|
|
111
|
+
|
|
112
|
+
### Added
|
|
113
|
+
- `--no-skip-permissions` flag to enable interactive permissions even when `SKIP_PERMISSIONS=true` is set in env
|
|
114
|
+
|
|
115
|
+
## [0.5.0] - 2025-12-28
|
|
116
|
+
|
|
117
|
+
### Added
|
|
118
|
+
- **Session collaboration** - invite users to specific sessions without global access
|
|
119
|
+
- **`/invite @username`** - Temporarily allow a user to participate in the current session
|
|
120
|
+
- **`/kick @username`** - Remove an invited user from the current session
|
|
121
|
+
- **Message approval flow** - When unauthorized users send messages in a session thread, the session owner/allowed users can approve via reactions:
|
|
122
|
+
- 👍 Allow this single message
|
|
123
|
+
- ✅ Invite them to the session
|
|
124
|
+
- 👎 Deny the message
|
|
125
|
+
- Per-session allowlist tracked via `sessionAllowedUsers` in each session
|
|
126
|
+
- **Side conversation support** - Messages starting with `@someone-else` are ignored, allowing users to chat without triggering the bot
|
|
127
|
+
- **Dynamic session header** - The session start message updates to show current participants when users are invited or kicked
|
|
128
|
+
|
|
129
|
+
### Changed
|
|
130
|
+
- Session owner is automatically added to session allowlist
|
|
131
|
+
- Authorization checks now use `isUserAllowedInSession()` for follow-ups
|
|
132
|
+
- Globally allowed users can still access all sessions
|
|
133
|
+
|
|
134
|
+
## [0.4.0] - 2025-12-28
|
|
135
|
+
|
|
136
|
+
### Added
|
|
137
|
+
- **CLI arguments** to override all config options (`--url`, `--token`, `--channel`, etc.)
|
|
138
|
+
- **Interactive onboarding** when no `.env` file exists - guided setup with help text
|
|
139
|
+
- Full `--help` output with all available options
|
|
140
|
+
- `--debug` flag to enable verbose logging
|
|
141
|
+
|
|
142
|
+
### Changed
|
|
143
|
+
- Switched from manual arg parsing to `commander` for better CLI experience
|
|
144
|
+
- Config now supports: CLI args > environment variables > defaults
|
|
145
|
+
|
|
146
|
+
## [0.3.4] - 2025-12-27
|
|
147
|
+
|
|
148
|
+
### Added
|
|
149
|
+
- Cancel sessions with `/stop`, `/cancel`, `stop`, or `cancel` commands in thread
|
|
150
|
+
- Cancel sessions by reacting with ❌ or 🛑 to any post in the thread
|
|
151
|
+
|
|
152
|
+
## [0.3.3] - 2025-12-27
|
|
153
|
+
|
|
154
|
+
### Added
|
|
155
|
+
- WebSocket heartbeat to detect dead connections after laptop sleep/idle
|
|
156
|
+
- Automatic reconnection when connection goes silent for 60+ seconds
|
|
157
|
+
- Ping every 30 seconds to keep connection alive
|
|
158
|
+
|
|
159
|
+
### Fixed
|
|
160
|
+
- Connections no longer go "zombie" after laptop sleep - mm-claude now detects and reconnects
|
|
161
|
+
|
|
162
|
+
## [0.3.2] - 2025-12-27
|
|
163
|
+
|
|
164
|
+
### Fixed
|
|
165
|
+
- Session card now correctly shows "mm-claude" instead of "Claude Code"
|
|
166
|
+
|
|
167
|
+
## [0.3.1] - 2025-12-27
|
|
168
|
+
|
|
169
|
+
### Changed
|
|
170
|
+
- Cleaner console output with colors (verbose logs only shown with `DEBUG=1`)
|
|
171
|
+
- Pimped session start card in Mattermost with version, directory, user, session count, permissions mode, and prompt preview
|
|
172
|
+
- Typing indicator starts immediately when session begins
|
|
173
|
+
- Shortened thread IDs in logs for readability
|
|
174
|
+
|
|
175
|
+
## [0.3.0] - 2025-12-27
|
|
176
|
+
|
|
177
|
+
### Added
|
|
178
|
+
- **Multiple concurrent sessions** - each Mattermost thread gets its own Claude CLI process
|
|
179
|
+
- Sessions tracked via `sessions: Map<threadId, Session>` and `postIndex: Map<postId, threadId>`
|
|
180
|
+
- Configurable session limits via `MAX_SESSIONS` env var (default: 5)
|
|
181
|
+
- Automatic idle session cleanup via `SESSION_TIMEOUT_MS` env var (default: 30 min)
|
|
182
|
+
- `killAllSessions()` for graceful shutdown of all sessions
|
|
183
|
+
- Session count logging for monitoring
|
|
184
|
+
|
|
185
|
+
### Changed
|
|
186
|
+
- `SessionManager` now manages multiple sessions instead of single session
|
|
187
|
+
- `sendFollowUp(threadId, message)` takes threadId parameter
|
|
188
|
+
- `isInSessionThread(threadId)` replaces `isInCurrentSessionThread()`
|
|
189
|
+
- `killSession(threadId)` takes threadId parameter
|
|
190
|
+
|
|
191
|
+
### Fixed
|
|
192
|
+
- Reaction routing now uses post index lookup for correct session targeting
|
|
193
|
+
|
|
194
|
+
## [0.2.3] - 2025-12-27
|
|
195
|
+
|
|
196
|
+
### Added
|
|
197
|
+
- GitHub Actions workflow for automated npm publishing on release
|
|
198
|
+
|
|
199
|
+
## [0.2.2] - 2025-12-27
|
|
200
|
+
|
|
201
|
+
### Added
|
|
202
|
+
- Comprehensive `CLAUDE.md` with project documentation for AI assistants
|
|
203
|
+
|
|
204
|
+
## [0.2.1] - 2025-12-27
|
|
205
|
+
|
|
206
|
+
### Added
|
|
207
|
+
- `--version` / `-v` flag to display version
|
|
208
|
+
- Version number shown in `--help` output
|
|
209
|
+
|
|
210
|
+
### Changed
|
|
211
|
+
- Lazy config loading (no .env file needed for --version/--help)
|
|
212
|
+
|
|
213
|
+
## [0.2.0] - 2025-12-27
|
|
214
|
+
|
|
215
|
+
### Added
|
|
216
|
+
- Interactive permission approval via Mattermost reactions
|
|
217
|
+
- Permission prompts forwarded to Mattermost thread
|
|
218
|
+
- React with 👍 to allow, ✅ to allow all, or 👎 to deny
|
|
219
|
+
- Only authorized users (ALLOWED_USERS) can approve permissions
|
|
220
|
+
- MCP-based permission server using Claude Code's `--permission-prompt-tool`
|
|
221
|
+
- `SKIP_PERMISSIONS` env var to control permission behavior
|
|
222
|
+
|
|
223
|
+
### Changed
|
|
224
|
+
- Permissions are now interactive by default (previously skipped)
|
|
225
|
+
- Use `SKIP_PERMISSIONS=true` or `--dangerously-skip-permissions` to skip
|
|
226
|
+
|
|
227
|
+
## [0.1.0] - 2024-12-27
|
|
228
|
+
|
|
229
|
+
### Added
|
|
230
|
+
- Initial release
|
|
231
|
+
- Connect Claude Code CLI to Mattermost channels
|
|
232
|
+
- Real-time streaming of Claude responses
|
|
233
|
+
- Interactive plan approval with emoji reactions
|
|
234
|
+
- Sequential question flow with emoji answers
|
|
235
|
+
- Task list display with live updates
|
|
236
|
+
- Code diffs for Edit operations
|
|
237
|
+
- Content preview for Write operations
|
|
238
|
+
- Subagent status tracking
|
|
239
|
+
- Typing indicator while Claude is processing
|
|
240
|
+
- User allowlist for access control
|
|
241
|
+
- Bot mention detection for triggering sessions
|
package/README.md
CHANGED
|
@@ -116,6 +116,8 @@ Type `!help` in any session thread to see available commands:
|
|
|
116
116
|
| Command | Description |
|
|
117
117
|
|:--------|:------------|
|
|
118
118
|
| `!help` | Show available commands |
|
|
119
|
+
| `!release-notes` | Show release notes for current version |
|
|
120
|
+
| `!cd <path>` | Change working directory (restarts Claude) |
|
|
119
121
|
| `!invite @user` | Invite a user to this session |
|
|
120
122
|
| `!kick @user` | Remove an invited user |
|
|
121
123
|
| `!permissions interactive` | Enable interactive permissions |
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
interface ReleaseNotes {
|
|
2
|
+
version: string;
|
|
3
|
+
date: string;
|
|
4
|
+
sections: {
|
|
5
|
+
[key: string]: string[];
|
|
6
|
+
};
|
|
7
|
+
}
|
|
8
|
+
/**
|
|
9
|
+
* Parse CHANGELOG.md and extract release notes for a specific version.
|
|
10
|
+
*/
|
|
11
|
+
export declare function getReleaseNotes(version?: string): ReleaseNotes | null;
|
|
12
|
+
/**
|
|
13
|
+
* Format release notes as a Mattermost message.
|
|
14
|
+
*/
|
|
15
|
+
export declare function formatReleaseNotes(notes: ReleaseNotes): string;
|
|
16
|
+
/**
|
|
17
|
+
* Get a short summary of what's new (for session header).
|
|
18
|
+
*/
|
|
19
|
+
export declare function getWhatsNewSummary(notes: ReleaseNotes): string;
|
|
20
|
+
export {};
|
|
@@ -0,0 +1,134 @@
|
|
|
1
|
+
import { readFileSync, existsSync } from 'fs';
|
|
2
|
+
import { dirname, resolve } from 'path';
|
|
3
|
+
import { fileURLToPath } from 'url';
|
|
4
|
+
const __dirname = dirname(fileURLToPath(import.meta.url));
|
|
5
|
+
/**
|
|
6
|
+
* Parse CHANGELOG.md and extract release notes for a specific version.
|
|
7
|
+
*/
|
|
8
|
+
export function getReleaseNotes(version) {
|
|
9
|
+
// Try to find CHANGELOG.md in various locations
|
|
10
|
+
const possiblePaths = [
|
|
11
|
+
resolve(__dirname, '..', 'CHANGELOG.md'), // dist/../CHANGELOG.md (installed)
|
|
12
|
+
resolve(__dirname, '..', '..', 'CHANGELOG.md'), // src/../CHANGELOG.md (dev)
|
|
13
|
+
];
|
|
14
|
+
let changelogPath = null;
|
|
15
|
+
for (const p of possiblePaths) {
|
|
16
|
+
if (existsSync(p)) {
|
|
17
|
+
changelogPath = p;
|
|
18
|
+
break;
|
|
19
|
+
}
|
|
20
|
+
}
|
|
21
|
+
if (!changelogPath) {
|
|
22
|
+
return null;
|
|
23
|
+
}
|
|
24
|
+
try {
|
|
25
|
+
const content = readFileSync(changelogPath, 'utf-8');
|
|
26
|
+
return parseChangelog(content, version);
|
|
27
|
+
}
|
|
28
|
+
catch {
|
|
29
|
+
return null;
|
|
30
|
+
}
|
|
31
|
+
}
|
|
32
|
+
/**
|
|
33
|
+
* Parse changelog content and extract notes for a version.
|
|
34
|
+
* If no version specified, returns the latest (first) version.
|
|
35
|
+
*/
|
|
36
|
+
function parseChangelog(content, targetVersion) {
|
|
37
|
+
const lines = content.split('\n');
|
|
38
|
+
let currentVersion = null;
|
|
39
|
+
let currentDate = null;
|
|
40
|
+
let currentSection = null;
|
|
41
|
+
let sections = {};
|
|
42
|
+
let foundTarget = false;
|
|
43
|
+
for (const line of lines) {
|
|
44
|
+
// Match version header: ## [0.8.0] - 2025-12-28
|
|
45
|
+
const versionMatch = line.match(/^## \[(\d+\.\d+\.\d+)\](?: - (\d{4}-\d{2}-\d{2}))?/);
|
|
46
|
+
if (versionMatch) {
|
|
47
|
+
// If we already found our target, we're done
|
|
48
|
+
if (foundTarget) {
|
|
49
|
+
break;
|
|
50
|
+
}
|
|
51
|
+
currentVersion = versionMatch[1];
|
|
52
|
+
currentDate = versionMatch[2] || '';
|
|
53
|
+
sections = {};
|
|
54
|
+
currentSection = null;
|
|
55
|
+
// Check if this is the version we want
|
|
56
|
+
if (!targetVersion || currentVersion === targetVersion) {
|
|
57
|
+
foundTarget = true;
|
|
58
|
+
}
|
|
59
|
+
continue;
|
|
60
|
+
}
|
|
61
|
+
// Only process if we're in the target version
|
|
62
|
+
if (!foundTarget)
|
|
63
|
+
continue;
|
|
64
|
+
// Match section header: ### Added, ### Fixed, ### Changed
|
|
65
|
+
const sectionMatch = line.match(/^### (\w+)/);
|
|
66
|
+
if (sectionMatch) {
|
|
67
|
+
currentSection = sectionMatch[1];
|
|
68
|
+
sections[currentSection] = [];
|
|
69
|
+
continue;
|
|
70
|
+
}
|
|
71
|
+
// Match list item: - Item text
|
|
72
|
+
const itemMatch = line.match(/^- (.+)/);
|
|
73
|
+
if (itemMatch && currentSection) {
|
|
74
|
+
sections[currentSection].push(itemMatch[1]);
|
|
75
|
+
}
|
|
76
|
+
}
|
|
77
|
+
if (!foundTarget || !currentVersion) {
|
|
78
|
+
return null;
|
|
79
|
+
}
|
|
80
|
+
return {
|
|
81
|
+
version: currentVersion,
|
|
82
|
+
date: currentDate || '',
|
|
83
|
+
sections,
|
|
84
|
+
};
|
|
85
|
+
}
|
|
86
|
+
/**
|
|
87
|
+
* Format release notes as a Mattermost message.
|
|
88
|
+
*/
|
|
89
|
+
export function formatReleaseNotes(notes) {
|
|
90
|
+
let msg = `### 📋 Release Notes - v${notes.version}`;
|
|
91
|
+
if (notes.date) {
|
|
92
|
+
msg += ` (${notes.date})`;
|
|
93
|
+
}
|
|
94
|
+
msg += '\n\n';
|
|
95
|
+
for (const [section, items] of Object.entries(notes.sections)) {
|
|
96
|
+
if (items.length === 0)
|
|
97
|
+
continue;
|
|
98
|
+
const emoji = section === 'Added' ? '✨' :
|
|
99
|
+
section === 'Fixed' ? '🐛' :
|
|
100
|
+
section === 'Changed' ? '🔄' :
|
|
101
|
+
section === 'Removed' ? '🗑️' : '•';
|
|
102
|
+
msg += `**${emoji} ${section}**\n`;
|
|
103
|
+
for (const item of items) {
|
|
104
|
+
msg += `- ${item}\n`;
|
|
105
|
+
}
|
|
106
|
+
msg += '\n';
|
|
107
|
+
}
|
|
108
|
+
return msg.trim();
|
|
109
|
+
}
|
|
110
|
+
/**
|
|
111
|
+
* Get a short summary of what's new (for session header).
|
|
112
|
+
*/
|
|
113
|
+
export function getWhatsNewSummary(notes) {
|
|
114
|
+
const items = [];
|
|
115
|
+
// Prioritize: Added > Fixed > Changed
|
|
116
|
+
for (const section of ['Added', 'Fixed', 'Changed']) {
|
|
117
|
+
const sectionItems = notes.sections[section] || [];
|
|
118
|
+
for (const item of sectionItems) {
|
|
119
|
+
// Extract just the first part (before any dash or detail)
|
|
120
|
+
const short = item.split(' - ')[0].replace(/\*\*/g, '');
|
|
121
|
+
if (short.length <= 50) {
|
|
122
|
+
items.push(short);
|
|
123
|
+
}
|
|
124
|
+
else {
|
|
125
|
+
items.push(short.substring(0, 47) + '...');
|
|
126
|
+
}
|
|
127
|
+
if (items.length >= 2)
|
|
128
|
+
break;
|
|
129
|
+
}
|
|
130
|
+
if (items.length >= 2)
|
|
131
|
+
break;
|
|
132
|
+
}
|
|
133
|
+
return items.join(', ');
|
|
134
|
+
}
|
package/dist/claude/cli.d.ts
CHANGED
|
@@ -3,6 +3,19 @@ export interface ClaudeEvent {
|
|
|
3
3
|
type: string;
|
|
4
4
|
[key: string]: unknown;
|
|
5
5
|
}
|
|
6
|
+
export interface TextContentBlock {
|
|
7
|
+
type: 'text';
|
|
8
|
+
text: string;
|
|
9
|
+
}
|
|
10
|
+
export interface ImageContentBlock {
|
|
11
|
+
type: 'image';
|
|
12
|
+
source: {
|
|
13
|
+
type: 'base64';
|
|
14
|
+
media_type: string;
|
|
15
|
+
data: string;
|
|
16
|
+
};
|
|
17
|
+
}
|
|
18
|
+
export type ContentBlock = TextContentBlock | ImageContentBlock;
|
|
6
19
|
export interface ClaudeCliOptions {
|
|
7
20
|
workingDir: string;
|
|
8
21
|
threadId?: string;
|
|
@@ -15,7 +28,7 @@ export declare class ClaudeCli extends EventEmitter {
|
|
|
15
28
|
debug: boolean;
|
|
16
29
|
constructor(options: ClaudeCliOptions);
|
|
17
30
|
start(): void;
|
|
18
|
-
sendMessage(content: string): void;
|
|
31
|
+
sendMessage(content: string | ContentBlock[]): void;
|
|
19
32
|
sendToolResult(toolUseId: string, content: unknown): void;
|
|
20
33
|
private parseOutput;
|
|
21
34
|
isRunning(): boolean;
|
package/dist/claude/cli.js
CHANGED
|
@@ -77,6 +77,7 @@ export class ClaudeCli extends EventEmitter {
|
|
|
77
77
|
});
|
|
78
78
|
}
|
|
79
79
|
// Send a user message via JSON stdin
|
|
80
|
+
// content can be a string or an array of content blocks (for images)
|
|
80
81
|
sendMessage(content) {
|
|
81
82
|
if (!this.process?.stdin)
|
|
82
83
|
throw new Error('Not running');
|
|
@@ -85,7 +86,10 @@ export class ClaudeCli extends EventEmitter {
|
|
|
85
86
|
message: { role: 'user', content }
|
|
86
87
|
}) + '\n';
|
|
87
88
|
if (this.debug) {
|
|
88
|
-
|
|
89
|
+
const preview = typeof content === 'string'
|
|
90
|
+
? content.substring(0, 50)
|
|
91
|
+
: `[${content.length} blocks]`;
|
|
92
|
+
console.log(` [claude] Sending: ${preview}...`);
|
|
89
93
|
}
|
|
90
94
|
this.process.stdin.write(msg);
|
|
91
95
|
}
|
package/dist/claude/session.d.ts
CHANGED
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import { ClaudeCli } from './cli.js';
|
|
2
2
|
import { MattermostClient } from '../mattermost/client.js';
|
|
3
|
+
import { MattermostFile } from '../mattermost/types.js';
|
|
3
4
|
interface QuestionOption {
|
|
4
5
|
label: string;
|
|
5
6
|
description: string;
|
|
@@ -81,6 +82,7 @@ export declare class SessionManager {
|
|
|
81
82
|
isUserAllowedInSession(threadId: string, username: string): boolean;
|
|
82
83
|
startSession(options: {
|
|
83
84
|
prompt: string;
|
|
85
|
+
files?: MattermostFile[];
|
|
84
86
|
}, username: string, replyToPostId?: string): Promise<void>;
|
|
85
87
|
private handleEvent;
|
|
86
88
|
private handleTaskComplete;
|
|
@@ -97,6 +99,11 @@ export declare class SessionManager {
|
|
|
97
99
|
private formatToolUse;
|
|
98
100
|
private appendContent;
|
|
99
101
|
private scheduleUpdate;
|
|
102
|
+
/**
|
|
103
|
+
* Build message content for Claude, including images if present.
|
|
104
|
+
* Returns either a string or an array of content blocks.
|
|
105
|
+
*/
|
|
106
|
+
private buildMessageContent;
|
|
100
107
|
private startTyping;
|
|
101
108
|
private stopTyping;
|
|
102
109
|
private flush;
|
|
@@ -106,7 +113,7 @@ export declare class SessionManager {
|
|
|
106
113
|
/** Check if a session exists for this thread */
|
|
107
114
|
isInSessionThread(threadRoot: string): boolean;
|
|
108
115
|
/** Send a follow-up message to an existing session */
|
|
109
|
-
sendFollowUp(threadId: string, message: string): Promise<void>;
|
|
116
|
+
sendFollowUp(threadId: string, message: string, files?: MattermostFile[]): Promise<void>;
|
|
110
117
|
/** Kill a specific session */
|
|
111
118
|
killSession(threadId: string): void;
|
|
112
119
|
/** Cancel a session with user feedback */
|
package/dist/claude/session.js
CHANGED
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import { ClaudeCli } from './cli.js';
|
|
2
2
|
import { getUpdateInfo } from '../update-notifier.js';
|
|
3
|
+
import { getReleaseNotes, getWhatsNewSummary } from '../changelog.js';
|
|
3
4
|
import { readFileSync } from 'fs';
|
|
4
5
|
import { dirname, resolve } from 'path';
|
|
5
6
|
import { fileURLToPath } from 'url';
|
|
@@ -90,7 +91,7 @@ export class SessionManager {
|
|
|
90
91
|
const existingSession = this.sessions.get(threadId);
|
|
91
92
|
if (existingSession && existingSession.claude.isRunning()) {
|
|
92
93
|
// Send as follow-up instead
|
|
93
|
-
await this.sendFollowUp(threadId, options.prompt);
|
|
94
|
+
await this.sendFollowUp(threadId, options.prompt, options.files);
|
|
94
95
|
return;
|
|
95
96
|
}
|
|
96
97
|
// Check max sessions limit
|
|
@@ -155,8 +156,9 @@ export class SessionManager {
|
|
|
155
156
|
this.sessions.delete(actualThreadId);
|
|
156
157
|
return;
|
|
157
158
|
}
|
|
158
|
-
// Send the message to Claude
|
|
159
|
-
|
|
159
|
+
// Send the message to Claude (with images if present)
|
|
160
|
+
const content = await this.buildMessageContent(options.prompt, options.files);
|
|
161
|
+
claude.sendMessage(content);
|
|
160
162
|
}
|
|
161
163
|
handleEvent(threadId, event) {
|
|
162
164
|
const session = this.sessions.get(threadId);
|
|
@@ -678,6 +680,50 @@ export class SessionManager {
|
|
|
678
680
|
this.flush(session);
|
|
679
681
|
}, 500);
|
|
680
682
|
}
|
|
683
|
+
/**
|
|
684
|
+
* Build message content for Claude, including images if present.
|
|
685
|
+
* Returns either a string or an array of content blocks.
|
|
686
|
+
*/
|
|
687
|
+
async buildMessageContent(text, files) {
|
|
688
|
+
// Filter to only image files
|
|
689
|
+
const imageFiles = files?.filter(f => f.mime_type.startsWith('image/') &&
|
|
690
|
+
['image/jpeg', 'image/png', 'image/gif', 'image/webp'].includes(f.mime_type)) || [];
|
|
691
|
+
// If no images, return plain text
|
|
692
|
+
if (imageFiles.length === 0) {
|
|
693
|
+
return text;
|
|
694
|
+
}
|
|
695
|
+
// Build content blocks with images
|
|
696
|
+
const blocks = [];
|
|
697
|
+
// Download and add each image
|
|
698
|
+
for (const file of imageFiles) {
|
|
699
|
+
try {
|
|
700
|
+
const buffer = await this.mattermost.downloadFile(file.id);
|
|
701
|
+
const base64 = buffer.toString('base64');
|
|
702
|
+
blocks.push({
|
|
703
|
+
type: 'image',
|
|
704
|
+
source: {
|
|
705
|
+
type: 'base64',
|
|
706
|
+
media_type: file.mime_type,
|
|
707
|
+
data: base64,
|
|
708
|
+
},
|
|
709
|
+
});
|
|
710
|
+
if (this.debug) {
|
|
711
|
+
console.log(` 📷 Attached image: ${file.name} (${file.mime_type}, ${Math.round(buffer.length / 1024)}KB)`);
|
|
712
|
+
}
|
|
713
|
+
}
|
|
714
|
+
catch (err) {
|
|
715
|
+
console.error(` ⚠️ Failed to download image ${file.name}:`, err);
|
|
716
|
+
}
|
|
717
|
+
}
|
|
718
|
+
// Add the text message
|
|
719
|
+
if (text) {
|
|
720
|
+
blocks.push({
|
|
721
|
+
type: 'text',
|
|
722
|
+
text,
|
|
723
|
+
});
|
|
724
|
+
}
|
|
725
|
+
return blocks;
|
|
726
|
+
}
|
|
681
727
|
startTyping(session) {
|
|
682
728
|
if (session.typingTimer)
|
|
683
729
|
return;
|
|
@@ -749,11 +795,12 @@ export class SessionManager {
|
|
|
749
795
|
return session !== undefined && session.claude.isRunning();
|
|
750
796
|
}
|
|
751
797
|
/** Send a follow-up message to an existing session */
|
|
752
|
-
async sendFollowUp(threadId, message) {
|
|
798
|
+
async sendFollowUp(threadId, message, files) {
|
|
753
799
|
const session = this.sessions.get(threadId);
|
|
754
800
|
if (!session || !session.claude.isRunning())
|
|
755
801
|
return;
|
|
756
|
-
|
|
802
|
+
const content = await this.buildMessageContent(message, files);
|
|
803
|
+
session.claude.sendMessage(content);
|
|
757
804
|
session.lastActivityAt = new Date();
|
|
758
805
|
this.startTyping(session);
|
|
759
806
|
}
|
|
@@ -964,9 +1011,14 @@ export class SessionManager {
|
|
|
964
1011
|
const updateNotice = updateInfo
|
|
965
1012
|
? `\n> ⚠️ **Update available:** v${updateInfo.current} → v${updateInfo.latest} - Run \`npm install -g mattermost-claude-code\`\n`
|
|
966
1013
|
: '';
|
|
1014
|
+
// Get "What's new" from release notes
|
|
1015
|
+
const releaseNotes = getReleaseNotes(pkg.version);
|
|
1016
|
+
const whatsNew = releaseNotes ? getWhatsNewSummary(releaseNotes) : '';
|
|
1017
|
+
const whatsNewLine = whatsNew ? `\n> ✨ **What's new:** ${whatsNew}\n` : '';
|
|
967
1018
|
const msg = [
|
|
968
1019
|
`### 🤖 mm-claude \`v${pkg.version}\``,
|
|
969
1020
|
updateNotice,
|
|
1021
|
+
whatsNewLine,
|
|
970
1022
|
`| | |`,
|
|
971
1023
|
`|:--|:--|`,
|
|
972
1024
|
...rows,
|
package/dist/index.js
CHANGED
|
@@ -8,6 +8,7 @@ import { readFileSync } from 'fs';
|
|
|
8
8
|
import { dirname, resolve } from 'path';
|
|
9
9
|
import { fileURLToPath } from 'url';
|
|
10
10
|
import { checkForUpdates } from './update-notifier.js';
|
|
11
|
+
import { getReleaseNotes, formatReleaseNotes } from './changelog.js';
|
|
11
12
|
const __dirname = dirname(fileURLToPath(import.meta.url));
|
|
12
13
|
const pkg = JSON.parse(readFileSync(resolve(__dirname, '..', 'package.json'), 'utf-8'));
|
|
13
14
|
const dim = (s) => `\x1b[2m${s}\x1b[0m`;
|
|
@@ -101,6 +102,7 @@ async function main() {
|
|
|
101
102
|
`| Command | Description |\n` +
|
|
102
103
|
`|:--------|:------------|\n` +
|
|
103
104
|
`| \`!help\` | Show this help message |\n` +
|
|
105
|
+
`| \`!release-notes\` | Show release notes for current version |\n` +
|
|
104
106
|
`| \`!cd <path>\` | Change working directory (restarts Claude) |\n` +
|
|
105
107
|
`| \`!invite @user\` | Invite a user to this session |\n` +
|
|
106
108
|
`| \`!kick @user\` | Remove an invited user |\n` +
|
|
@@ -111,6 +113,17 @@ async function main() {
|
|
|
111
113
|
`- ❌ or 🛑 on any message to stop session`, threadRoot);
|
|
112
114
|
return;
|
|
113
115
|
}
|
|
116
|
+
// Check for !release-notes command
|
|
117
|
+
if (lowerContent === '!release-notes' || lowerContent === '!changelog') {
|
|
118
|
+
const notes = getReleaseNotes(pkg.version);
|
|
119
|
+
if (notes) {
|
|
120
|
+
await mattermost.createPost(formatReleaseNotes(notes), threadRoot);
|
|
121
|
+
}
|
|
122
|
+
else {
|
|
123
|
+
await mattermost.createPost(`📋 **mm-claude v${pkg.version}**\n\nRelease notes not available. See [GitHub releases](https://github.com/anneschuth/mattermost-claude-code/releases).`, threadRoot);
|
|
124
|
+
}
|
|
125
|
+
return;
|
|
126
|
+
}
|
|
114
127
|
// Check for !invite command
|
|
115
128
|
const inviteMatch = content.match(/^!invite\s+@?([\w.-]+)/i);
|
|
116
129
|
if (inviteMatch) {
|
|
@@ -149,8 +162,10 @@ async function main() {
|
|
|
149
162
|
await session.requestMessageApproval(threadRoot, username, content);
|
|
150
163
|
return;
|
|
151
164
|
}
|
|
152
|
-
|
|
153
|
-
|
|
165
|
+
// Get any attached files (images)
|
|
166
|
+
const files = post.metadata?.files;
|
|
167
|
+
if (content || files?.length)
|
|
168
|
+
await session.sendFollowUp(threadRoot, content, files);
|
|
154
169
|
return;
|
|
155
170
|
}
|
|
156
171
|
// New session requires @mention
|
|
@@ -161,11 +176,12 @@ async function main() {
|
|
|
161
176
|
return;
|
|
162
177
|
}
|
|
163
178
|
const prompt = mattermost.extractPrompt(message);
|
|
164
|
-
|
|
179
|
+
const files = post.metadata?.files;
|
|
180
|
+
if (!prompt && !files?.length) {
|
|
165
181
|
await mattermost.createPost(`Mention me with your request`, threadRoot);
|
|
166
182
|
return;
|
|
167
183
|
}
|
|
168
|
-
await session.startSession({ prompt }, username, threadRoot);
|
|
184
|
+
await session.startSession({ prompt, files }, username, threadRoot);
|
|
169
185
|
});
|
|
170
186
|
mattermost.on('connected', () => { });
|
|
171
187
|
mattermost.on('error', (e) => console.error(' ❌ Error:', e));
|
|
@@ -30,6 +30,7 @@ export declare class MattermostClient extends EventEmitter {
|
|
|
30
30
|
updatePost(postId: string, message: string): Promise<MattermostPost>;
|
|
31
31
|
addReaction(postId: string, emojiName: string): Promise<void>;
|
|
32
32
|
downloadFile(fileId: string): Promise<Buffer>;
|
|
33
|
+
getFileInfo(fileId: string): Promise<import('./types.js').MattermostFile>;
|
|
33
34
|
connect(): Promise<void>;
|
|
34
35
|
private handleEvent;
|
|
35
36
|
private scheduleReconnect;
|
|
@@ -102,6 +102,10 @@ export class MattermostClient extends EventEmitter {
|
|
|
102
102
|
const arrayBuffer = await response.arrayBuffer();
|
|
103
103
|
return Buffer.from(arrayBuffer);
|
|
104
104
|
}
|
|
105
|
+
// Get file info (metadata)
|
|
106
|
+
async getFileInfo(fileId) {
|
|
107
|
+
return this.api('GET', `/files/${fileId}/info`);
|
|
108
|
+
}
|
|
105
109
|
// Connect to WebSocket
|
|
106
110
|
async connect() {
|
|
107
111
|
// Get bot user first
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "mattermost-claude-code",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.8.1",
|
|
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",
|
|
@@ -36,6 +36,7 @@
|
|
|
36
36
|
"files": [
|
|
37
37
|
"dist",
|
|
38
38
|
"README.md",
|
|
39
|
+
"CHANGELOG.md",
|
|
39
40
|
"LICENSE",
|
|
40
41
|
"package.json"
|
|
41
42
|
],
|