claude-threads 0.13.0 → 0.14.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 (66) hide show
  1. package/CHANGELOG.md +32 -0
  2. package/README.md +78 -28
  3. package/dist/claude/cli.d.ts +8 -0
  4. package/dist/claude/cli.js +16 -8
  5. package/dist/config/migration.d.ts +45 -0
  6. package/dist/config/migration.js +35 -0
  7. package/dist/config.d.ts +12 -18
  8. package/dist/config.js +7 -94
  9. package/dist/git/worktree.d.ts +0 -4
  10. package/dist/git/worktree.js +1 -1
  11. package/dist/index.js +31 -13
  12. package/dist/logo.d.ts +3 -20
  13. package/dist/logo.js +7 -23
  14. package/dist/mcp/permission-server.js +61 -112
  15. package/dist/onboarding.js +262 -137
  16. package/dist/persistence/session-store.d.ts +8 -2
  17. package/dist/persistence/session-store.js +41 -16
  18. package/dist/platform/client.d.ts +140 -0
  19. package/dist/platform/formatter.d.ts +74 -0
  20. package/dist/platform/index.d.ts +11 -0
  21. package/dist/platform/index.js +8 -0
  22. package/dist/platform/mattermost/client.d.ts +70 -0
  23. package/dist/{mattermost → platform/mattermost}/client.js +117 -34
  24. package/dist/platform/mattermost/formatter.d.ts +20 -0
  25. package/dist/platform/mattermost/formatter.js +46 -0
  26. package/dist/platform/mattermost/permission-api.d.ts +10 -0
  27. package/dist/platform/mattermost/permission-api.js +139 -0
  28. package/dist/platform/mattermost/types.js +1 -0
  29. package/dist/platform/permission-api-factory.d.ts +11 -0
  30. package/dist/platform/permission-api-factory.js +21 -0
  31. package/dist/platform/permission-api.d.ts +67 -0
  32. package/dist/platform/permission-api.js +8 -0
  33. package/dist/platform/types.d.ts +70 -0
  34. package/dist/platform/types.js +7 -0
  35. package/dist/session/commands.d.ts +52 -0
  36. package/dist/session/commands.js +323 -0
  37. package/dist/session/events.d.ts +25 -0
  38. package/dist/session/events.js +368 -0
  39. package/dist/session/index.d.ts +7 -0
  40. package/dist/session/index.js +6 -0
  41. package/dist/session/lifecycle.d.ts +70 -0
  42. package/dist/session/lifecycle.js +456 -0
  43. package/dist/session/manager.d.ts +96 -0
  44. package/dist/session/manager.js +537 -0
  45. package/dist/session/reactions.d.ts +25 -0
  46. package/dist/session/reactions.js +151 -0
  47. package/dist/session/streaming.d.ts +47 -0
  48. package/dist/session/streaming.js +152 -0
  49. package/dist/session/types.d.ts +78 -0
  50. package/dist/session/types.js +9 -0
  51. package/dist/session/worktree.d.ts +56 -0
  52. package/dist/session/worktree.js +339 -0
  53. package/dist/{mattermost → utils}/emoji.d.ts +3 -3
  54. package/dist/{mattermost → utils}/emoji.js +3 -3
  55. package/dist/utils/emoji.test.d.ts +1 -0
  56. package/dist/utils/tool-formatter.d.ts +10 -13
  57. package/dist/utils/tool-formatter.js +48 -43
  58. package/dist/utils/tool-formatter.test.js +67 -52
  59. package/package.json +2 -3
  60. package/dist/claude/session.d.ts +0 -256
  61. package/dist/claude/session.js +0 -1964
  62. package/dist/mattermost/client.d.ts +0 -56
  63. /package/dist/{mattermost/emoji.test.d.ts → platform/client.js} +0 -0
  64. /package/dist/{mattermost/types.js → platform/formatter.js} +0 -0
  65. /package/dist/{mattermost → platform/mattermost}/types.d.ts +0 -0
  66. /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 for Mattermost
3
+ * MCP Permission Server
4
4
  *
5
5
  * This server handles Claude Code's permission prompts by forwarding them to
6
- * Mattermost for user approval via emoji reactions.
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
- * - MATTERMOST_URL: Mattermost server URL
18
- * - MATTERMOST_TOKEN: Bot access token
19
- * - MATTERMOST_CHANNEL_ID: Channel to post permission requests
20
- * - MM_THREAD_ID: Thread ID for the current session
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 WebSocket from 'ws';
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 { getMe, getUser, createInteractivePost, updatePost, isUserAllowed, } from '../mattermost/api.js';
34
+ import { createPermissionApi } from '../platform/permission-api-factory.js';
32
35
  // =============================================================================
33
36
  // Configuration
34
37
  // =============================================================================
35
- const MM_URL = process.env.MATTERMOST_URL || '';
36
- const MM_TOKEN = process.env.MATTERMOST_TOKEN || '';
37
- const MM_CHANNEL_ID = process.env.MATTERMOST_CHANNEL_ID || '';
38
- const MM_THREAD_ID = process.env.MM_THREAD_ID || '';
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
- // API configuration (created from environment variables)
48
+ // =============================================================================
49
+ // Permission API Instance
50
+ // =============================================================================
45
51
  const apiConfig = {
46
- url: MM_URL,
47
- token: MM_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 (!MM_URL || !MM_TOKEN || !MM_CHANNEL_ID) {
143
- mcpLogger.error('Missing Mattermost config');
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
- // Post permission request to Mattermost with reaction options
148
- const toolInfo = formatToolForPermission(toolName, toolInput);
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 userId = await getBotUserId();
152
- const post = await createInteractivePost(apiConfig, MM_CHANNEL_ID, message, [APPROVAL_EMOJIS[0], ALLOW_ALL_EMOJIS[0], DENIAL_EMOJIS[0]], MM_THREAD_ID || undefined, userId);
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 { emoji, username } = await waitForReaction(post.id);
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(apiConfig, post.id, `✅ **Allowed** by @${username}\n\n${toolInfo}`);
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(apiConfig, post.id, `✅ **Allowed all** by @${username}\n\n${toolInfo}`);
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(apiConfig, post.id, `❌ **Denied** by @${username}\n\n${toolInfo}`);
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 Mattermost reactions', {
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('Permission server ready');
145
+ mcpLogger.info(`Permission server ready (platform: ${PLATFORM_TYPE})`);
197
146
  }
198
147
  main().catch((err) => {
199
148
  mcpLogger.error(`Fatal: ${err}`);