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
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Permission API Factory
|
|
3
|
+
*
|
|
4
|
+
* Creates platform-specific permission API implementations based on platform type.
|
|
5
|
+
* This isolates platform selection logic to the platform layer.
|
|
6
|
+
*/
|
|
7
|
+
import type { PermissionApi, PermissionApiConfig } from './permission-api.js';
|
|
8
|
+
/**
|
|
9
|
+
* Create a permission API instance for the specified platform type
|
|
10
|
+
*/
|
|
11
|
+
export declare function createPermissionApi(platformType: string, config: PermissionApiConfig): PermissionApi;
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Permission API Factory
|
|
3
|
+
*
|
|
4
|
+
* Creates platform-specific permission API implementations based on platform type.
|
|
5
|
+
* This isolates platform selection logic to the platform layer.
|
|
6
|
+
*/
|
|
7
|
+
import { createMattermostPermissionApi } from './mattermost/permission-api.js';
|
|
8
|
+
/**
|
|
9
|
+
* Create a permission API instance for the specified platform type
|
|
10
|
+
*/
|
|
11
|
+
export function createPermissionApi(platformType, config) {
|
|
12
|
+
switch (platformType) {
|
|
13
|
+
case 'mattermost':
|
|
14
|
+
return createMattermostPermissionApi(config);
|
|
15
|
+
// TODO: Add Slack support
|
|
16
|
+
// case 'slack':
|
|
17
|
+
// return createSlackPermissionApi(config);
|
|
18
|
+
default:
|
|
19
|
+
throw new Error(`Unsupported platform type: ${platformType}`);
|
|
20
|
+
}
|
|
21
|
+
}
|
|
@@ -0,0 +1,67 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Permission API interface for MCP permission server
|
|
3
|
+
*
|
|
4
|
+
* This interface defines the operations needed by the permission server
|
|
5
|
+
* to post permission requests and receive user responses via reactions.
|
|
6
|
+
* Each platform implements this interface with its specific API.
|
|
7
|
+
*/
|
|
8
|
+
import type { PlatformFormatter } from './formatter.js';
|
|
9
|
+
/**
|
|
10
|
+
* Reaction event from WebSocket
|
|
11
|
+
*/
|
|
12
|
+
export interface ReactionEvent {
|
|
13
|
+
postId: string;
|
|
14
|
+
userId: string;
|
|
15
|
+
emojiName: string;
|
|
16
|
+
}
|
|
17
|
+
/**
|
|
18
|
+
* Posted message with ID
|
|
19
|
+
*/
|
|
20
|
+
export interface PostedMessage {
|
|
21
|
+
id: string;
|
|
22
|
+
}
|
|
23
|
+
/**
|
|
24
|
+
* Platform-specific permission API
|
|
25
|
+
*/
|
|
26
|
+
export interface PermissionApi {
|
|
27
|
+
/**
|
|
28
|
+
* Get the markdown formatter for this platform
|
|
29
|
+
*/
|
|
30
|
+
getFormatter(): PlatformFormatter;
|
|
31
|
+
/**
|
|
32
|
+
* Get the bot's user ID
|
|
33
|
+
*/
|
|
34
|
+
getBotUserId(): Promise<string>;
|
|
35
|
+
/**
|
|
36
|
+
* Get a username from a user ID
|
|
37
|
+
*/
|
|
38
|
+
getUsername(userId: string): Promise<string | null>;
|
|
39
|
+
/**
|
|
40
|
+
* Check if a username is in the allowed users list
|
|
41
|
+
*/
|
|
42
|
+
isUserAllowed(username: string): boolean;
|
|
43
|
+
/**
|
|
44
|
+
* Create a post with reaction options
|
|
45
|
+
*/
|
|
46
|
+
createInteractivePost(message: string, reactions: string[], threadId?: string): Promise<PostedMessage>;
|
|
47
|
+
/**
|
|
48
|
+
* Update an existing post
|
|
49
|
+
*/
|
|
50
|
+
updatePost(postId: string, message: string): Promise<void>;
|
|
51
|
+
/**
|
|
52
|
+
* Wait for a reaction on a post
|
|
53
|
+
* Returns the reaction event or null on timeout
|
|
54
|
+
*/
|
|
55
|
+
waitForReaction(postId: string, botUserId: string, timeoutMs: number): Promise<ReactionEvent | null>;
|
|
56
|
+
}
|
|
57
|
+
/**
|
|
58
|
+
* Configuration for permission API
|
|
59
|
+
*/
|
|
60
|
+
export interface PermissionApiConfig {
|
|
61
|
+
url: string;
|
|
62
|
+
token: string;
|
|
63
|
+
channelId: string;
|
|
64
|
+
threadId?: string;
|
|
65
|
+
allowedUsers: string[];
|
|
66
|
+
debug?: boolean;
|
|
67
|
+
}
|
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Permission API interface for MCP permission server
|
|
3
|
+
*
|
|
4
|
+
* This interface defines the operations needed by the permission server
|
|
5
|
+
* to post permission requests and receive user responses via reactions.
|
|
6
|
+
* Each platform implements this interface with its specific API.
|
|
7
|
+
*/
|
|
8
|
+
export {};
|
|
@@ -0,0 +1,70 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Platform-agnostic types for multi-platform support
|
|
3
|
+
*
|
|
4
|
+
* These types normalize the differences between Mattermost, Slack, etc.
|
|
5
|
+
* into a common interface that SessionManager can work with.
|
|
6
|
+
*/
|
|
7
|
+
/**
|
|
8
|
+
* Normalized user representation across platforms
|
|
9
|
+
*/
|
|
10
|
+
export interface PlatformUser {
|
|
11
|
+
id: string;
|
|
12
|
+
username: string;
|
|
13
|
+
email?: string;
|
|
14
|
+
}
|
|
15
|
+
/**
|
|
16
|
+
* Normalized post/message representation across platforms
|
|
17
|
+
*/
|
|
18
|
+
export interface PlatformPost {
|
|
19
|
+
id: string;
|
|
20
|
+
platformId: string;
|
|
21
|
+
channelId: string;
|
|
22
|
+
userId: string;
|
|
23
|
+
message: string;
|
|
24
|
+
rootId?: string;
|
|
25
|
+
createAt?: number;
|
|
26
|
+
metadata?: {
|
|
27
|
+
files?: PlatformFile[];
|
|
28
|
+
[key: string]: unknown;
|
|
29
|
+
};
|
|
30
|
+
}
|
|
31
|
+
/**
|
|
32
|
+
* Normalized reaction representation across platforms
|
|
33
|
+
*/
|
|
34
|
+
export interface PlatformReaction {
|
|
35
|
+
userId: string;
|
|
36
|
+
postId: string;
|
|
37
|
+
emojiName: string;
|
|
38
|
+
createAt?: number;
|
|
39
|
+
}
|
|
40
|
+
/**
|
|
41
|
+
* Normalized file attachment representation across platforms
|
|
42
|
+
*/
|
|
43
|
+
export interface PlatformFile {
|
|
44
|
+
id: string;
|
|
45
|
+
name: string;
|
|
46
|
+
size: number;
|
|
47
|
+
mimeType: string;
|
|
48
|
+
extension?: string;
|
|
49
|
+
}
|
|
50
|
+
/**
|
|
51
|
+
* Request to create a new post
|
|
52
|
+
*/
|
|
53
|
+
export interface CreatePostRequest {
|
|
54
|
+
message: string;
|
|
55
|
+
threadId?: string;
|
|
56
|
+
}
|
|
57
|
+
/**
|
|
58
|
+
* Request to update an existing post
|
|
59
|
+
*/
|
|
60
|
+
export interface UpdatePostRequest {
|
|
61
|
+
postId: string;
|
|
62
|
+
message: string;
|
|
63
|
+
}
|
|
64
|
+
/**
|
|
65
|
+
* Request to add a reaction to a post
|
|
66
|
+
*/
|
|
67
|
+
export interface AddReactionRequest {
|
|
68
|
+
postId: string;
|
|
69
|
+
emojiName: string;
|
|
70
|
+
}
|
|
@@ -0,0 +1,52 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* User commands module
|
|
3
|
+
*
|
|
4
|
+
* Handles user commands like !cd, !invite, !kick, !permissions, !escape, !stop.
|
|
5
|
+
*/
|
|
6
|
+
import type { Session } from './types.js';
|
|
7
|
+
import type { ClaudeEvent } from '../claude/cli.js';
|
|
8
|
+
export interface CommandContext {
|
|
9
|
+
skipPermissions: boolean;
|
|
10
|
+
chromeEnabled: boolean;
|
|
11
|
+
maxSessions: number;
|
|
12
|
+
handleEvent: (sessionId: string, event: ClaudeEvent) => void;
|
|
13
|
+
handleExit: (sessionId: string, code: number) => Promise<void>;
|
|
14
|
+
flush: (session: Session) => Promise<void>;
|
|
15
|
+
startTyping: (session: Session) => void;
|
|
16
|
+
stopTyping: (session: Session) => void;
|
|
17
|
+
persistSession: (session: Session) => void;
|
|
18
|
+
killSession: (threadId: string) => void;
|
|
19
|
+
registerPost: (postId: string, threadId: string) => void;
|
|
20
|
+
}
|
|
21
|
+
/**
|
|
22
|
+
* Cancel a session completely (like !stop or ❌ reaction).
|
|
23
|
+
*/
|
|
24
|
+
export declare function cancelSession(session: Session, username: string, ctx: CommandContext): Promise<void>;
|
|
25
|
+
/**
|
|
26
|
+
* Interrupt current processing but keep session alive (like !escape or ⏸️).
|
|
27
|
+
*/
|
|
28
|
+
export declare function interruptSession(session: Session, username: string): Promise<void>;
|
|
29
|
+
/**
|
|
30
|
+
* Change working directory for a session (restarts Claude CLI).
|
|
31
|
+
*/
|
|
32
|
+
export declare function changeDirectory(session: Session, newDir: string, username: string, ctx: CommandContext): Promise<void>;
|
|
33
|
+
/**
|
|
34
|
+
* Invite a user to participate in a session.
|
|
35
|
+
*/
|
|
36
|
+
export declare function inviteUser(session: Session, invitedUser: string, invitedBy: string, ctx: CommandContext): Promise<void>;
|
|
37
|
+
/**
|
|
38
|
+
* Kick a user from a session.
|
|
39
|
+
*/
|
|
40
|
+
export declare function kickUser(session: Session, kickedUser: string, kickedBy: string, ctx: CommandContext): Promise<void>;
|
|
41
|
+
/**
|
|
42
|
+
* Enable interactive permissions for a session.
|
|
43
|
+
*/
|
|
44
|
+
export declare function enableInteractivePermissions(session: Session, username: string, ctx: CommandContext): Promise<void>;
|
|
45
|
+
/**
|
|
46
|
+
* Request approval for a message from an unauthorized user.
|
|
47
|
+
*/
|
|
48
|
+
export declare function requestMessageApproval(session: Session, username: string, message: string, ctx: CommandContext): Promise<void>;
|
|
49
|
+
/**
|
|
50
|
+
* Update the session header post with current participants and status.
|
|
51
|
+
*/
|
|
52
|
+
export declare function updateSessionHeader(session: Session, ctx: CommandContext): Promise<void>;
|
|
@@ -0,0 +1,323 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* User commands module
|
|
3
|
+
*
|
|
4
|
+
* Handles user commands like !cd, !invite, !kick, !permissions, !escape, !stop.
|
|
5
|
+
*/
|
|
6
|
+
import { ClaudeCli } from '../claude/cli.js';
|
|
7
|
+
import { randomUUID } from 'crypto';
|
|
8
|
+
import { resolve } from 'path';
|
|
9
|
+
import { existsSync, statSync } from 'fs';
|
|
10
|
+
import { getUpdateInfo } from '../update-notifier.js';
|
|
11
|
+
import { getReleaseNotes, getWhatsNewSummary } from '../changelog.js';
|
|
12
|
+
import { getLogo } from '../logo.js';
|
|
13
|
+
import { readFileSync } from 'fs';
|
|
14
|
+
import { dirname } from 'path';
|
|
15
|
+
import { fileURLToPath } from 'url';
|
|
16
|
+
import { APPROVAL_EMOJIS, DENIAL_EMOJIS, ALLOW_ALL_EMOJIS, } from '../utils/emoji.js';
|
|
17
|
+
const __dirname = dirname(fileURLToPath(import.meta.url));
|
|
18
|
+
const pkg = JSON.parse(readFileSync(resolve(__dirname, '..', '..', 'package.json'), 'utf-8'));
|
|
19
|
+
// ---------------------------------------------------------------------------
|
|
20
|
+
// Session control commands
|
|
21
|
+
// ---------------------------------------------------------------------------
|
|
22
|
+
/**
|
|
23
|
+
* Cancel a session completely (like !stop or ❌ reaction).
|
|
24
|
+
*/
|
|
25
|
+
export async function cancelSession(session, username, ctx) {
|
|
26
|
+
const shortId = session.threadId.substring(0, 8);
|
|
27
|
+
console.log(` 🛑 Session (${shortId}…) cancelled by @${username}`);
|
|
28
|
+
await session.platform.createPost(`🛑 **Session cancelled** by @${username}`, session.threadId);
|
|
29
|
+
ctx.killSession(session.threadId);
|
|
30
|
+
}
|
|
31
|
+
/**
|
|
32
|
+
* Interrupt current processing but keep session alive (like !escape or ⏸️).
|
|
33
|
+
*/
|
|
34
|
+
export async function interruptSession(session, username) {
|
|
35
|
+
if (!session.claude.isRunning()) {
|
|
36
|
+
await session.platform.createPost(`ℹ️ Session is idle, nothing to interrupt`, session.threadId);
|
|
37
|
+
return;
|
|
38
|
+
}
|
|
39
|
+
const shortId = session.threadId.substring(0, 8);
|
|
40
|
+
// Set flag BEFORE interrupt - if Claude exits due to SIGINT, we won't unpersist
|
|
41
|
+
session.wasInterrupted = true;
|
|
42
|
+
const interrupted = session.claude.interrupt();
|
|
43
|
+
if (interrupted) {
|
|
44
|
+
console.log(` ⏸️ Session (${shortId}…) interrupted by @${username}`);
|
|
45
|
+
await session.platform.createPost(`⏸️ **Interrupted** by @${username}`, session.threadId);
|
|
46
|
+
}
|
|
47
|
+
}
|
|
48
|
+
// ---------------------------------------------------------------------------
|
|
49
|
+
// Directory management
|
|
50
|
+
// ---------------------------------------------------------------------------
|
|
51
|
+
/**
|
|
52
|
+
* Change working directory for a session (restarts Claude CLI).
|
|
53
|
+
*/
|
|
54
|
+
export async function changeDirectory(session, newDir, username, ctx) {
|
|
55
|
+
// Only session owner or globally allowed users can change directory
|
|
56
|
+
if (session.startedBy !== username && !session.platform.isUserAllowed(username)) {
|
|
57
|
+
await session.platform.createPost(`⚠️ Only @${session.startedBy} or allowed users can change the working directory`, session.threadId);
|
|
58
|
+
return;
|
|
59
|
+
}
|
|
60
|
+
// Expand ~ to home directory
|
|
61
|
+
const expandedDir = newDir.startsWith('~')
|
|
62
|
+
? newDir.replace('~', process.env.HOME || '')
|
|
63
|
+
: newDir;
|
|
64
|
+
// Resolve to absolute path
|
|
65
|
+
const absoluteDir = resolve(expandedDir);
|
|
66
|
+
// Check if directory exists
|
|
67
|
+
if (!existsSync(absoluteDir)) {
|
|
68
|
+
await session.platform.createPost(`❌ Directory does not exist: \`${newDir}\``, session.threadId);
|
|
69
|
+
return;
|
|
70
|
+
}
|
|
71
|
+
if (!statSync(absoluteDir).isDirectory()) {
|
|
72
|
+
await session.platform.createPost(`❌ Not a directory: \`${newDir}\``, session.threadId);
|
|
73
|
+
return;
|
|
74
|
+
}
|
|
75
|
+
const shortId = session.threadId.substring(0, 8);
|
|
76
|
+
const shortDir = absoluteDir.replace(process.env.HOME || '', '~');
|
|
77
|
+
console.log(` 📂 Session (${shortId}…) changing directory to ${shortDir}`);
|
|
78
|
+
// Stop the current Claude CLI
|
|
79
|
+
ctx.stopTyping(session);
|
|
80
|
+
session.isRestarting = true; // Suppress exit message during restart
|
|
81
|
+
session.claude.kill();
|
|
82
|
+
// Flush any pending content
|
|
83
|
+
await ctx.flush(session);
|
|
84
|
+
session.currentPostId = null;
|
|
85
|
+
session.pendingContent = '';
|
|
86
|
+
// Update session working directory
|
|
87
|
+
session.workingDir = absoluteDir;
|
|
88
|
+
// Generate new session ID for fresh start in new directory
|
|
89
|
+
const newSessionId = randomUUID();
|
|
90
|
+
session.claudeSessionId = newSessionId;
|
|
91
|
+
const cliOptions = {
|
|
92
|
+
workingDir: absoluteDir,
|
|
93
|
+
threadId: session.threadId,
|
|
94
|
+
skipPermissions: ctx.skipPermissions || !session.forceInteractivePermissions,
|
|
95
|
+
sessionId: newSessionId,
|
|
96
|
+
resume: false, // Fresh start - can't resume across directories
|
|
97
|
+
chrome: ctx.chromeEnabled,
|
|
98
|
+
platformConfig: session.platform.getMcpConfig(),
|
|
99
|
+
};
|
|
100
|
+
session.claude = new ClaudeCli(cliOptions);
|
|
101
|
+
// Rebind event handlers (use sessionId which is the composite key)
|
|
102
|
+
session.claude.on('event', (e) => ctx.handleEvent(session.sessionId, e));
|
|
103
|
+
session.claude.on('exit', (code) => ctx.handleExit(session.sessionId, code));
|
|
104
|
+
// Start the new Claude CLI
|
|
105
|
+
try {
|
|
106
|
+
session.claude.start();
|
|
107
|
+
}
|
|
108
|
+
catch (err) {
|
|
109
|
+
session.isRestarting = false;
|
|
110
|
+
console.error(' ❌ Failed to restart Claude:', err);
|
|
111
|
+
await session.platform.createPost(`❌ Failed to restart Claude: ${err}`, session.threadId);
|
|
112
|
+
return;
|
|
113
|
+
}
|
|
114
|
+
// Update session header with new directory
|
|
115
|
+
await updateSessionHeader(session, ctx);
|
|
116
|
+
// Post confirmation
|
|
117
|
+
await session.platform.createPost(`📂 **Working directory changed** to \`${shortDir}\`\n*Claude Code restarted in new directory*`, session.threadId);
|
|
118
|
+
// Update activity
|
|
119
|
+
session.lastActivityAt = new Date();
|
|
120
|
+
session.timeoutWarningPosted = false;
|
|
121
|
+
// Persist the updated session state
|
|
122
|
+
ctx.persistSession(session);
|
|
123
|
+
}
|
|
124
|
+
// ---------------------------------------------------------------------------
|
|
125
|
+
// User collaboration commands
|
|
126
|
+
// ---------------------------------------------------------------------------
|
|
127
|
+
/**
|
|
128
|
+
* Invite a user to participate in a session.
|
|
129
|
+
*/
|
|
130
|
+
export async function inviteUser(session, invitedUser, invitedBy, ctx) {
|
|
131
|
+
// Only session owner or globally allowed users can invite
|
|
132
|
+
if (session.startedBy !== invitedBy && !session.platform.isUserAllowed(invitedBy)) {
|
|
133
|
+
await session.platform.createPost(`⚠️ Only @${session.startedBy} or allowed users can invite others`, session.threadId);
|
|
134
|
+
return;
|
|
135
|
+
}
|
|
136
|
+
session.sessionAllowedUsers.add(invitedUser);
|
|
137
|
+
await session.platform.createPost(`✅ @${invitedUser} can now participate in this session (invited by @${invitedBy})`, session.threadId);
|
|
138
|
+
console.log(` 👋 @${invitedUser} invited to session by @${invitedBy}`);
|
|
139
|
+
await updateSessionHeader(session, ctx);
|
|
140
|
+
ctx.persistSession(session);
|
|
141
|
+
}
|
|
142
|
+
/**
|
|
143
|
+
* Kick a user from a session.
|
|
144
|
+
*/
|
|
145
|
+
export async function kickUser(session, kickedUser, kickedBy, ctx) {
|
|
146
|
+
// Only session owner or globally allowed users can kick
|
|
147
|
+
if (session.startedBy !== kickedBy && !session.platform.isUserAllowed(kickedBy)) {
|
|
148
|
+
await session.platform.createPost(`⚠️ Only @${session.startedBy} or allowed users can kick others`, session.threadId);
|
|
149
|
+
return;
|
|
150
|
+
}
|
|
151
|
+
// Can't kick session owner
|
|
152
|
+
if (kickedUser === session.startedBy) {
|
|
153
|
+
await session.platform.createPost(`⚠️ Cannot kick session owner @${session.startedBy}`, session.threadId);
|
|
154
|
+
return;
|
|
155
|
+
}
|
|
156
|
+
// Can't kick globally allowed users
|
|
157
|
+
if (session.platform.isUserAllowed(kickedUser)) {
|
|
158
|
+
await session.platform.createPost(`⚠️ @${kickedUser} is globally allowed and cannot be kicked from individual sessions`, session.threadId);
|
|
159
|
+
return;
|
|
160
|
+
}
|
|
161
|
+
if (session.sessionAllowedUsers.delete(kickedUser)) {
|
|
162
|
+
await session.platform.createPost(`🚫 @${kickedUser} removed from this session by @${kickedBy}`, session.threadId);
|
|
163
|
+
console.log(` 🚫 @${kickedUser} kicked from session by @${kickedBy}`);
|
|
164
|
+
await updateSessionHeader(session, ctx);
|
|
165
|
+
ctx.persistSession(session);
|
|
166
|
+
}
|
|
167
|
+
else {
|
|
168
|
+
await session.platform.createPost(`⚠️ @${kickedUser} was not in this session`, session.threadId);
|
|
169
|
+
}
|
|
170
|
+
}
|
|
171
|
+
// ---------------------------------------------------------------------------
|
|
172
|
+
// Permission management
|
|
173
|
+
// ---------------------------------------------------------------------------
|
|
174
|
+
/**
|
|
175
|
+
* Enable interactive permissions for a session.
|
|
176
|
+
*/
|
|
177
|
+
export async function enableInteractivePermissions(session, username, ctx) {
|
|
178
|
+
// Only session owner or globally allowed users can change permissions
|
|
179
|
+
if (session.startedBy !== username && !session.platform.isUserAllowed(username)) {
|
|
180
|
+
await session.platform.createPost(`⚠️ Only @${session.startedBy} or allowed users can change permissions`, session.threadId);
|
|
181
|
+
return;
|
|
182
|
+
}
|
|
183
|
+
// Can only downgrade, not upgrade
|
|
184
|
+
if (!ctx.skipPermissions) {
|
|
185
|
+
await session.platform.createPost(`ℹ️ Permissions are already interactive for this session`, session.threadId);
|
|
186
|
+
return;
|
|
187
|
+
}
|
|
188
|
+
// Already enabled for this session
|
|
189
|
+
if (session.forceInteractivePermissions) {
|
|
190
|
+
await session.platform.createPost(`ℹ️ Interactive permissions already enabled for this session`, session.threadId);
|
|
191
|
+
return;
|
|
192
|
+
}
|
|
193
|
+
// Set the flag
|
|
194
|
+
session.forceInteractivePermissions = true;
|
|
195
|
+
const shortId = session.threadId.substring(0, 8);
|
|
196
|
+
console.log(` 🔐 Session (${shortId}…) enabling interactive permissions`);
|
|
197
|
+
// Stop the current Claude CLI and restart with new permission setting
|
|
198
|
+
ctx.stopTyping(session);
|
|
199
|
+
session.isRestarting = true;
|
|
200
|
+
session.claude.kill();
|
|
201
|
+
// Flush any pending content
|
|
202
|
+
await ctx.flush(session);
|
|
203
|
+
session.currentPostId = null;
|
|
204
|
+
session.pendingContent = '';
|
|
205
|
+
// Create new CLI options with interactive permissions
|
|
206
|
+
const cliOptions = {
|
|
207
|
+
workingDir: session.workingDir,
|
|
208
|
+
threadId: session.threadId,
|
|
209
|
+
skipPermissions: false, // Force interactive permissions
|
|
210
|
+
sessionId: session.claudeSessionId,
|
|
211
|
+
resume: true, // Resume to keep conversation context
|
|
212
|
+
chrome: ctx.chromeEnabled,
|
|
213
|
+
platformConfig: session.platform.getMcpConfig(),
|
|
214
|
+
};
|
|
215
|
+
session.claude = new ClaudeCli(cliOptions);
|
|
216
|
+
// Rebind event handlers (use sessionId which is the composite key)
|
|
217
|
+
session.claude.on('event', (e) => ctx.handleEvent(session.sessionId, e));
|
|
218
|
+
session.claude.on('exit', (code) => ctx.handleExit(session.sessionId, code));
|
|
219
|
+
// Start the new Claude CLI
|
|
220
|
+
try {
|
|
221
|
+
session.claude.start();
|
|
222
|
+
}
|
|
223
|
+
catch (err) {
|
|
224
|
+
session.isRestarting = false;
|
|
225
|
+
console.error(' ❌ Failed to restart Claude:', err);
|
|
226
|
+
await session.platform.createPost(`❌ Failed to enable interactive permissions: ${err}`, session.threadId);
|
|
227
|
+
return;
|
|
228
|
+
}
|
|
229
|
+
// Update session header with new permission status
|
|
230
|
+
await updateSessionHeader(session, ctx);
|
|
231
|
+
// Post confirmation
|
|
232
|
+
await session.platform.createPost(`🔐 **Interactive permissions enabled** for this session by @${username}\n*Claude Code restarted with permission prompts*`, session.threadId);
|
|
233
|
+
console.log(` 🔐 Interactive permissions enabled for session by @${username}`);
|
|
234
|
+
// Update activity and persist
|
|
235
|
+
session.lastActivityAt = new Date();
|
|
236
|
+
session.timeoutWarningPosted = false;
|
|
237
|
+
ctx.persistSession(session);
|
|
238
|
+
}
|
|
239
|
+
// ---------------------------------------------------------------------------
|
|
240
|
+
// Message approval
|
|
241
|
+
// ---------------------------------------------------------------------------
|
|
242
|
+
/**
|
|
243
|
+
* Request approval for a message from an unauthorized user.
|
|
244
|
+
*/
|
|
245
|
+
export async function requestMessageApproval(session, username, message, ctx) {
|
|
246
|
+
// If there's already a pending message approval, ignore
|
|
247
|
+
if (session.pendingMessageApproval) {
|
|
248
|
+
return;
|
|
249
|
+
}
|
|
250
|
+
// Truncate long messages for display
|
|
251
|
+
const displayMessage = message.length > 200 ? message.substring(0, 200) + '...' : message;
|
|
252
|
+
const approvalMessage = `🔒 **Message from @${username}** needs approval:\n\n` +
|
|
253
|
+
`> ${displayMessage}\n\n` +
|
|
254
|
+
`React: 👍 Allow once | ✅ Invite to session | 👎 Deny`;
|
|
255
|
+
const post = await session.platform.createInteractivePost(approvalMessage, [APPROVAL_EMOJIS[0], ALLOW_ALL_EMOJIS[0], DENIAL_EMOJIS[0]], session.threadId);
|
|
256
|
+
session.pendingMessageApproval = {
|
|
257
|
+
postId: post.id,
|
|
258
|
+
originalMessage: message,
|
|
259
|
+
fromUser: username,
|
|
260
|
+
};
|
|
261
|
+
// Register post for reaction routing
|
|
262
|
+
ctx.registerPost(post.id, session.threadId);
|
|
263
|
+
}
|
|
264
|
+
// ---------------------------------------------------------------------------
|
|
265
|
+
// Session header
|
|
266
|
+
// ---------------------------------------------------------------------------
|
|
267
|
+
/**
|
|
268
|
+
* Update the session header post with current participants and status.
|
|
269
|
+
*/
|
|
270
|
+
export async function updateSessionHeader(session, ctx) {
|
|
271
|
+
if (!session.sessionStartPostId)
|
|
272
|
+
return;
|
|
273
|
+
// Use session's working directory
|
|
274
|
+
const shortDir = session.workingDir.replace(process.env.HOME || '', '~');
|
|
275
|
+
// Check session-level permission override
|
|
276
|
+
const isInteractive = !ctx.skipPermissions || session.forceInteractivePermissions;
|
|
277
|
+
const permMode = isInteractive ? '🔐 Interactive' : '⚡ Auto';
|
|
278
|
+
// Build participants list (excluding owner)
|
|
279
|
+
const otherParticipants = [...session.sessionAllowedUsers]
|
|
280
|
+
.filter((u) => u !== session.startedBy)
|
|
281
|
+
.map((u) => `@${u}`)
|
|
282
|
+
.join(', ');
|
|
283
|
+
const rows = [
|
|
284
|
+
`| 📂 **Directory** | \`${shortDir}\` |`,
|
|
285
|
+
`| 👤 **Started by** | @${session.startedBy} |`,
|
|
286
|
+
];
|
|
287
|
+
// Show worktree info if active
|
|
288
|
+
if (session.worktreeInfo) {
|
|
289
|
+
const shortRepoRoot = session.worktreeInfo.repoRoot.replace(process.env.HOME || '', '~');
|
|
290
|
+
rows.push(`| 🌿 **Worktree** | \`${session.worktreeInfo.branch}\` (from \`${shortRepoRoot}\`) |`);
|
|
291
|
+
}
|
|
292
|
+
if (otherParticipants) {
|
|
293
|
+
rows.push(`| 👥 **Participants** | ${otherParticipants} |`);
|
|
294
|
+
}
|
|
295
|
+
rows.push(`| 🔢 **Session** | #${session.sessionNumber} of ${ctx.maxSessions} max |`);
|
|
296
|
+
rows.push(`| ${permMode.split(' ')[0]} **Permissions** | ${permMode.split(' ')[1]} |`);
|
|
297
|
+
if (ctx.chromeEnabled) {
|
|
298
|
+
rows.push(`| 🌐 **Chrome** | Enabled |`);
|
|
299
|
+
}
|
|
300
|
+
// Check for available updates
|
|
301
|
+
const updateInfo = getUpdateInfo();
|
|
302
|
+
const updateNotice = updateInfo
|
|
303
|
+
? `\n> ⚠️ **Update available:** v${updateInfo.current} → v${updateInfo.latest} - Run \`npm install -g claude-threads\`\n`
|
|
304
|
+
: '';
|
|
305
|
+
// Get "What's new" from release notes
|
|
306
|
+
const releaseNotes = getReleaseNotes(pkg.version);
|
|
307
|
+
const whatsNew = releaseNotes ? getWhatsNewSummary(releaseNotes) : '';
|
|
308
|
+
const whatsNewLine = whatsNew ? `\n> ✨ **What's new:** ${whatsNew}\n` : '';
|
|
309
|
+
const msg = [
|
|
310
|
+
getLogo(pkg.version),
|
|
311
|
+
updateNotice,
|
|
312
|
+
whatsNewLine,
|
|
313
|
+
`| | |`,
|
|
314
|
+
`|:--|:--|`,
|
|
315
|
+
...rows,
|
|
316
|
+
].join('\n');
|
|
317
|
+
try {
|
|
318
|
+
await session.platform.updatePost(session.sessionStartPostId, msg);
|
|
319
|
+
}
|
|
320
|
+
catch (err) {
|
|
321
|
+
console.error(' ⚠️ Failed to update session header:', err);
|
|
322
|
+
}
|
|
323
|
+
}
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Claude event handling module
|
|
3
|
+
*
|
|
4
|
+
* Handles events from Claude CLI: assistant messages, tool use,
|
|
5
|
+
* tool results, tasks, questions, and plan approvals.
|
|
6
|
+
*/
|
|
7
|
+
import type { Session } from './types.js';
|
|
8
|
+
import type { ClaudeEvent } from '../claude/cli.js';
|
|
9
|
+
export interface EventContext {
|
|
10
|
+
debug: boolean;
|
|
11
|
+
registerPost: (postId: string, threadId: string) => void;
|
|
12
|
+
flush: (session: Session) => Promise<void>;
|
|
13
|
+
startTyping: (session: Session) => void;
|
|
14
|
+
stopTyping: (session: Session) => void;
|
|
15
|
+
appendContent: (session: Session, text: string) => void;
|
|
16
|
+
}
|
|
17
|
+
/**
|
|
18
|
+
* Handle a Claude event from the CLI stream.
|
|
19
|
+
* Routes to appropriate handler based on event type.
|
|
20
|
+
*/
|
|
21
|
+
export declare function handleEvent(session: Session, event: ClaudeEvent, ctx: EventContext): void;
|
|
22
|
+
/**
|
|
23
|
+
* Post the current question in the question set.
|
|
24
|
+
*/
|
|
25
|
+
export declare function postCurrentQuestion(session: Session, ctx: EventContext): Promise<void>;
|