claude-threads 0.13.0 → 0.14.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 +32 -0
- package/README.md +78 -28
- package/dist/claude/cli.d.ts +8 -0
- package/dist/claude/cli.js +16 -8
- package/dist/config/migration.d.ts +45 -0
- package/dist/config/migration.js +35 -0
- package/dist/config.d.ts +12 -18
- package/dist/config.js +7 -94
- package/dist/git/worktree.d.ts +0 -4
- package/dist/git/worktree.js +1 -1
- package/dist/index.js +31 -13
- package/dist/logo.d.ts +3 -20
- package/dist/logo.js +7 -23
- package/dist/mcp/permission-server.js +61 -112
- package/dist/onboarding.js +262 -137
- package/dist/persistence/session-store.d.ts +8 -2
- package/dist/persistence/session-store.js +41 -16
- package/dist/platform/client.d.ts +140 -0
- package/dist/platform/formatter.d.ts +74 -0
- package/dist/platform/index.d.ts +11 -0
- package/dist/platform/index.js +8 -0
- package/dist/platform/mattermost/client.d.ts +70 -0
- package/dist/{mattermost → platform/mattermost}/client.js +117 -34
- package/dist/platform/mattermost/formatter.d.ts +20 -0
- package/dist/platform/mattermost/formatter.js +46 -0
- package/dist/platform/mattermost/permission-api.d.ts +10 -0
- package/dist/platform/mattermost/permission-api.js +139 -0
- package/dist/platform/mattermost/types.js +1 -0
- package/dist/platform/permission-api-factory.d.ts +11 -0
- package/dist/platform/permission-api-factory.js +21 -0
- package/dist/platform/permission-api.d.ts +67 -0
- package/dist/platform/permission-api.js +8 -0
- package/dist/platform/types.d.ts +70 -0
- package/dist/platform/types.js +7 -0
- package/dist/session/commands.d.ts +52 -0
- package/dist/session/commands.js +323 -0
- package/dist/session/events.d.ts +25 -0
- package/dist/session/events.js +368 -0
- package/dist/session/index.d.ts +7 -0
- package/dist/session/index.js +6 -0
- package/dist/session/lifecycle.d.ts +70 -0
- package/dist/session/lifecycle.js +456 -0
- package/dist/session/manager.d.ts +96 -0
- package/dist/session/manager.js +537 -0
- package/dist/session/reactions.d.ts +25 -0
- package/dist/session/reactions.js +151 -0
- package/dist/session/streaming.d.ts +47 -0
- package/dist/session/streaming.js +152 -0
- package/dist/session/types.d.ts +78 -0
- package/dist/session/types.js +9 -0
- package/dist/session/worktree.d.ts +56 -0
- package/dist/session/worktree.js +339 -0
- package/dist/update-notifier.js +10 -0
- package/dist/{mattermost → utils}/emoji.d.ts +3 -3
- package/dist/{mattermost → utils}/emoji.js +3 -3
- package/dist/utils/emoji.test.d.ts +1 -0
- package/dist/utils/tool-formatter.d.ts +10 -13
- package/dist/utils/tool-formatter.js +48 -43
- package/dist/utils/tool-formatter.test.js +67 -52
- package/package.json +4 -3
- package/dist/claude/session.d.ts +0 -256
- package/dist/claude/session.js +0 -1964
- package/dist/mattermost/client.d.ts +0 -56
- /package/dist/{mattermost/emoji.test.d.ts → platform/client.js} +0 -0
- /package/dist/{mattermost/types.js → platform/formatter.js} +0 -0
- /package/dist/{mattermost → platform/mattermost}/types.d.ts +0 -0
- /package/dist/{mattermost → utils}/emoji.test.js +0 -0
|
@@ -1,9 +1,12 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
2
|
/**
|
|
3
|
-
* MCP Permission Server
|
|
3
|
+
* MCP Permission Server
|
|
4
4
|
*
|
|
5
5
|
* This server handles Claude Code's permission prompts by forwarding them to
|
|
6
|
-
*
|
|
6
|
+
* the chat platform for user approval via emoji reactions.
|
|
7
|
+
*
|
|
8
|
+
* Platform-agnostic design: Uses PermissionApi interface with platform-specific
|
|
9
|
+
* implementations selected based on PLATFORM_TYPE environment variable.
|
|
7
10
|
*
|
|
8
11
|
* It is spawned by Claude Code when using --permission-prompt-tool and
|
|
9
12
|
* communicates via stdio (MCP protocol).
|
|
@@ -14,124 +17,54 @@
|
|
|
14
17
|
* - 👎 (-1) Deny this tool use
|
|
15
18
|
*
|
|
16
19
|
* Environment variables (passed by claude-threads):
|
|
17
|
-
* -
|
|
18
|
-
* -
|
|
19
|
-
* -
|
|
20
|
-
* -
|
|
20
|
+
* - PLATFORM_TYPE: Platform type ('mattermost' or 'slack')
|
|
21
|
+
* - PLATFORM_URL: Platform server URL
|
|
22
|
+
* - PLATFORM_TOKEN: Bot access token
|
|
23
|
+
* - PLATFORM_CHANNEL_ID: Channel to post permission requests
|
|
24
|
+
* - PLATFORM_THREAD_ID: Thread ID for the current session
|
|
21
25
|
* - ALLOWED_USERS: Comma-separated list of authorized usernames
|
|
22
26
|
* - DEBUG: Set to '1' for debug logging
|
|
23
27
|
*/
|
|
24
28
|
import { McpServer } from '@modelcontextprotocol/sdk/server/mcp.js';
|
|
25
29
|
import { StdioServerTransport } from '@modelcontextprotocol/sdk/server/stdio.js';
|
|
26
30
|
import { z } from 'zod';
|
|
27
|
-
import
|
|
28
|
-
import { isApprovalEmoji, isAllowAllEmoji, APPROVAL_EMOJIS, ALLOW_ALL_EMOJIS, DENIAL_EMOJIS } from '../mattermost/emoji.js';
|
|
31
|
+
import { isApprovalEmoji, isAllowAllEmoji, APPROVAL_EMOJIS, ALLOW_ALL_EMOJIS, DENIAL_EMOJIS } from '../utils/emoji.js';
|
|
29
32
|
import { formatToolForPermission } from '../utils/tool-formatter.js';
|
|
30
33
|
import { mcpLogger } from '../utils/logger.js';
|
|
31
|
-
import {
|
|
34
|
+
import { createPermissionApi } from '../platform/permission-api-factory.js';
|
|
32
35
|
// =============================================================================
|
|
33
36
|
// Configuration
|
|
34
37
|
// =============================================================================
|
|
35
|
-
const
|
|
36
|
-
const
|
|
37
|
-
const
|
|
38
|
-
const
|
|
38
|
+
const PLATFORM_TYPE = process.env.PLATFORM_TYPE || '';
|
|
39
|
+
const PLATFORM_URL = process.env.PLATFORM_URL || '';
|
|
40
|
+
const PLATFORM_TOKEN = process.env.PLATFORM_TOKEN || '';
|
|
41
|
+
const PLATFORM_CHANNEL_ID = process.env.PLATFORM_CHANNEL_ID || '';
|
|
42
|
+
const PLATFORM_THREAD_ID = process.env.PLATFORM_THREAD_ID || '';
|
|
39
43
|
const ALLOWED_USERS = (process.env.ALLOWED_USERS || '')
|
|
40
44
|
.split(',')
|
|
41
45
|
.map(u => u.trim())
|
|
42
46
|
.filter(u => u.length > 0);
|
|
43
47
|
const PERMISSION_TIMEOUT_MS = 120000; // 2 minutes
|
|
44
|
-
//
|
|
48
|
+
// =============================================================================
|
|
49
|
+
// Permission API Instance
|
|
50
|
+
// =============================================================================
|
|
45
51
|
const apiConfig = {
|
|
46
|
-
url:
|
|
47
|
-
token:
|
|
52
|
+
url: PLATFORM_URL,
|
|
53
|
+
token: PLATFORM_TOKEN,
|
|
54
|
+
channelId: PLATFORM_CHANNEL_ID,
|
|
55
|
+
threadId: PLATFORM_THREAD_ID || undefined,
|
|
56
|
+
allowedUsers: ALLOWED_USERS,
|
|
57
|
+
debug: process.env.DEBUG === '1',
|
|
48
58
|
};
|
|
59
|
+
let permissionApi = null;
|
|
60
|
+
function getApi() {
|
|
61
|
+
if (!permissionApi) {
|
|
62
|
+
permissionApi = createPermissionApi(PLATFORM_TYPE, apiConfig);
|
|
63
|
+
}
|
|
64
|
+
return permissionApi;
|
|
65
|
+
}
|
|
49
66
|
// Session state
|
|
50
67
|
let allowAllSession = false;
|
|
51
|
-
let botUserId = null;
|
|
52
|
-
// =============================================================================
|
|
53
|
-
// Mattermost API Helpers (using shared API layer)
|
|
54
|
-
// =============================================================================
|
|
55
|
-
async function getBotUserId() {
|
|
56
|
-
if (botUserId)
|
|
57
|
-
return botUserId;
|
|
58
|
-
const me = await getMe(apiConfig);
|
|
59
|
-
botUserId = me.id;
|
|
60
|
-
return botUserId;
|
|
61
|
-
}
|
|
62
|
-
async function getUserById(userId) {
|
|
63
|
-
const user = await getUser(apiConfig, userId);
|
|
64
|
-
return user?.username || null;
|
|
65
|
-
}
|
|
66
|
-
function checkUserAllowed(username) {
|
|
67
|
-
return isUserAllowed(username, ALLOWED_USERS);
|
|
68
|
-
}
|
|
69
|
-
// =============================================================================
|
|
70
|
-
// Reaction Handling
|
|
71
|
-
// =============================================================================
|
|
72
|
-
function waitForReaction(postId) {
|
|
73
|
-
return new Promise((resolve, reject) => {
|
|
74
|
-
const wsUrl = MM_URL.replace(/^http/, 'ws') + '/api/v4/websocket';
|
|
75
|
-
mcpLogger.debug(`Connecting to WebSocket: ${wsUrl}`);
|
|
76
|
-
const ws = new WebSocket(wsUrl);
|
|
77
|
-
const timeout = setTimeout(() => {
|
|
78
|
-
mcpLogger.debug(`Timeout waiting for reaction on ${postId}`);
|
|
79
|
-
ws.close();
|
|
80
|
-
reject(new Error('Permission request timed out'));
|
|
81
|
-
}, PERMISSION_TIMEOUT_MS);
|
|
82
|
-
ws.on('open', () => {
|
|
83
|
-
mcpLogger.debug(`WebSocket connected, authenticating...`);
|
|
84
|
-
ws.send(JSON.stringify({
|
|
85
|
-
seq: 1,
|
|
86
|
-
action: 'authentication_challenge',
|
|
87
|
-
data: { token: MM_TOKEN },
|
|
88
|
-
}));
|
|
89
|
-
});
|
|
90
|
-
ws.on('message', async (data) => {
|
|
91
|
-
try {
|
|
92
|
-
const event = JSON.parse(data.toString());
|
|
93
|
-
mcpLogger.debug(`WS event: ${event.event || event.status || 'unknown'}`);
|
|
94
|
-
if (event.event === 'reaction_added') {
|
|
95
|
-
const reactionData = event.data;
|
|
96
|
-
// Mattermost sends reaction as JSON string
|
|
97
|
-
const reaction = typeof reactionData.reaction === 'string'
|
|
98
|
-
? JSON.parse(reactionData.reaction)
|
|
99
|
-
: reactionData.reaction;
|
|
100
|
-
mcpLogger.debug(`Reaction on post ${reaction?.post_id}, looking for ${postId}`);
|
|
101
|
-
if (reaction?.post_id === postId) {
|
|
102
|
-
const userId = reaction.user_id;
|
|
103
|
-
mcpLogger.debug(`Reaction from user ${userId}, emoji: ${reaction.emoji_name}`);
|
|
104
|
-
// Ignore bot's own reactions (from adding reaction options)
|
|
105
|
-
const myId = await getBotUserId();
|
|
106
|
-
if (userId === myId) {
|
|
107
|
-
mcpLogger.debug(`Ignoring bot's own reaction`);
|
|
108
|
-
return;
|
|
109
|
-
}
|
|
110
|
-
// Check if user is authorized
|
|
111
|
-
const username = await getUserById(userId);
|
|
112
|
-
mcpLogger.debug(`Username: ${username}, allowed: ${ALLOWED_USERS.join(',') || '(all)'}`);
|
|
113
|
-
if (!username || !checkUserAllowed(username)) {
|
|
114
|
-
mcpLogger.debug(`Ignoring unauthorized user: ${username || userId}`);
|
|
115
|
-
return;
|
|
116
|
-
}
|
|
117
|
-
mcpLogger.debug(`Accepting reaction ${reaction.emoji_name} from ${username}`);
|
|
118
|
-
clearTimeout(timeout);
|
|
119
|
-
ws.close();
|
|
120
|
-
resolve({ emoji: reaction.emoji_name, username });
|
|
121
|
-
}
|
|
122
|
-
}
|
|
123
|
-
}
|
|
124
|
-
catch (e) {
|
|
125
|
-
mcpLogger.debug(`Parse error: ${e}`);
|
|
126
|
-
}
|
|
127
|
-
});
|
|
128
|
-
ws.on('error', (err) => {
|
|
129
|
-
mcpLogger.debug(`WebSocket error: ${err}`);
|
|
130
|
-
clearTimeout(timeout);
|
|
131
|
-
reject(err);
|
|
132
|
-
});
|
|
133
|
-
});
|
|
134
|
-
}
|
|
135
68
|
async function handlePermission(toolName, toolInput) {
|
|
136
69
|
mcpLogger.debug(`handlePermission called for ${toolName}`);
|
|
137
70
|
// Auto-approve if "allow all" was selected earlier
|
|
@@ -139,32 +72,48 @@ async function handlePermission(toolName, toolInput) {
|
|
|
139
72
|
mcpLogger.debug(`Auto-allowing ${toolName} (allow all active)`);
|
|
140
73
|
return { behavior: 'allow', updatedInput: toolInput };
|
|
141
74
|
}
|
|
142
|
-
if (!
|
|
143
|
-
mcpLogger.error('Missing
|
|
75
|
+
if (!PLATFORM_URL || !PLATFORM_TOKEN || !PLATFORM_CHANNEL_ID) {
|
|
76
|
+
mcpLogger.error('Missing platform config');
|
|
144
77
|
return { behavior: 'deny', message: 'Permission service not configured' };
|
|
145
78
|
}
|
|
146
79
|
try {
|
|
147
|
-
|
|
148
|
-
const
|
|
80
|
+
const api = getApi();
|
|
81
|
+
const formatter = api.getFormatter();
|
|
82
|
+
// Post permission request with reaction options
|
|
83
|
+
const toolInfo = formatToolForPermission(toolName, toolInput, formatter);
|
|
149
84
|
const message = `⚠️ **Permission requested**\n\n${toolInfo}\n\n` +
|
|
150
85
|
`👍 Allow | ✅ Allow all | 👎 Deny`;
|
|
151
|
-
const
|
|
152
|
-
const post = await createInteractivePost(
|
|
86
|
+
const botUserId = await api.getBotUserId();
|
|
87
|
+
const post = await api.createInteractivePost(message, [APPROVAL_EMOJIS[0], ALLOW_ALL_EMOJIS[0], DENIAL_EMOJIS[0]], PLATFORM_THREAD_ID || undefined);
|
|
153
88
|
// Wait for user's reaction
|
|
154
|
-
const
|
|
89
|
+
const reaction = await api.waitForReaction(post.id, botUserId, PERMISSION_TIMEOUT_MS);
|
|
90
|
+
if (!reaction) {
|
|
91
|
+
await api.updatePost(post.id, `⏱️ **Timed out** - permission denied\n\n${toolInfo}`);
|
|
92
|
+
mcpLogger.info(`Timeout: ${toolName}`);
|
|
93
|
+
return { behavior: 'deny', message: 'Permission request timed out' };
|
|
94
|
+
}
|
|
95
|
+
// Get username and check if allowed
|
|
96
|
+
const username = await api.getUsername(reaction.userId);
|
|
97
|
+
if (!username || !api.isUserAllowed(username)) {
|
|
98
|
+
mcpLogger.debug(`Ignoring unauthorized user: ${username || reaction.userId}`);
|
|
99
|
+
// Keep waiting for authorized user - for now just deny
|
|
100
|
+
return { behavior: 'deny', message: 'Unauthorized user' };
|
|
101
|
+
}
|
|
102
|
+
const emoji = reaction.emojiName;
|
|
103
|
+
mcpLogger.debug(`Reaction ${emoji} from ${username}`);
|
|
155
104
|
if (isApprovalEmoji(emoji)) {
|
|
156
|
-
await updatePost(
|
|
105
|
+
await api.updatePost(post.id, `✅ **Allowed** by @${username}\n\n${toolInfo}`);
|
|
157
106
|
mcpLogger.info(`Allowed: ${toolName}`);
|
|
158
107
|
return { behavior: 'allow', updatedInput: toolInput };
|
|
159
108
|
}
|
|
160
109
|
else if (isAllowAllEmoji(emoji)) {
|
|
161
110
|
allowAllSession = true;
|
|
162
|
-
await updatePost(
|
|
111
|
+
await api.updatePost(post.id, `✅ **Allowed all** by @${username}\n\n${toolInfo}`);
|
|
163
112
|
mcpLogger.info(`Allowed all: ${toolName}`);
|
|
164
113
|
return { behavior: 'allow', updatedInput: toolInput };
|
|
165
114
|
}
|
|
166
115
|
else {
|
|
167
|
-
await updatePost(
|
|
116
|
+
await api.updatePost(post.id, `❌ **Denied** by @${username}\n\n${toolInfo}`);
|
|
168
117
|
mcpLogger.info(`Denied: ${toolName}`);
|
|
169
118
|
return { behavior: 'deny', message: 'User denied permission' };
|
|
170
119
|
}
|
|
@@ -182,7 +131,7 @@ async function main() {
|
|
|
182
131
|
name: 'claude-threads-permissions',
|
|
183
132
|
version: '1.0.0',
|
|
184
133
|
});
|
|
185
|
-
server.tool('permission_prompt', 'Handle permission requests via
|
|
134
|
+
server.tool('permission_prompt', 'Handle permission requests via chat platform reactions', {
|
|
186
135
|
tool_name: z.string().describe('Name of the tool requesting permission'),
|
|
187
136
|
input: z.record(z.string(), z.unknown()).describe('Tool input parameters'),
|
|
188
137
|
}, async ({ tool_name, input }) => {
|
|
@@ -193,7 +142,7 @@ async function main() {
|
|
|
193
142
|
});
|
|
194
143
|
const transport = new StdioServerTransport();
|
|
195
144
|
await server.connect(transport);
|
|
196
|
-
mcpLogger.info(
|
|
145
|
+
mcpLogger.info(`Permission server ready (platform: ${PLATFORM_TYPE})`);
|
|
197
146
|
}
|
|
198
147
|
main().catch((err) => {
|
|
199
148
|
mcpLogger.error(`Fatal: ${err}`);
|