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.
- package/CHANGELOG.md +53 -0
- package/README.md +142 -37
- 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 +39 -15
- package/dist/logo.d.ts +3 -20
- package/dist/logo.js +7 -23
- package/dist/mcp/permission-server.js +61 -112
- package/dist/onboarding.d.ts +1 -1
- package/dist/onboarding.js +271 -69
- 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/{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 +2 -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
package/dist/index.js
CHANGED
|
@@ -1,9 +1,9 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
2
|
import { program } from 'commander';
|
|
3
|
-
import {
|
|
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 './
|
|
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 (
|
|
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
|
|
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('@' +
|
|
69
|
-
console.log(` 🌐 ${dim(
|
|
70
|
-
if (
|
|
71
|
-
console.log(` ⚠️
|
|
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(
|
|
81
|
-
const session = new SessionManager(
|
|
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.
|
|
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.
|
|
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 @${
|
|
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
|
|
8
|
-
*
|
|
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
|
|
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
|
-
//
|
|
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
|
-
|
|
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}
|
|
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
|
|
27
|
-
*
|
|
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
|
|
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
|
-
✴ █▀ █ ✴
|
|
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
|
|
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}`);
|
package/dist/onboarding.d.ts
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
export declare function runOnboarding(): Promise<void>;
|
|
1
|
+
export declare function runOnboarding(reconfigure?: boolean): Promise<void>;
|