claude-threads 0.12.1 → 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 (67) hide show
  1. package/CHANGELOG.md +53 -0
  2. package/README.md +142 -37
  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 +39 -15
  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.d.ts +1 -1
  16. package/dist/onboarding.js +271 -69
  17. package/dist/persistence/session-store.d.ts +8 -2
  18. package/dist/persistence/session-store.js +41 -16
  19. package/dist/platform/client.d.ts +140 -0
  20. package/dist/platform/formatter.d.ts +74 -0
  21. package/dist/platform/index.d.ts +11 -0
  22. package/dist/platform/index.js +8 -0
  23. package/dist/platform/mattermost/client.d.ts +70 -0
  24. package/dist/{mattermost → platform/mattermost}/client.js +117 -34
  25. package/dist/platform/mattermost/formatter.d.ts +20 -0
  26. package/dist/platform/mattermost/formatter.js +46 -0
  27. package/dist/platform/mattermost/permission-api.d.ts +10 -0
  28. package/dist/platform/mattermost/permission-api.js +139 -0
  29. package/dist/platform/mattermost/types.js +1 -0
  30. package/dist/platform/permission-api-factory.d.ts +11 -0
  31. package/dist/platform/permission-api-factory.js +21 -0
  32. package/dist/platform/permission-api.d.ts +67 -0
  33. package/dist/platform/permission-api.js +8 -0
  34. package/dist/platform/types.d.ts +70 -0
  35. package/dist/platform/types.js +7 -0
  36. package/dist/session/commands.d.ts +52 -0
  37. package/dist/session/commands.js +323 -0
  38. package/dist/session/events.d.ts +25 -0
  39. package/dist/session/events.js +368 -0
  40. package/dist/session/index.d.ts +7 -0
  41. package/dist/session/index.js +6 -0
  42. package/dist/session/lifecycle.d.ts +70 -0
  43. package/dist/session/lifecycle.js +456 -0
  44. package/dist/session/manager.d.ts +96 -0
  45. package/dist/session/manager.js +537 -0
  46. package/dist/session/reactions.d.ts +25 -0
  47. package/dist/session/reactions.js +151 -0
  48. package/dist/session/streaming.d.ts +47 -0
  49. package/dist/session/streaming.js +152 -0
  50. package/dist/session/types.d.ts +78 -0
  51. package/dist/session/types.js +9 -0
  52. package/dist/session/worktree.d.ts +56 -0
  53. package/dist/session/worktree.js +339 -0
  54. package/dist/{mattermost → utils}/emoji.d.ts +3 -3
  55. package/dist/{mattermost → utils}/emoji.js +3 -3
  56. package/dist/utils/emoji.test.d.ts +1 -0
  57. package/dist/utils/tool-formatter.d.ts +10 -13
  58. package/dist/utils/tool-formatter.js +48 -43
  59. package/dist/utils/tool-formatter.test.js +67 -52
  60. package/package.json +2 -3
  61. package/dist/claude/session.d.ts +0 -256
  62. package/dist/claude/session.js +0 -1964
  63. package/dist/mattermost/client.d.ts +0 -56
  64. /package/dist/{mattermost/emoji.test.d.ts → platform/client.js} +0 -0
  65. /package/dist/{mattermost/types.js → platform/formatter.js} +0 -0
  66. /package/dist/{mattermost → platform/mattermost}/types.d.ts +0 -0
  67. /package/dist/{mattermost → utils}/emoji.test.js +0 -0
package/dist/index.js CHANGED
@@ -1,9 +1,9 @@
1
1
  #!/usr/bin/env node
2
2
  import { program } from 'commander';
3
- import { loadConfig, configExists } from './config.js';
3
+ import { loadConfigWithMigration, configExists as checkConfigExists } from './config/migration.js';
4
4
  import { runOnboarding } from './onboarding.js';
5
- import { MattermostClient } from './mattermost/client.js';
6
- import { SessionManager } from './claude/session.js';
5
+ import { MattermostClient } from './platform/mattermost/client.js';
6
+ import { SessionManager } from './session/index.js';
7
7
  import { readFileSync } from 'fs';
8
8
  import { dirname, resolve } from 'path';
9
9
  import { fileURLToPath } from 'url';
@@ -29,6 +29,8 @@ program
29
29
  .option('--no-skip-permissions', 'Enable interactive permission prompts (override env)')
30
30
  .option('--chrome', 'Enable Claude in Chrome integration')
31
31
  .option('--no-chrome', 'Disable Claude in Chrome integration')
32
+ .option('--worktree-mode <mode>', 'Git worktree mode: off, prompt, require (default: prompt)')
33
+ .option('--setup', 'Run interactive setup wizard (reconfigure existing settings)')
32
34
  .option('--debug', 'Enable debug logging')
33
35
  .parse();
34
36
  const opts = program.opts();
@@ -52,23 +54,43 @@ async function main() {
52
54
  allowedUsers: opts.allowedUsers,
53
55
  skipPermissions: opts.skipPermissions,
54
56
  chrome: opts.chrome,
57
+ worktreeMode: opts.worktreeMode,
55
58
  };
56
59
  // Check if we need onboarding
57
- if (!configExists() && !hasRequiredCliArgs(opts)) {
58
- await runOnboarding();
60
+ if (opts.setup) {
61
+ await runOnboarding(true); // reconfigure mode
62
+ }
63
+ else if (!checkConfigExists() && !hasRequiredCliArgs(opts)) {
64
+ await runOnboarding(false); // first-time mode
59
65
  }
60
66
  const workingDir = process.cwd();
61
- const config = loadConfig(cliArgs);
67
+ const newConfig = loadConfigWithMigration();
68
+ if (!newConfig) {
69
+ throw new Error('No configuration found. Run with --setup to configure.');
70
+ }
71
+ // CLI args can override global settings
72
+ if (cliArgs.chrome !== undefined) {
73
+ newConfig.chrome = cliArgs.chrome;
74
+ }
75
+ if (cliArgs.worktreeMode !== undefined) {
76
+ newConfig.worktreeMode = cliArgs.worktreeMode;
77
+ }
78
+ // Get first Mattermost platform
79
+ const platformConfig = newConfig.platforms.find(p => p.type === 'mattermost');
80
+ if (!platformConfig) {
81
+ throw new Error('No Mattermost platform configured.');
82
+ }
83
+ const config = newConfig;
62
84
  // Print ASCII logo
63
85
  printLogo();
64
86
  // Startup info
65
87
  console.log(dim(` v${pkg.version}`));
66
88
  console.log('');
67
89
  console.log(` 📂 ${cyan(workingDir)}`);
68
- console.log(` 💬 ${cyan('@' + config.mattermost.botName)}`);
69
- console.log(` 🌐 ${dim(config.mattermost.url)}`);
70
- if (config.skipPermissions) {
71
- console.log(` ⚠️ ${dim('Permissions disabled')}`);
90
+ console.log(` 💬 ${cyan('@' + platformConfig.botName)}`);
91
+ console.log(` 🌐 ${dim(platformConfig.url)}`);
92
+ if (platformConfig.skipPermissions) {
93
+ console.log(` ⚠️ ${dim('Permissions disabled')}`);
72
94
  }
73
95
  else {
74
96
  console.log(` 🔐 ${dim('Interactive permissions')}`);
@@ -77,13 +99,15 @@ async function main() {
77
99
  console.log(` 🌐 ${dim('Chrome integration enabled')}`);
78
100
  }
79
101
  console.log('');
80
- const mattermost = new MattermostClient(config);
81
- const session = new SessionManager(mattermost, workingDir, config.skipPermissions, config.chrome, config.worktreeMode);
102
+ const mattermost = new MattermostClient(platformConfig);
103
+ const session = new SessionManager(workingDir, platformConfig.skipPermissions, config.chrome, config.worktreeMode);
104
+ // Register platform (connects event handlers)
105
+ session.addPlatform(platformConfig.id, mattermost);
82
106
  mattermost.on('message', async (post, user) => {
83
107
  try {
84
108
  const username = user?.username || 'unknown';
85
109
  const message = post.message;
86
- const threadRoot = post.root_id || post.id;
110
+ const threadRoot = post.rootId || post.id;
87
111
  // Check for !kill command FIRST - works anywhere, even as the first message
88
112
  const lowerMessage = message.trim().toLowerCase();
89
113
  if (lowerMessage === '!kill' || (mattermost.isBotMentioned(message) && mattermost.extractPrompt(message).toLowerCase() === '!kill')) {
@@ -323,7 +347,7 @@ async function main() {
323
347
  console.error(' ❌ Error handling message:', err);
324
348
  // Try to notify user if possible
325
349
  try {
326
- const threadRoot = post.root_id || post.id;
350
+ const threadRoot = post.rootId || post.id;
327
351
  await mattermost.createPost(`⚠️ An error occurred. Please try again.`, threadRoot);
328
352
  }
329
353
  catch {
@@ -336,7 +360,7 @@ async function main() {
336
360
  await mattermost.connect();
337
361
  // Resume any persisted sessions from before restart
338
362
  await session.initialize();
339
- console.log(` ✅ ${bold('Ready!')} Waiting for @${config.mattermost.botName} mentions...`);
363
+ console.log(` ✅ ${bold('Ready!')} Waiting for @${platformConfig.botName} mentions...`);
340
364
  console.log('');
341
365
  let isShuttingDown = false;
342
366
  const shutdown = async () => {
package/dist/logo.d.ts CHANGED
@@ -4,27 +4,10 @@
4
4
  * Stylized CT in Claude Code's block character style.
5
5
  */
6
6
  /**
7
- * ASCII logo for CLI display (with ANSI colors)
8
- * Stylized CT in block characters
7
+ * Get ASCII logo for claude-threads with version included
8
+ * For display in chat platforms (plain text, no ANSI codes)
9
9
  */
10
- export declare const CLI_LOGO: string;
11
- /**
12
- * ASCII logo for Mattermost (plain text, no ANSI codes)
13
- * Use getMattermostLogo(version) instead to include version
14
- */
15
- export declare const MATTERMOST_LOGO = "```\n \u2734 \u2584\u2588\u2580 \u2588\u2588\u2588 \u2734 claude-threads\n\u2734 \u2588\u2580 \u2588 \u2734 Mattermost \u00D7 Claude Code\n \u2734 \u2580\u2588\u2584 \u2588 \u2734\n```";
16
- /**
17
- * Get ASCII logo for Mattermost with version included
18
- */
19
- export declare function getMattermostLogo(version: string): string;
20
- /**
21
- * Compact inline logo for Mattermost headers
22
- */
23
- export declare const MATTERMOST_LOGO_INLINE = "`\u2584\u2588\u2580T` **claude-threads**";
24
- /**
25
- * Very compact logo for space-constrained contexts
26
- */
27
- export declare const LOGO_COMPACT = "\u2584\u2588\u2580T claude-threads";
10
+ export declare function getLogo(version: string): string;
28
11
  /**
29
12
  * Print CLI logo to stdout
30
13
  */
package/dist/logo.js CHANGED
@@ -8,7 +8,7 @@ const colors = {
8
8
  reset: '\x1b[0m',
9
9
  bold: '\x1b[1m',
10
10
  dim: '\x1b[2m',
11
- // Mattermost blue (#1C58D9)
11
+ // Claude blue
12
12
  blue: '\x1b[38;5;27m',
13
13
  // Claude orange/coral
14
14
  orange: '\x1b[38;5;209m',
@@ -17,38 +17,22 @@ const colors = {
17
17
  * ASCII logo for CLI display (with ANSI colors)
18
18
  * Stylized CT in block characters
19
19
  */
20
- export const CLI_LOGO = `
20
+ const CLI_LOGO = `
21
21
  ${colors.orange} ✴${colors.reset} ${colors.blue}▄█▀ ███${colors.reset} ${colors.orange}✴${colors.reset} ${colors.bold}claude-threads${colors.reset}
22
- ${colors.orange}✴${colors.reset} ${colors.blue}█▀ █${colors.reset} ${colors.orange}✴${colors.reset} ${colors.dim}Mattermost × Claude Code${colors.reset}
22
+ ${colors.orange}✴${colors.reset} ${colors.blue}█▀ █${colors.reset} ${colors.orange}✴${colors.reset} ${colors.dim}Chat × Claude Code${colors.reset}
23
23
  ${colors.orange}✴${colors.reset} ${colors.blue}▀█▄ █${colors.reset} ${colors.orange}✴${colors.reset}
24
24
  `;
25
25
  /**
26
- * ASCII logo for Mattermost (plain text, no ANSI codes)
27
- * Use getMattermostLogo(version) instead to include version
26
+ * Get ASCII logo for claude-threads with version included
27
+ * For display in chat platforms (plain text, no ANSI codes)
28
28
  */
29
- export const MATTERMOST_LOGO = `\`\`\`
30
- ✴ ▄█▀ ███ ✴ claude-threads
31
- ✴ █▀ █ ✴ Mattermost × Claude Code
32
- ✴ ▀█▄ █ ✴
33
- \`\`\``;
34
- /**
35
- * Get ASCII logo for Mattermost with version included
36
- */
37
- export function getMattermostLogo(version) {
29
+ export function getLogo(version) {
38
30
  return `\`\`\`
39
31
  ✴ ▄█▀ ███ ✴ claude-threads v${version}
40
- ✴ █▀ █ ✴ Mattermost × Claude Code
32
+ ✴ █▀ █ ✴ Chat × Claude Code
41
33
  ✴ ▀█▄ █ ✴
42
34
  \`\`\``;
43
35
  }
44
- /**
45
- * Compact inline logo for Mattermost headers
46
- */
47
- export const MATTERMOST_LOGO_INLINE = '`▄█▀T` **claude-threads**';
48
- /**
49
- * Very compact logo for space-constrained contexts
50
- */
51
- export const LOGO_COMPACT = '▄█▀T claude-threads';
52
36
  /**
53
37
  * Print CLI logo to stdout
54
38
  */
@@ -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}`);
@@ -1 +1 @@
1
- export declare function runOnboarding(): Promise<void>;
1
+ export declare function runOnboarding(reconfigure?: boolean): Promise<void>;