mattermost-claude-code 0.3.0 → 0.3.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/dist/claude/cli.js +16 -6
- package/dist/claude/session.js +43 -12
- package/dist/config.js +3 -1
- package/dist/index.js +21 -11
- package/dist/mattermost/client.d.ts +2 -0
- package/dist/mattermost/client.js +15 -10
- package/package.json +1 -1
package/dist/claude/cli.js
CHANGED
|
@@ -47,7 +47,9 @@ export class ClaudeCli extends EventEmitter {
|
|
|
47
47
|
args.push('--mcp-config', JSON.stringify(mcpConfig));
|
|
48
48
|
args.push('--permission-prompt-tool', 'mcp__mm-claude-permissions__permission_prompt');
|
|
49
49
|
}
|
|
50
|
-
|
|
50
|
+
if (this.debug) {
|
|
51
|
+
console.log(` [claude] Starting: ${claudePath} ${args.slice(0, 5).join(' ')}...`);
|
|
52
|
+
}
|
|
51
53
|
this.process = spawn(claudePath, args, {
|
|
52
54
|
cwd: this.options.workingDir,
|
|
53
55
|
env: process.env,
|
|
@@ -57,14 +59,18 @@ export class ClaudeCli extends EventEmitter {
|
|
|
57
59
|
this.parseOutput(chunk.toString());
|
|
58
60
|
});
|
|
59
61
|
this.process.stderr?.on('data', (chunk) => {
|
|
60
|
-
|
|
62
|
+
if (this.debug) {
|
|
63
|
+
console.error(` [claude:err] ${chunk.toString().trim()}`);
|
|
64
|
+
}
|
|
61
65
|
});
|
|
62
66
|
this.process.on('error', (err) => {
|
|
63
|
-
console.error('
|
|
67
|
+
console.error(' ❌ Claude error:', err);
|
|
64
68
|
this.emit('error', err);
|
|
65
69
|
});
|
|
66
70
|
this.process.on('exit', (code) => {
|
|
67
|
-
|
|
71
|
+
if (this.debug) {
|
|
72
|
+
console.log(` [claude] Exited ${code}`);
|
|
73
|
+
}
|
|
68
74
|
this.process = null;
|
|
69
75
|
this.buffer = '';
|
|
70
76
|
this.emit('exit', code);
|
|
@@ -78,7 +84,9 @@ export class ClaudeCli extends EventEmitter {
|
|
|
78
84
|
type: 'user',
|
|
79
85
|
message: { role: 'user', content }
|
|
80
86
|
}) + '\n';
|
|
81
|
-
|
|
87
|
+
if (this.debug) {
|
|
88
|
+
console.log(` [claude] Sending: ${content.substring(0, 50)}...`);
|
|
89
|
+
}
|
|
82
90
|
this.process.stdin.write(msg);
|
|
83
91
|
}
|
|
84
92
|
// Send a tool result response
|
|
@@ -96,7 +104,9 @@ export class ClaudeCli extends EventEmitter {
|
|
|
96
104
|
}]
|
|
97
105
|
}
|
|
98
106
|
}) + '\n';
|
|
99
|
-
|
|
107
|
+
if (this.debug) {
|
|
108
|
+
console.log(` [claude] Sending tool_result for ${toolUseId}`);
|
|
109
|
+
}
|
|
100
110
|
this.process.stdin.write(msg);
|
|
101
111
|
}
|
|
102
112
|
parseOutput(data) {
|
package/dist/claude/session.js
CHANGED
|
@@ -1,4 +1,9 @@
|
|
|
1
1
|
import { ClaudeCli } from './cli.js';
|
|
2
|
+
import { readFileSync } from 'fs';
|
|
3
|
+
import { dirname, resolve } from 'path';
|
|
4
|
+
import { fileURLToPath } from 'url';
|
|
5
|
+
const __dirname = dirname(fileURLToPath(import.meta.url));
|
|
6
|
+
const pkg = JSON.parse(readFileSync(resolve(__dirname, '..', '..', 'package.json'), 'utf-8'));
|
|
2
7
|
const REACTION_EMOJIS = ['one', 'two', 'three', 'four'];
|
|
3
8
|
const EMOJI_TO_INDEX = {
|
|
4
9
|
'one': 0, '1️⃣': 0,
|
|
@@ -78,7 +83,24 @@ export class SessionManager {
|
|
|
78
83
|
return;
|
|
79
84
|
}
|
|
80
85
|
// Post session start message
|
|
81
|
-
const
|
|
86
|
+
const shortDir = this.workingDir.replace(process.env.HOME || '', '~');
|
|
87
|
+
const sessionNum = this.sessions.size + 1;
|
|
88
|
+
const permMode = this.skipPermissions ? '⚡ Auto' : '🔐 Interactive';
|
|
89
|
+
const promptPreview = options.prompt.length > 60
|
|
90
|
+
? options.prompt.substring(0, 60) + '…'
|
|
91
|
+
: options.prompt;
|
|
92
|
+
const msg = [
|
|
93
|
+
`### 🤖 Claude Code \`v${pkg.version}\``,
|
|
94
|
+
``,
|
|
95
|
+
`| | |`,
|
|
96
|
+
`|:--|:--|`,
|
|
97
|
+
`| 📂 **Directory** | \`${shortDir}\` |`,
|
|
98
|
+
`| 👤 **Started by** | @${username} |`,
|
|
99
|
+
`| 🔢 **Session** | #${sessionNum} of ${MAX_SESSIONS} max |`,
|
|
100
|
+
`| ${permMode.split(' ')[0]} **Permissions** | ${permMode.split(' ')[1]} |`,
|
|
101
|
+
``,
|
|
102
|
+
`> ${promptPreview}`,
|
|
103
|
+
].join('\n');
|
|
82
104
|
const post = await this.mattermost.createPost(msg, replyToPostId);
|
|
83
105
|
const actualThreadId = replyToPostId || post.id;
|
|
84
106
|
// Create Claude CLI with options
|
|
@@ -107,7 +129,10 @@ export class SessionManager {
|
|
|
107
129
|
};
|
|
108
130
|
// Register session
|
|
109
131
|
this.sessions.set(actualThreadId, session);
|
|
110
|
-
|
|
132
|
+
const shortId = actualThreadId.substring(0, 8);
|
|
133
|
+
console.log(` ▶ Session #${this.sessions.size} started (${shortId}…) by @${username}`);
|
|
134
|
+
// Start typing indicator immediately so user sees activity
|
|
135
|
+
this.startTyping(session);
|
|
111
136
|
// Bind event handlers with closure over threadId
|
|
112
137
|
claude.on('event', (e) => this.handleEvent(actualThreadId, e));
|
|
113
138
|
claude.on('exit', (code) => this.handleExit(actualThreadId, code));
|
|
@@ -115,14 +140,14 @@ export class SessionManager {
|
|
|
115
140
|
claude.start();
|
|
116
141
|
}
|
|
117
142
|
catch (err) {
|
|
118
|
-
console.error('
|
|
143
|
+
console.error(' ❌ Failed to start Claude:', err);
|
|
144
|
+
this.stopTyping(session);
|
|
119
145
|
await this.mattermost.createPost(`❌ ${err}`, actualThreadId);
|
|
120
146
|
this.sessions.delete(actualThreadId);
|
|
121
147
|
return;
|
|
122
148
|
}
|
|
123
|
-
// Send the message
|
|
149
|
+
// Send the message to Claude
|
|
124
150
|
claude.sendMessage(options.prompt);
|
|
125
|
-
this.startTyping(session);
|
|
126
151
|
}
|
|
127
152
|
handleEvent(threadId, event) {
|
|
128
153
|
const session = this.sessions.get(threadId);
|
|
@@ -642,7 +667,8 @@ export class SessionManager {
|
|
|
642
667
|
this.postIndex.delete(postId);
|
|
643
668
|
}
|
|
644
669
|
}
|
|
645
|
-
|
|
670
|
+
const shortId = threadId.substring(0, 8);
|
|
671
|
+
console.log(` ■ Session ended (${shortId}…) — ${this.sessions.size} active`);
|
|
646
672
|
}
|
|
647
673
|
// ---------------------------------------------------------------------------
|
|
648
674
|
// Public Session API
|
|
@@ -679,14 +705,15 @@ export class SessionManager {
|
|
|
679
705
|
this.postIndex.delete(postId);
|
|
680
706
|
}
|
|
681
707
|
}
|
|
682
|
-
|
|
708
|
+
const shortId = threadId.substring(0, 8);
|
|
709
|
+
console.log(` ✖ Session killed (${shortId}…) — ${this.sessions.size} active`);
|
|
683
710
|
}
|
|
684
711
|
/** Kill all active sessions (for graceful shutdown) */
|
|
685
712
|
killAllSessions() {
|
|
686
|
-
|
|
713
|
+
const count = this.sessions.size;
|
|
714
|
+
for (const [, session] of this.sessions.entries()) {
|
|
687
715
|
this.stopTyping(session);
|
|
688
716
|
session.claude.kill();
|
|
689
|
-
console.log(`[Sessions] Killed session for thread ${threadId}`);
|
|
690
717
|
}
|
|
691
718
|
this.sessions.clear();
|
|
692
719
|
this.postIndex.clear();
|
|
@@ -694,7 +721,9 @@ export class SessionManager {
|
|
|
694
721
|
clearInterval(this.cleanupTimer);
|
|
695
722
|
this.cleanupTimer = null;
|
|
696
723
|
}
|
|
697
|
-
|
|
724
|
+
if (count > 0) {
|
|
725
|
+
console.log(` ✖ Killed ${count} session${count === 1 ? '' : 's'}`);
|
|
726
|
+
}
|
|
698
727
|
}
|
|
699
728
|
/** Cleanup idle sessions that have exceeded timeout */
|
|
700
729
|
cleanupIdleSessions() {
|
|
@@ -702,8 +731,10 @@ export class SessionManager {
|
|
|
702
731
|
for (const [threadId, session] of this.sessions.entries()) {
|
|
703
732
|
const idleTime = now - session.lastActivityAt.getTime();
|
|
704
733
|
if (idleTime > SESSION_TIMEOUT_MS) {
|
|
705
|
-
|
|
706
|
-
|
|
734
|
+
const mins = Math.round(idleTime / 60000);
|
|
735
|
+
const shortId = threadId.substring(0, 8);
|
|
736
|
+
console.log(` ⏰ Session (${shortId}…) timed out after ${mins}m idle`);
|
|
737
|
+
this.mattermost.createPost(`⏰ **Session timed out** — no activity for ${mins} minutes`, session.threadId).catch(() => { });
|
|
707
738
|
this.killSession(threadId);
|
|
708
739
|
}
|
|
709
740
|
}
|
package/dist/config.js
CHANGED
|
@@ -15,7 +15,9 @@ function loadEnv() {
|
|
|
15
15
|
];
|
|
16
16
|
for (const envPath of envPaths) {
|
|
17
17
|
if (existsSync(envPath)) {
|
|
18
|
-
|
|
18
|
+
if (process.env.DEBUG === '1' || process.argv.includes('--debug')) {
|
|
19
|
+
console.log(` [config] Loading from: ${envPath}`);
|
|
20
|
+
}
|
|
19
21
|
config({ path: envPath });
|
|
20
22
|
break;
|
|
21
23
|
}
|
package/dist/index.js
CHANGED
|
@@ -7,6 +7,9 @@ import { dirname, resolve } from 'path';
|
|
|
7
7
|
import { fileURLToPath } from 'url';
|
|
8
8
|
const __dirname = dirname(fileURLToPath(import.meta.url));
|
|
9
9
|
const pkg = JSON.parse(readFileSync(resolve(__dirname, '..', 'package.json'), 'utf-8'));
|
|
10
|
+
const dim = (s) => `\x1b[2m${s}\x1b[0m`;
|
|
11
|
+
const bold = (s) => `\x1b[1m${s}\x1b[0m`;
|
|
12
|
+
const cyan = (s) => `\x1b[36m${s}\x1b[0m`;
|
|
10
13
|
async function main() {
|
|
11
14
|
if (process.argv.includes('--version') || process.argv.includes('-v')) {
|
|
12
15
|
console.log(pkg.version);
|
|
@@ -20,17 +23,22 @@ Usage: cd /your/project && mm-claude`);
|
|
|
20
23
|
}
|
|
21
24
|
const workingDir = process.cwd();
|
|
22
25
|
const config = loadConfig();
|
|
23
|
-
|
|
24
|
-
console.log(
|
|
25
|
-
console.log(
|
|
26
|
-
|
|
27
|
-
|
|
26
|
+
// Nice startup banner
|
|
27
|
+
console.log('');
|
|
28
|
+
console.log(bold(` 🤖 mm-claude v${pkg.version}`));
|
|
29
|
+
console.log(dim(' ─────────────────────────────────'));
|
|
30
|
+
console.log(` 📂 ${cyan(workingDir)}`);
|
|
31
|
+
console.log(` 💬 ${cyan('@' + config.mattermost.botName)}`);
|
|
32
|
+
console.log(` 🌐 ${dim(config.mattermost.url)}`);
|
|
28
33
|
if (config.skipPermissions) {
|
|
29
|
-
console.log(
|
|
34
|
+
console.log(` ⚠️ ${dim('Permissions disabled')}`);
|
|
30
35
|
}
|
|
31
36
|
else {
|
|
32
|
-
console.log(
|
|
37
|
+
console.log(` 🔐 ${dim('Interactive permissions')}`);
|
|
33
38
|
}
|
|
39
|
+
console.log('');
|
|
40
|
+
const mattermost = new MattermostClient(config);
|
|
41
|
+
const session = new SessionManager(mattermost, workingDir, config.skipPermissions);
|
|
34
42
|
mattermost.on('message', async (post, user) => {
|
|
35
43
|
const username = user?.username || 'unknown';
|
|
36
44
|
const message = post.message;
|
|
@@ -60,12 +68,14 @@ Usage: cd /your/project && mm-claude`);
|
|
|
60
68
|
}
|
|
61
69
|
await session.startSession({ prompt }, username, threadRoot);
|
|
62
70
|
});
|
|
63
|
-
mattermost.on('connected', () =>
|
|
64
|
-
mattermost.on('error', (e) => console.error('❌', e));
|
|
71
|
+
mattermost.on('connected', () => { });
|
|
72
|
+
mattermost.on('error', (e) => console.error(' ❌ Error:', e));
|
|
65
73
|
await mattermost.connect();
|
|
66
|
-
console.log(
|
|
74
|
+
console.log(` ✅ ${bold('Ready!')} Waiting for @${config.mattermost.botName} mentions...`);
|
|
75
|
+
console.log('');
|
|
67
76
|
const shutdown = () => {
|
|
68
|
-
console.log('
|
|
77
|
+
console.log('');
|
|
78
|
+
console.log(` 👋 ${dim('Shutting down...')}`);
|
|
69
79
|
session.killAllSessions();
|
|
70
80
|
mattermost.disconnect();
|
|
71
81
|
process.exit(0);
|
|
@@ -16,7 +16,9 @@ export declare class MattermostClient extends EventEmitter {
|
|
|
16
16
|
private reconnectDelay;
|
|
17
17
|
private userCache;
|
|
18
18
|
private botUserId;
|
|
19
|
+
private debug;
|
|
19
20
|
constructor(config: Config);
|
|
21
|
+
private log;
|
|
20
22
|
private api;
|
|
21
23
|
getBotUser(): Promise<MattermostUser>;
|
|
22
24
|
getUser(userId: string): Promise<MattermostUser | null>;
|
|
@@ -8,10 +8,15 @@ export class MattermostClient extends EventEmitter {
|
|
|
8
8
|
reconnectDelay = 1000;
|
|
9
9
|
userCache = new Map();
|
|
10
10
|
botUserId = null;
|
|
11
|
+
debug = process.env.DEBUG === '1' || process.argv.includes('--debug');
|
|
11
12
|
constructor(config) {
|
|
12
13
|
super();
|
|
13
14
|
this.config = config;
|
|
14
15
|
}
|
|
16
|
+
log(msg) {
|
|
17
|
+
if (this.debug)
|
|
18
|
+
console.log(` [ws] ${msg}`);
|
|
19
|
+
}
|
|
15
20
|
// REST API helper
|
|
16
21
|
async api(method, path, body) {
|
|
17
22
|
const url = `${this.config.mattermost.url}/api/v4${path}`;
|
|
@@ -78,14 +83,14 @@ export class MattermostClient extends EventEmitter {
|
|
|
78
83
|
async connect() {
|
|
79
84
|
// Get bot user first
|
|
80
85
|
await this.getBotUser();
|
|
81
|
-
|
|
86
|
+
this.log(`Bot user ID: ${this.botUserId}`);
|
|
82
87
|
const wsUrl = this.config.mattermost.url
|
|
83
88
|
.replace(/^http/, 'ws')
|
|
84
89
|
.concat('/api/v4/websocket');
|
|
85
90
|
return new Promise((resolve, reject) => {
|
|
86
91
|
this.ws = new WebSocket(wsUrl);
|
|
87
92
|
this.ws.on('open', () => {
|
|
88
|
-
|
|
93
|
+
this.log('WebSocket connected');
|
|
89
94
|
// Authenticate
|
|
90
95
|
this.ws.send(JSON.stringify({
|
|
91
96
|
seq: 1,
|
|
@@ -105,16 +110,16 @@ export class MattermostClient extends EventEmitter {
|
|
|
105
110
|
}
|
|
106
111
|
}
|
|
107
112
|
catch (err) {
|
|
108
|
-
|
|
113
|
+
this.log(`Failed to parse message: ${err}`);
|
|
109
114
|
}
|
|
110
115
|
});
|
|
111
116
|
this.ws.on('close', () => {
|
|
112
|
-
|
|
117
|
+
this.log('WebSocket disconnected');
|
|
113
118
|
this.emit('disconnected');
|
|
114
119
|
this.scheduleReconnect();
|
|
115
120
|
});
|
|
116
121
|
this.ws.on('error', (err) => {
|
|
117
|
-
|
|
122
|
+
this.log(`WebSocket error: ${err}`);
|
|
118
123
|
this.emit('error', err);
|
|
119
124
|
reject(err);
|
|
120
125
|
});
|
|
@@ -140,7 +145,7 @@ export class MattermostClient extends EventEmitter {
|
|
|
140
145
|
});
|
|
141
146
|
}
|
|
142
147
|
catch (err) {
|
|
143
|
-
|
|
148
|
+
this.log(`Failed to parse post: ${err}`);
|
|
144
149
|
}
|
|
145
150
|
return;
|
|
146
151
|
}
|
|
@@ -160,21 +165,21 @@ export class MattermostClient extends EventEmitter {
|
|
|
160
165
|
});
|
|
161
166
|
}
|
|
162
167
|
catch (err) {
|
|
163
|
-
|
|
168
|
+
this.log(`Failed to parse reaction: ${err}`);
|
|
164
169
|
}
|
|
165
170
|
}
|
|
166
171
|
}
|
|
167
172
|
scheduleReconnect() {
|
|
168
173
|
if (this.reconnectAttempts >= this.maxReconnectAttempts) {
|
|
169
|
-
console.error('
|
|
174
|
+
console.error(' ⚠️ Max reconnection attempts reached');
|
|
170
175
|
return;
|
|
171
176
|
}
|
|
172
177
|
this.reconnectAttempts++;
|
|
173
178
|
const delay = this.reconnectDelay * Math.pow(2, this.reconnectAttempts - 1);
|
|
174
|
-
console.log(`
|
|
179
|
+
console.log(` 🔄 Reconnecting... (attempt ${this.reconnectAttempts})`);
|
|
175
180
|
setTimeout(() => {
|
|
176
181
|
this.connect().catch((err) => {
|
|
177
|
-
console.error(
|
|
182
|
+
console.error(` ❌ Reconnection failed: ${err}`);
|
|
178
183
|
});
|
|
179
184
|
}, delay);
|
|
180
185
|
}
|