claude-threads 0.13.0 → 0.14.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (67) 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/update-notifier.js +10 -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 +4 -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
@@ -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>;