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.
Files changed (46) hide show
  1. package/CHANGELOG.md +473 -0
  2. package/LICENSE +21 -0
  3. package/README.md +303 -0
  4. package/dist/changelog.d.ts +20 -0
  5. package/dist/changelog.js +134 -0
  6. package/dist/claude/cli.d.ts +42 -0
  7. package/dist/claude/cli.js +173 -0
  8. package/dist/claude/session.d.ts +256 -0
  9. package/dist/claude/session.js +1964 -0
  10. package/dist/config.d.ts +27 -0
  11. package/dist/config.js +94 -0
  12. package/dist/git/worktree.d.ts +50 -0
  13. package/dist/git/worktree.js +228 -0
  14. package/dist/index.d.ts +2 -0
  15. package/dist/index.js +371 -0
  16. package/dist/logo.d.ts +31 -0
  17. package/dist/logo.js +57 -0
  18. package/dist/mattermost/api.d.ts +85 -0
  19. package/dist/mattermost/api.js +124 -0
  20. package/dist/mattermost/api.test.d.ts +1 -0
  21. package/dist/mattermost/api.test.js +319 -0
  22. package/dist/mattermost/client.d.ts +56 -0
  23. package/dist/mattermost/client.js +321 -0
  24. package/dist/mattermost/emoji.d.ts +43 -0
  25. package/dist/mattermost/emoji.js +65 -0
  26. package/dist/mattermost/emoji.test.d.ts +1 -0
  27. package/dist/mattermost/emoji.test.js +131 -0
  28. package/dist/mattermost/types.d.ts +71 -0
  29. package/dist/mattermost/types.js +1 -0
  30. package/dist/mcp/permission-server.d.ts +2 -0
  31. package/dist/mcp/permission-server.js +201 -0
  32. package/dist/onboarding.d.ts +1 -0
  33. package/dist/onboarding.js +116 -0
  34. package/dist/persistence/session-store.d.ts +65 -0
  35. package/dist/persistence/session-store.js +127 -0
  36. package/dist/update-notifier.d.ts +3 -0
  37. package/dist/update-notifier.js +31 -0
  38. package/dist/utils/logger.d.ts +34 -0
  39. package/dist/utils/logger.js +42 -0
  40. package/dist/utils/logger.test.d.ts +1 -0
  41. package/dist/utils/logger.test.js +121 -0
  42. package/dist/utils/tool-formatter.d.ts +56 -0
  43. package/dist/utils/tool-formatter.js +247 -0
  44. package/dist/utils/tool-formatter.test.d.ts +1 -0
  45. package/dist/utils/tool-formatter.test.js +357 -0
  46. package/package.json +85 -0
package/README.md ADDED
@@ -0,0 +1,303 @@
1
+ # Mattermost Claude Code Bridge
2
+
3
+ [![npm version](https://img.shields.io/npm/v/claude-threads.svg)](https://www.npmjs.com/package/claude-threads)
4
+ [![License: MIT](https://img.shields.io/badge/License-MIT-yellow.svg)](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
+ }