claude-threads 0.12.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/CHANGELOG.md +473 -0
- package/LICENSE +21 -0
- package/README.md +303 -0
- package/dist/changelog.d.ts +20 -0
- package/dist/changelog.js +134 -0
- package/dist/claude/cli.d.ts +42 -0
- package/dist/claude/cli.js +173 -0
- package/dist/claude/session.d.ts +256 -0
- package/dist/claude/session.js +1964 -0
- package/dist/config.d.ts +27 -0
- package/dist/config.js +94 -0
- package/dist/git/worktree.d.ts +50 -0
- package/dist/git/worktree.js +228 -0
- package/dist/index.d.ts +2 -0
- package/dist/index.js +371 -0
- package/dist/logo.d.ts +31 -0
- package/dist/logo.js +57 -0
- package/dist/mattermost/api.d.ts +85 -0
- package/dist/mattermost/api.js +124 -0
- package/dist/mattermost/api.test.d.ts +1 -0
- package/dist/mattermost/api.test.js +319 -0
- package/dist/mattermost/client.d.ts +56 -0
- package/dist/mattermost/client.js +321 -0
- package/dist/mattermost/emoji.d.ts +43 -0
- package/dist/mattermost/emoji.js +65 -0
- package/dist/mattermost/emoji.test.d.ts +1 -0
- package/dist/mattermost/emoji.test.js +131 -0
- package/dist/mattermost/types.d.ts +71 -0
- package/dist/mattermost/types.js +1 -0
- package/dist/mcp/permission-server.d.ts +2 -0
- package/dist/mcp/permission-server.js +201 -0
- package/dist/onboarding.d.ts +1 -0
- package/dist/onboarding.js +116 -0
- package/dist/persistence/session-store.d.ts +65 -0
- package/dist/persistence/session-store.js +127 -0
- package/dist/update-notifier.d.ts +3 -0
- package/dist/update-notifier.js +31 -0
- package/dist/utils/logger.d.ts +34 -0
- package/dist/utils/logger.js +42 -0
- package/dist/utils/logger.test.d.ts +1 -0
- package/dist/utils/logger.test.js +121 -0
- package/dist/utils/tool-formatter.d.ts +56 -0
- package/dist/utils/tool-formatter.js +247 -0
- package/dist/utils/tool-formatter.test.d.ts +1 -0
- package/dist/utils/tool-formatter.test.js +357 -0
- package/package.json +85 -0
package/README.md
ADDED
|
@@ -0,0 +1,303 @@
|
|
|
1
|
+
# Mattermost Claude Code Bridge
|
|
2
|
+
|
|
3
|
+
[](https://www.npmjs.com/package/claude-threads)
|
|
4
|
+
[](https://opensource.org/licenses/MIT)
|
|
5
|
+
|
|
6
|
+
Share Claude Code sessions live in a Mattermost channel. Your colleagues can watch you work with Claude in real-time, collaborate on sessions, and even trigger their own sessions from Mattermost.
|
|
7
|
+
|
|
8
|
+
## Features
|
|
9
|
+
|
|
10
|
+
- **Real-time streaming** - Claude's responses stream live to Mattermost
|
|
11
|
+
- **Multiple concurrent sessions** - Each thread gets its own Claude session
|
|
12
|
+
- **Session collaboration** - Invite others to participate in your session
|
|
13
|
+
- **Interactive permissions** - Approve Claude's actions via emoji reactions
|
|
14
|
+
- **Plan approval** - Review and approve Claude's plans before execution
|
|
15
|
+
- **Task tracking** - Live todo list updates as Claude works
|
|
16
|
+
- **Code diffs** - See exactly what Claude is changing
|
|
17
|
+
|
|
18
|
+
## How it works
|
|
19
|
+
|
|
20
|
+
```mermaid
|
|
21
|
+
flowchart TB
|
|
22
|
+
subgraph local["Your Local Machine"]
|
|
23
|
+
cli["Claude Code CLI<br/>(subprocess)"]
|
|
24
|
+
mm["claude-threads<br/>(this service)"]
|
|
25
|
+
cli <-->|"stdio"| mm
|
|
26
|
+
end
|
|
27
|
+
|
|
28
|
+
subgraph server["Mattermost Server"]
|
|
29
|
+
bot["Bot Account<br/>@claude-code"]
|
|
30
|
+
channel["Channel<br/>#claude-sessions"]
|
|
31
|
+
bot <--> channel
|
|
32
|
+
end
|
|
33
|
+
|
|
34
|
+
mm -->|"WebSocket + REST API<br/>(outbound only)"| server
|
|
35
|
+
```
|
|
36
|
+
|
|
37
|
+
Runs entirely on your machine - only **outbound** connections to Mattermost. No port forwarding needed!
|
|
38
|
+
|
|
39
|
+
## Prerequisites
|
|
40
|
+
|
|
41
|
+
1. **Claude Code CLI** installed and authenticated (`claude --version`)
|
|
42
|
+
2. **Node.js 18+**
|
|
43
|
+
3. **Mattermost bot account** with a personal access token
|
|
44
|
+
|
|
45
|
+
## Quick Start
|
|
46
|
+
|
|
47
|
+
### 1. Install
|
|
48
|
+
|
|
49
|
+
```bash
|
|
50
|
+
npm install -g claude-threads
|
|
51
|
+
```
|
|
52
|
+
|
|
53
|
+
### 2. Run
|
|
54
|
+
|
|
55
|
+
```bash
|
|
56
|
+
cd /your/project
|
|
57
|
+
claude-threads
|
|
58
|
+
```
|
|
59
|
+
|
|
60
|
+
On first run, an interactive setup wizard guides you through configuration:
|
|
61
|
+
|
|
62
|
+
```
|
|
63
|
+
Welcome to claude-threads!
|
|
64
|
+
|
|
65
|
+
No configuration found. Let's set things up.
|
|
66
|
+
|
|
67
|
+
You'll need:
|
|
68
|
+
• A Mattermost bot account with a token
|
|
69
|
+
• A channel ID where the bot will listen
|
|
70
|
+
|
|
71
|
+
? Mattermost URL: https://your-mattermost.com
|
|
72
|
+
? Bot token: ********
|
|
73
|
+
? Channel ID: abc123def456
|
|
74
|
+
? Bot mention name: claude-code
|
|
75
|
+
? Allowed usernames: alice,bob
|
|
76
|
+
? Skip permission prompts? No
|
|
77
|
+
|
|
78
|
+
✓ Configuration saved!
|
|
79
|
+
~/.config/claude-threads/.env
|
|
80
|
+
|
|
81
|
+
Starting claude-threads...
|
|
82
|
+
```
|
|
83
|
+
|
|
84
|
+
### 3. Use
|
|
85
|
+
|
|
86
|
+
In Mattermost, mention the bot:
|
|
87
|
+
|
|
88
|
+
```
|
|
89
|
+
@claude-code help me fix the bug in src/auth.ts
|
|
90
|
+
```
|
|
91
|
+
|
|
92
|
+
## CLI Options
|
|
93
|
+
|
|
94
|
+
```bash
|
|
95
|
+
claude-threads [options]
|
|
96
|
+
|
|
97
|
+
Options:
|
|
98
|
+
--url <url> Mattermost server URL
|
|
99
|
+
--token <token> Bot token
|
|
100
|
+
--channel <id> Channel ID
|
|
101
|
+
--bot-name <name> Bot mention name (default: claude-code)
|
|
102
|
+
--allowed-users <list> Comma-separated allowed usernames
|
|
103
|
+
--skip-permissions Skip permission prompts (auto-approve)
|
|
104
|
+
--no-skip-permissions Enable permission prompts (override env)
|
|
105
|
+
--debug Enable debug logging
|
|
106
|
+
--version Show version
|
|
107
|
+
--help Show help
|
|
108
|
+
```
|
|
109
|
+
|
|
110
|
+
CLI options override environment variables.
|
|
111
|
+
|
|
112
|
+
## Session Commands
|
|
113
|
+
|
|
114
|
+
Type `!help` in any session thread to see available commands:
|
|
115
|
+
|
|
116
|
+
| Command | Description |
|
|
117
|
+
|:--------|:------------|
|
|
118
|
+
| `!help` | Show available commands |
|
|
119
|
+
| `!release-notes` | Show release notes for current version |
|
|
120
|
+
| `!cd <path>` | Change working directory (restarts Claude) |
|
|
121
|
+
| `!invite @user` | Invite a user to this session |
|
|
122
|
+
| `!kick @user` | Remove an invited user |
|
|
123
|
+
| `!permissions interactive` | Enable interactive permissions |
|
|
124
|
+
| `!stop` | Stop this session |
|
|
125
|
+
|
|
126
|
+
> **Note:** Commands use `!` prefix instead of `/` to avoid conflicts with Mattermost's slash commands.
|
|
127
|
+
|
|
128
|
+
## Session Collaboration
|
|
129
|
+
|
|
130
|
+
### Invite Users
|
|
131
|
+
|
|
132
|
+
Session owners can temporarily allow others to participate:
|
|
133
|
+
|
|
134
|
+
```
|
|
135
|
+
!invite @colleague
|
|
136
|
+
```
|
|
137
|
+
|
|
138
|
+
The colleague can now send messages in this session thread.
|
|
139
|
+
|
|
140
|
+
### Kick Users
|
|
141
|
+
|
|
142
|
+
Remove an invited user from the session:
|
|
143
|
+
|
|
144
|
+
```
|
|
145
|
+
!kick @colleague
|
|
146
|
+
```
|
|
147
|
+
|
|
148
|
+
### Message Approval
|
|
149
|
+
|
|
150
|
+
When an unauthorized user sends a message in a session thread, the owner sees an approval prompt:
|
|
151
|
+
|
|
152
|
+
```
|
|
153
|
+
🔒 @unauthorized-user wants to send a message:
|
|
154
|
+
> Can you also add error handling?
|
|
155
|
+
|
|
156
|
+
React 👍 to allow this message, ✅ to invite them to the session, 👎 to deny
|
|
157
|
+
```
|
|
158
|
+
|
|
159
|
+
### Side Conversations
|
|
160
|
+
|
|
161
|
+
Messages starting with `@someone-else` are ignored by the bot, allowing side conversations in the thread without triggering Claude.
|
|
162
|
+
|
|
163
|
+
### Downgrade Permissions
|
|
164
|
+
|
|
165
|
+
If the bot is running with `--skip-permissions` (auto mode), you can enable interactive permissions for a specific session:
|
|
166
|
+
|
|
167
|
+
```
|
|
168
|
+
!permissions interactive
|
|
169
|
+
```
|
|
170
|
+
|
|
171
|
+
This allows collaboration by requiring approval for Claude's actions. Note: you can only downgrade (auto → interactive), not upgrade - this ensures security.
|
|
172
|
+
|
|
173
|
+
## Interactive Features
|
|
174
|
+
|
|
175
|
+
### Permission Approval
|
|
176
|
+
|
|
177
|
+
When Claude wants to execute a tool (edit file, run command, etc.):
|
|
178
|
+
|
|
179
|
+
- **👍 Allow** - Approve this specific action
|
|
180
|
+
- **✅ Allow all** - Approve all future actions this session
|
|
181
|
+
- **👎 Deny** - Reject this action
|
|
182
|
+
|
|
183
|
+
To skip prompts: `claude-threads --skip-permissions` or set `SKIP_PERMISSIONS=true`
|
|
184
|
+
|
|
185
|
+
### Plan Mode
|
|
186
|
+
|
|
187
|
+
When Claude creates a plan and is ready to implement:
|
|
188
|
+
|
|
189
|
+
- **👍** Approve and start building
|
|
190
|
+
- **👎** Request changes
|
|
191
|
+
|
|
192
|
+
Once approved, subsequent plans auto-continue.
|
|
193
|
+
|
|
194
|
+
### Questions
|
|
195
|
+
|
|
196
|
+
When Claude asks questions with multiple choice options:
|
|
197
|
+
|
|
198
|
+
- React with 1️⃣ 2️⃣ 3️⃣ or 4️⃣ to answer
|
|
199
|
+
- Questions are asked one at a time
|
|
200
|
+
|
|
201
|
+
### Task List
|
|
202
|
+
|
|
203
|
+
Claude's todo list shows live in Mattermost:
|
|
204
|
+
|
|
205
|
+
- ⬜ Pending
|
|
206
|
+
- 🔄 In progress
|
|
207
|
+
- ✅ Completed
|
|
208
|
+
|
|
209
|
+
### Session Header
|
|
210
|
+
|
|
211
|
+
The session start message shows current status and updates when participants change:
|
|
212
|
+
|
|
213
|
+
```
|
|
214
|
+
🤖 claude-threads v0.5.1
|
|
215
|
+
|
|
216
|
+
| | |
|
|
217
|
+
|:--|:--|
|
|
218
|
+
| 📂 Directory | ~/project |
|
|
219
|
+
| 👤 Started by | @alice |
|
|
220
|
+
| 👥 Participants | @bob, @carol |
|
|
221
|
+
| 🔢 Session | #1 of 5 max |
|
|
222
|
+
| 🔐 Permissions | Interactive |
|
|
223
|
+
```
|
|
224
|
+
|
|
225
|
+
### Cancel Session
|
|
226
|
+
|
|
227
|
+
Stop a running session:
|
|
228
|
+
|
|
229
|
+
- Type `!stop` or `!cancel` in the thread
|
|
230
|
+
- React with ❌ or 🛑 to any message in the thread
|
|
231
|
+
|
|
232
|
+
## Access Control
|
|
233
|
+
|
|
234
|
+
Set `ALLOWED_USERS` to restrict who can use the bot:
|
|
235
|
+
|
|
236
|
+
```env
|
|
237
|
+
ALLOWED_USERS=alice,bob,carol
|
|
238
|
+
```
|
|
239
|
+
|
|
240
|
+
- Only listed users can start sessions
|
|
241
|
+
- Only listed users can approve permissions
|
|
242
|
+
- Session owners can `!invite` others temporarily
|
|
243
|
+
- Empty = anyone can use (be careful!)
|
|
244
|
+
|
|
245
|
+
## Environment Variables
|
|
246
|
+
|
|
247
|
+
| Variable | Description |
|
|
248
|
+
|----------|-------------|
|
|
249
|
+
| `MATTERMOST_URL` | Server URL |
|
|
250
|
+
| `MATTERMOST_TOKEN` | Bot token |
|
|
251
|
+
| `MATTERMOST_CHANNEL_ID` | Channel to listen in |
|
|
252
|
+
| `MATTERMOST_BOT_NAME` | Mention name (default: `claude-code`) |
|
|
253
|
+
| `ALLOWED_USERS` | Comma-separated usernames |
|
|
254
|
+
| `SKIP_PERMISSIONS` | `true` to auto-approve actions |
|
|
255
|
+
| `MAX_SESSIONS` | Max concurrent sessions (default: `5`) |
|
|
256
|
+
| `SESSION_TIMEOUT_MS` | Idle timeout in ms (default: `1800000` = 30 min) |
|
|
257
|
+
| `NO_UPDATE_NOTIFIER` | Set to `1` to disable update checks |
|
|
258
|
+
|
|
259
|
+
Config file locations (in priority order):
|
|
260
|
+
1. `./.env` (current directory)
|
|
261
|
+
2. `~/.config/claude-threads/.env`
|
|
262
|
+
3. `~/.claude-threads.env`
|
|
263
|
+
|
|
264
|
+
## Code Display
|
|
265
|
+
|
|
266
|
+
- **Edit**: Shows diff with `-` removed and `+` added lines
|
|
267
|
+
- **Write**: Shows preview of new file content
|
|
268
|
+
- **Bash**: Shows command being executed
|
|
269
|
+
- **Read**: Shows file path being read
|
|
270
|
+
- **MCP tools**: Shows tool name and server
|
|
271
|
+
|
|
272
|
+
## Auto-Updates
|
|
273
|
+
|
|
274
|
+
claude-threads checks for updates every 30 minutes and notifies you when a new version is available:
|
|
275
|
+
|
|
276
|
+
- **CLI**: Shows a notification box on startup
|
|
277
|
+
- **Mattermost**: Shows a warning in session headers
|
|
278
|
+
|
|
279
|
+
To update:
|
|
280
|
+
|
|
281
|
+
```bash
|
|
282
|
+
npm install -g claude-threads
|
|
283
|
+
```
|
|
284
|
+
|
|
285
|
+
To disable update checks, set `NO_UPDATE_NOTIFIER=1`.
|
|
286
|
+
|
|
287
|
+
## For Mattermost Admins
|
|
288
|
+
|
|
289
|
+
To set up a bot account:
|
|
290
|
+
|
|
291
|
+
1. Go to **Integrations > Bot Accounts > Add Bot Account**
|
|
292
|
+
2. Give it a username (e.g., `claude-code`) and display name
|
|
293
|
+
3. Create a **Personal Access Token** for the bot
|
|
294
|
+
4. Add the bot to the channel where it should listen
|
|
295
|
+
|
|
296
|
+
The bot needs permissions to:
|
|
297
|
+
- Post messages
|
|
298
|
+
- Add reactions
|
|
299
|
+
- Read channel messages
|
|
300
|
+
|
|
301
|
+
## License
|
|
302
|
+
|
|
303
|
+
MIT
|
|
@@ -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
|
+
}
|
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
import { EventEmitter } from 'events';
|
|
2
|
+
export interface ClaudeEvent {
|
|
3
|
+
type: string;
|
|
4
|
+
[key: string]: unknown;
|
|
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;
|
|
19
|
+
export interface ClaudeCliOptions {
|
|
20
|
+
workingDir: string;
|
|
21
|
+
threadId?: string;
|
|
22
|
+
skipPermissions?: boolean;
|
|
23
|
+
sessionId?: string;
|
|
24
|
+
resume?: boolean;
|
|
25
|
+
chrome?: boolean;
|
|
26
|
+
}
|
|
27
|
+
export declare class ClaudeCli extends EventEmitter {
|
|
28
|
+
private process;
|
|
29
|
+
private options;
|
|
30
|
+
private buffer;
|
|
31
|
+
debug: boolean;
|
|
32
|
+
constructor(options: ClaudeCliOptions);
|
|
33
|
+
start(): void;
|
|
34
|
+
sendMessage(content: string | ContentBlock[]): void;
|
|
35
|
+
sendToolResult(toolUseId: string, content: unknown): void;
|
|
36
|
+
private parseOutput;
|
|
37
|
+
isRunning(): boolean;
|
|
38
|
+
kill(): void;
|
|
39
|
+
/** Interrupt current processing (like Escape in CLI) - keeps process alive */
|
|
40
|
+
interrupt(): boolean;
|
|
41
|
+
private getMcpServerPath;
|
|
42
|
+
}
|
|
@@ -0,0 +1,173 @@
|
|
|
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
|
+
// Add session ID for persistence/resume support
|
|
24
|
+
if (this.options.sessionId) {
|
|
25
|
+
if (this.options.resume) {
|
|
26
|
+
args.push('--resume', this.options.sessionId);
|
|
27
|
+
}
|
|
28
|
+
else {
|
|
29
|
+
args.push('--session-id', this.options.sessionId);
|
|
30
|
+
}
|
|
31
|
+
}
|
|
32
|
+
// Either use skip permissions or the MCP-based permission system
|
|
33
|
+
if (this.options.skipPermissions) {
|
|
34
|
+
args.push('--dangerously-skip-permissions');
|
|
35
|
+
}
|
|
36
|
+
else {
|
|
37
|
+
// Configure the permission MCP server
|
|
38
|
+
const mcpServerPath = this.getMcpServerPath();
|
|
39
|
+
const mcpConfig = {
|
|
40
|
+
mcpServers: {
|
|
41
|
+
'claude-threads-permissions': {
|
|
42
|
+
type: 'stdio',
|
|
43
|
+
command: 'node',
|
|
44
|
+
args: [mcpServerPath],
|
|
45
|
+
env: {
|
|
46
|
+
MM_THREAD_ID: this.options.threadId || '',
|
|
47
|
+
MATTERMOST_URL: process.env.MATTERMOST_URL || '',
|
|
48
|
+
MATTERMOST_TOKEN: process.env.MATTERMOST_TOKEN || '',
|
|
49
|
+
MATTERMOST_CHANNEL_ID: process.env.MATTERMOST_CHANNEL_ID || '',
|
|
50
|
+
ALLOWED_USERS: process.env.ALLOWED_USERS || '',
|
|
51
|
+
DEBUG: this.debug ? '1' : '',
|
|
52
|
+
},
|
|
53
|
+
},
|
|
54
|
+
},
|
|
55
|
+
};
|
|
56
|
+
args.push('--mcp-config', JSON.stringify(mcpConfig));
|
|
57
|
+
args.push('--permission-prompt-tool', 'mcp__claude-threads-permissions__permission_prompt');
|
|
58
|
+
}
|
|
59
|
+
// Chrome integration
|
|
60
|
+
if (this.options.chrome) {
|
|
61
|
+
args.push('--chrome');
|
|
62
|
+
}
|
|
63
|
+
if (this.debug) {
|
|
64
|
+
console.log(` [claude] Starting: ${claudePath} ${args.slice(0, 5).join(' ')}...`);
|
|
65
|
+
}
|
|
66
|
+
this.process = spawn(claudePath, args, {
|
|
67
|
+
cwd: this.options.workingDir,
|
|
68
|
+
env: process.env,
|
|
69
|
+
stdio: ['pipe', 'pipe', 'pipe'],
|
|
70
|
+
});
|
|
71
|
+
this.process.stdout?.on('data', (chunk) => {
|
|
72
|
+
this.parseOutput(chunk.toString());
|
|
73
|
+
});
|
|
74
|
+
this.process.stderr?.on('data', (chunk) => {
|
|
75
|
+
if (this.debug) {
|
|
76
|
+
console.error(` [claude:err] ${chunk.toString().trim()}`);
|
|
77
|
+
}
|
|
78
|
+
});
|
|
79
|
+
this.process.on('error', (err) => {
|
|
80
|
+
console.error(' ❌ Claude error:', err);
|
|
81
|
+
this.emit('error', err);
|
|
82
|
+
});
|
|
83
|
+
this.process.on('exit', (code) => {
|
|
84
|
+
if (this.debug) {
|
|
85
|
+
console.log(` [claude] Exited ${code}`);
|
|
86
|
+
}
|
|
87
|
+
this.process = null;
|
|
88
|
+
this.buffer = '';
|
|
89
|
+
this.emit('exit', code);
|
|
90
|
+
});
|
|
91
|
+
}
|
|
92
|
+
// Send a user message via JSON stdin
|
|
93
|
+
// content can be a string or an array of content blocks (for images)
|
|
94
|
+
sendMessage(content) {
|
|
95
|
+
if (!this.process?.stdin)
|
|
96
|
+
throw new Error('Not running');
|
|
97
|
+
const msg = JSON.stringify({
|
|
98
|
+
type: 'user',
|
|
99
|
+
message: { role: 'user', content }
|
|
100
|
+
}) + '\n';
|
|
101
|
+
if (this.debug) {
|
|
102
|
+
const preview = typeof content === 'string'
|
|
103
|
+
? content.substring(0, 50)
|
|
104
|
+
: `[${content.length} blocks]`;
|
|
105
|
+
console.log(` [claude] Sending: ${preview}...`);
|
|
106
|
+
}
|
|
107
|
+
this.process.stdin.write(msg);
|
|
108
|
+
}
|
|
109
|
+
// Send a tool result response
|
|
110
|
+
sendToolResult(toolUseId, content) {
|
|
111
|
+
if (!this.process?.stdin)
|
|
112
|
+
throw new Error('Not running');
|
|
113
|
+
const msg = JSON.stringify({
|
|
114
|
+
type: 'user',
|
|
115
|
+
message: {
|
|
116
|
+
role: 'user',
|
|
117
|
+
content: [{
|
|
118
|
+
type: 'tool_result',
|
|
119
|
+
tool_use_id: toolUseId,
|
|
120
|
+
content: typeof content === 'string' ? content : JSON.stringify(content)
|
|
121
|
+
}]
|
|
122
|
+
}
|
|
123
|
+
}) + '\n';
|
|
124
|
+
if (this.debug) {
|
|
125
|
+
console.log(` [claude] Sending tool_result for ${toolUseId}`);
|
|
126
|
+
}
|
|
127
|
+
this.process.stdin.write(msg);
|
|
128
|
+
}
|
|
129
|
+
parseOutput(data) {
|
|
130
|
+
this.buffer += data;
|
|
131
|
+
const lines = this.buffer.split('\n');
|
|
132
|
+
this.buffer = lines.pop() || '';
|
|
133
|
+
for (const line of lines) {
|
|
134
|
+
const trimmed = line.trim();
|
|
135
|
+
if (!trimmed)
|
|
136
|
+
continue;
|
|
137
|
+
try {
|
|
138
|
+
const event = JSON.parse(trimmed);
|
|
139
|
+
if (this.debug) {
|
|
140
|
+
console.log(`[DEBUG] Event: ${event.type}`, JSON.stringify(event).substring(0, 200));
|
|
141
|
+
}
|
|
142
|
+
this.emit('event', event);
|
|
143
|
+
}
|
|
144
|
+
catch {
|
|
145
|
+
if (this.debug) {
|
|
146
|
+
console.log(`[DEBUG] Raw: ${trimmed.substring(0, 200)}`);
|
|
147
|
+
}
|
|
148
|
+
}
|
|
149
|
+
}
|
|
150
|
+
}
|
|
151
|
+
isRunning() {
|
|
152
|
+
return this.process !== null;
|
|
153
|
+
}
|
|
154
|
+
kill() {
|
|
155
|
+
this.process?.kill('SIGTERM');
|
|
156
|
+
this.process = null;
|
|
157
|
+
}
|
|
158
|
+
/** Interrupt current processing (like Escape in CLI) - keeps process alive */
|
|
159
|
+
interrupt() {
|
|
160
|
+
if (!this.process)
|
|
161
|
+
return false;
|
|
162
|
+
this.process.kill('SIGINT');
|
|
163
|
+
return true;
|
|
164
|
+
}
|
|
165
|
+
getMcpServerPath() {
|
|
166
|
+
// Get the path to the MCP permission server
|
|
167
|
+
// When running from source: src/mcp/permission-server.ts -> dist/mcp/permission-server.js
|
|
168
|
+
// When installed globally: the bin entry points to dist/mcp/permission-server.js
|
|
169
|
+
const __filename = fileURLToPath(import.meta.url);
|
|
170
|
+
const __dirname = dirname(__filename);
|
|
171
|
+
return resolve(__dirname, '..', 'mcp', 'permission-server.js');
|
|
172
|
+
}
|
|
173
|
+
}
|