claude-threads 0.15.0 → 0.16.2

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 (79) hide show
  1. package/CHANGELOG.md +28 -0
  2. package/README.md +5 -5
  3. package/dist/index.js +20397 -387
  4. package/dist/mcp/permission-server.js +34039 -139
  5. package/package.json +14 -18
  6. package/dist/changelog.d.ts +0 -20
  7. package/dist/changelog.js +0 -134
  8. package/dist/claude/cli.d.ts +0 -50
  9. package/dist/claude/cli.js +0 -181
  10. package/dist/config/migration.d.ts +0 -45
  11. package/dist/config/migration.js +0 -35
  12. package/dist/config.d.ts +0 -21
  13. package/dist/config.js +0 -7
  14. package/dist/git/worktree.d.ts +0 -46
  15. package/dist/git/worktree.js +0 -228
  16. package/dist/index.d.ts +0 -2
  17. package/dist/logo.d.ts +0 -14
  18. package/dist/logo.js +0 -41
  19. package/dist/mattermost/api.d.ts +0 -85
  20. package/dist/mattermost/api.js +0 -124
  21. package/dist/mattermost/api.test.d.ts +0 -1
  22. package/dist/mattermost/api.test.js +0 -319
  23. package/dist/mcp/permission-server.d.ts +0 -2
  24. package/dist/onboarding.d.ts +0 -1
  25. package/dist/onboarding.js +0 -318
  26. package/dist/persistence/session-store.d.ts +0 -71
  27. package/dist/persistence/session-store.js +0 -152
  28. package/dist/platform/client.d.ts +0 -140
  29. package/dist/platform/client.js +0 -1
  30. package/dist/platform/formatter.d.ts +0 -74
  31. package/dist/platform/formatter.js +0 -1
  32. package/dist/platform/index.d.ts +0 -11
  33. package/dist/platform/index.js +0 -8
  34. package/dist/platform/mattermost/client.d.ts +0 -70
  35. package/dist/platform/mattermost/client.js +0 -404
  36. package/dist/platform/mattermost/formatter.d.ts +0 -20
  37. package/dist/platform/mattermost/formatter.js +0 -46
  38. package/dist/platform/mattermost/permission-api.d.ts +0 -10
  39. package/dist/platform/mattermost/permission-api.js +0 -139
  40. package/dist/platform/mattermost/types.d.ts +0 -71
  41. package/dist/platform/mattermost/types.js +0 -1
  42. package/dist/platform/permission-api-factory.d.ts +0 -11
  43. package/dist/platform/permission-api-factory.js +0 -21
  44. package/dist/platform/permission-api.d.ts +0 -67
  45. package/dist/platform/permission-api.js +0 -8
  46. package/dist/platform/types.d.ts +0 -70
  47. package/dist/platform/types.js +0 -7
  48. package/dist/session/commands.d.ts +0 -52
  49. package/dist/session/commands.js +0 -323
  50. package/dist/session/events.d.ts +0 -25
  51. package/dist/session/events.js +0 -368
  52. package/dist/session/index.d.ts +0 -7
  53. package/dist/session/index.js +0 -6
  54. package/dist/session/lifecycle.d.ts +0 -70
  55. package/dist/session/lifecycle.js +0 -456
  56. package/dist/session/manager.d.ts +0 -96
  57. package/dist/session/manager.js +0 -537
  58. package/dist/session/reactions.d.ts +0 -25
  59. package/dist/session/reactions.js +0 -151
  60. package/dist/session/streaming.d.ts +0 -47
  61. package/dist/session/streaming.js +0 -152
  62. package/dist/session/types.d.ts +0 -78
  63. package/dist/session/types.js +0 -9
  64. package/dist/session/worktree.d.ts +0 -56
  65. package/dist/session/worktree.js +0 -339
  66. package/dist/update-notifier.d.ts +0 -3
  67. package/dist/update-notifier.js +0 -41
  68. package/dist/utils/emoji.d.ts +0 -43
  69. package/dist/utils/emoji.js +0 -65
  70. package/dist/utils/emoji.test.d.ts +0 -1
  71. package/dist/utils/emoji.test.js +0 -131
  72. package/dist/utils/logger.d.ts +0 -34
  73. package/dist/utils/logger.js +0 -42
  74. package/dist/utils/logger.test.d.ts +0 -1
  75. package/dist/utils/logger.test.js +0 -121
  76. package/dist/utils/tool-formatter.d.ts +0 -53
  77. package/dist/utils/tool-formatter.js +0 -252
  78. package/dist/utils/tool-formatter.test.d.ts +0 -1
  79. package/dist/utils/tool-formatter.test.js +0 -372
@@ -1,368 +0,0 @@
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 { formatToolUse as sharedFormatToolUse } from '../utils/tool-formatter.js';
8
- import { NUMBER_EMOJIS, APPROVAL_EMOJIS, DENIAL_EMOJIS, } from '../utils/emoji.js';
9
- // ---------------------------------------------------------------------------
10
- // Main event handler
11
- // ---------------------------------------------------------------------------
12
- /**
13
- * Handle a Claude event from the CLI stream.
14
- * Routes to appropriate handler based on event type.
15
- */
16
- export function handleEvent(session, event, ctx) {
17
- // Update last activity and reset timeout warning
18
- session.lastActivityAt = new Date();
19
- session.timeoutWarningPosted = false;
20
- // Check for special tool uses that need custom handling
21
- if (event.type === 'assistant') {
22
- const msg = event.message;
23
- let hasSpecialTool = false;
24
- for (const block of msg?.content || []) {
25
- if (block.type === 'tool_use') {
26
- if (block.name === 'ExitPlanMode') {
27
- handleExitPlanMode(session, block.id, ctx);
28
- hasSpecialTool = true;
29
- }
30
- else if (block.name === 'TodoWrite') {
31
- handleTodoWrite(session, block.input);
32
- }
33
- else if (block.name === 'Task') {
34
- handleTaskStart(session, block.id, block.input);
35
- }
36
- else if (block.name === 'AskUserQuestion') {
37
- handleAskUserQuestion(session, block.id, block.input, ctx);
38
- hasSpecialTool = true;
39
- }
40
- }
41
- }
42
- if (hasSpecialTool)
43
- return;
44
- }
45
- // Check for tool_result to update subagent status
46
- if (event.type === 'user') {
47
- const msg = event.message;
48
- for (const block of msg?.content || []) {
49
- if (block.type === 'tool_result' && block.tool_use_id) {
50
- const postId = session.activeSubagents.get(block.tool_use_id);
51
- if (postId) {
52
- handleTaskComplete(session, block.tool_use_id, postId);
53
- }
54
- }
55
- }
56
- }
57
- const formatted = formatEvent(session, event, ctx);
58
- if (ctx.debug) {
59
- console.log(`[DEBUG] handleEvent(${session.threadId}): ${event.type} -> ${formatted ? formatted.substring(0, 100) : '(null)'}`);
60
- }
61
- if (formatted)
62
- ctx.appendContent(session, formatted);
63
- }
64
- // ---------------------------------------------------------------------------
65
- // Event formatters
66
- // ---------------------------------------------------------------------------
67
- /**
68
- * Format a Claude event for display in chat platforms.
69
- */
70
- function formatEvent(session, e, ctx) {
71
- switch (e.type) {
72
- case 'assistant': {
73
- const msg = e.message;
74
- const parts = [];
75
- for (const block of msg?.content || []) {
76
- if (block.type === 'text' && block.text) {
77
- // Filter out <thinking> tags that may appear in text content
78
- const text = block.text.replace(/<thinking>[\s\S]*?<\/thinking>/g, '').trim();
79
- if (text)
80
- parts.push(text);
81
- }
82
- else if (block.type === 'tool_use' && block.name) {
83
- const formatted = sharedFormatToolUse(block.name, block.input || {}, session.platform.getFormatter(), { detailed: true });
84
- if (formatted)
85
- parts.push(formatted);
86
- }
87
- else if (block.type === 'thinking' && block.thinking) {
88
- // Extended thinking - show abbreviated version
89
- const thinking = block.thinking;
90
- const preview = thinking.length > 100 ? thinking.substring(0, 100) + '...' : thinking;
91
- parts.push(`💭 *Thinking: ${preview}*`);
92
- }
93
- else if (block.type === 'server_tool_use' && block.name) {
94
- // Server-managed tools like web search
95
- parts.push(`🌐 **${block.name}** ${block.input ? JSON.stringify(block.input).substring(0, 50) : ''}`);
96
- }
97
- }
98
- return parts.length > 0 ? parts.join('\n') : null;
99
- }
100
- case 'tool_use': {
101
- const tool = e.tool_use;
102
- // Track tool start time for elapsed display
103
- if (tool.id) {
104
- session.activeToolStarts.set(tool.id, Date.now());
105
- }
106
- return sharedFormatToolUse(tool.name, tool.input || {}, session.platform.getFormatter(), { detailed: true }) || null;
107
- }
108
- case 'tool_result': {
109
- const result = e.tool_result;
110
- // Calculate elapsed time
111
- let elapsed = '';
112
- if (result.tool_use_id) {
113
- const startTime = session.activeToolStarts.get(result.tool_use_id);
114
- if (startTime) {
115
- const secs = Math.round((Date.now() - startTime) / 1000);
116
- if (secs >= 3) {
117
- // Only show if >= 3 seconds
118
- elapsed = ` (${secs}s)`;
119
- }
120
- session.activeToolStarts.delete(result.tool_use_id);
121
- }
122
- }
123
- if (result.is_error)
124
- return ` ↳ ❌ Error${elapsed}`;
125
- if (elapsed)
126
- return ` ↳ ✓${elapsed}`;
127
- return null;
128
- }
129
- case 'result': {
130
- // Response complete - stop typing and start new post for next message
131
- ctx.stopTyping(session);
132
- ctx.flush(session);
133
- session.currentPostId = null;
134
- session.pendingContent = '';
135
- return null;
136
- }
137
- case 'system':
138
- if (e.subtype === 'error')
139
- return `❌ ${e.error}`;
140
- return null;
141
- case 'user': {
142
- // Handle local command output (e.g., /context, /cost responses)
143
- const msg = e.message;
144
- if (typeof msg?.content === 'string') {
145
- // Extract content from <local-command-stdout> tags
146
- const match = msg.content.match(/<local-command-stdout>([\s\S]*?)<\/local-command-stdout>/);
147
- if (match) {
148
- return match[1].trim();
149
- }
150
- }
151
- return null;
152
- }
153
- default:
154
- return null;
155
- }
156
- }
157
- // ---------------------------------------------------------------------------
158
- // Plan mode handling
159
- // ---------------------------------------------------------------------------
160
- /**
161
- * Handle ExitPlanMode tool use - post approval prompt.
162
- */
163
- async function handleExitPlanMode(session, toolUseId, ctx) {
164
- // If already approved in this session, send empty tool result to acknowledge
165
- if (session.planApproved) {
166
- if (ctx.debug)
167
- console.log(' ↪ Plan already approved, sending acknowledgment');
168
- if (session.claude.isRunning()) {
169
- session.claude.sendToolResult(toolUseId, 'Plan already approved. Proceeding.');
170
- }
171
- return;
172
- }
173
- // If we already have a pending approval, don't post another one
174
- if (session.pendingApproval && session.pendingApproval.type === 'plan') {
175
- if (ctx.debug)
176
- console.log(' ↪ Plan approval already pending, waiting');
177
- return;
178
- }
179
- // Flush any pending content first
180
- await ctx.flush(session);
181
- session.currentPostId = null;
182
- session.pendingContent = '';
183
- // Post approval message with reactions
184
- const message = `✅ **Plan ready for approval**\n\n` +
185
- `👍 Approve and start building\n` +
186
- `👎 Request changes\n\n` +
187
- `*React to respond*`;
188
- const post = await session.platform.createInteractivePost(message, [APPROVAL_EMOJIS[0], DENIAL_EMOJIS[0]], session.threadId);
189
- // Register post for reaction routing
190
- ctx.registerPost(post.id, session.threadId);
191
- // Track this for reaction handling - include toolUseId for proper response
192
- session.pendingApproval = { postId: post.id, type: 'plan', toolUseId };
193
- // Stop typing while waiting
194
- ctx.stopTyping(session);
195
- }
196
- // ---------------------------------------------------------------------------
197
- // Task/Todo handling
198
- // ---------------------------------------------------------------------------
199
- /**
200
- * Handle TodoWrite tool use - update task list display.
201
- */
202
- async function handleTodoWrite(session, input) {
203
- const todos = input.todos;
204
- if (!todos || todos.length === 0) {
205
- // Clear tasks display if empty
206
- if (session.tasksPostId) {
207
- try {
208
- await session.platform.updatePost(session.tasksPostId, '📋 ~~Tasks~~ *(completed)*');
209
- }
210
- catch (err) {
211
- console.error(' ⚠️ Failed to update tasks:', err);
212
- }
213
- }
214
- return;
215
- }
216
- // Count progress
217
- const completed = todos.filter((t) => t.status === 'completed').length;
218
- const total = todos.length;
219
- const pct = Math.round((completed / total) * 100);
220
- // Check if there's an in_progress task and track timing
221
- const hasInProgress = todos.some((t) => t.status === 'in_progress');
222
- if (hasInProgress && !session.inProgressTaskStart) {
223
- session.inProgressTaskStart = Date.now();
224
- }
225
- else if (!hasInProgress) {
226
- session.inProgressTaskStart = null;
227
- }
228
- // Format tasks nicely with progress header
229
- let message = `📋 **Tasks** (${completed}/${total} · ${pct}%)\n\n`;
230
- for (const todo of todos) {
231
- let icon;
232
- let text;
233
- switch (todo.status) {
234
- case 'completed':
235
- icon = '✅';
236
- text = `~~${todo.content}~~`;
237
- break;
238
- case 'in_progress': {
239
- icon = '🔄';
240
- // Add elapsed time if we have a start time
241
- let elapsed = '';
242
- if (session.inProgressTaskStart) {
243
- const secs = Math.round((Date.now() - session.inProgressTaskStart) / 1000);
244
- if (secs >= 5) {
245
- // Only show if >= 5 seconds
246
- elapsed = ` (${secs}s)`;
247
- }
248
- }
249
- text = `**${todo.activeForm}**${elapsed}`;
250
- break;
251
- }
252
- default:
253
- // pending
254
- icon = '○';
255
- text = todo.content;
256
- }
257
- message += `${icon} ${text}\n`;
258
- }
259
- // Update or create tasks post
260
- try {
261
- if (session.tasksPostId) {
262
- await session.platform.updatePost(session.tasksPostId, message);
263
- }
264
- else {
265
- const post = await session.platform.createPost(message, session.threadId);
266
- session.tasksPostId = post.id;
267
- }
268
- }
269
- catch (err) {
270
- console.error(' ⚠️ Failed to update tasks:', err);
271
- }
272
- }
273
- /**
274
- * Handle Task (subagent) start - post status message.
275
- */
276
- async function handleTaskStart(session, toolUseId, input) {
277
- const description = input.description || 'Working...';
278
- const subagentType = input.subagent_type || 'general';
279
- // Post subagent status
280
- const message = `🤖 **Subagent** *(${subagentType})*\n` + `> ${description}\n` + `⏳ Running...`;
281
- try {
282
- const post = await session.platform.createPost(message, session.threadId);
283
- session.activeSubagents.set(toolUseId, post.id);
284
- }
285
- catch (err) {
286
- console.error(' ⚠️ Failed to post subagent status:', err);
287
- }
288
- }
289
- /**
290
- * Handle Task (subagent) completion - update status message.
291
- */
292
- async function handleTaskComplete(session, toolUseId, postId) {
293
- try {
294
- await session.platform.updatePost(postId, session.activeSubagents.has(toolUseId)
295
- ? `🤖 **Subagent** ✅ *completed*`
296
- : `🤖 **Subagent** ✅`);
297
- session.activeSubagents.delete(toolUseId);
298
- }
299
- catch (err) {
300
- console.error(' ⚠️ Failed to update subagent completion:', err);
301
- }
302
- }
303
- // ---------------------------------------------------------------------------
304
- // Question handling
305
- // ---------------------------------------------------------------------------
306
- /**
307
- * Handle AskUserQuestion tool use - start interactive question flow.
308
- */
309
- async function handleAskUserQuestion(session, toolUseId, input, ctx) {
310
- // If we already have pending questions, don't start another set
311
- if (session.pendingQuestionSet) {
312
- if (ctx.debug)
313
- console.log(' ↪ Questions already pending, waiting');
314
- return;
315
- }
316
- // Flush any pending content first
317
- await ctx.flush(session);
318
- session.currentPostId = null;
319
- session.pendingContent = '';
320
- const questions = input.questions;
321
- if (!questions || questions.length === 0)
322
- return;
323
- // Create a new question set - we'll ask one at a time
324
- session.pendingQuestionSet = {
325
- toolUseId,
326
- currentIndex: 0,
327
- currentPostId: null,
328
- questions: questions.map((q) => ({
329
- header: q.header,
330
- question: q.question,
331
- options: q.options,
332
- answer: null,
333
- })),
334
- };
335
- // Post the first question
336
- await postCurrentQuestion(session, ctx);
337
- // Stop typing while waiting for answer
338
- ctx.stopTyping(session);
339
- }
340
- /**
341
- * Post the current question in the question set.
342
- */
343
- export async function postCurrentQuestion(session, ctx) {
344
- if (!session.pendingQuestionSet)
345
- return;
346
- const { currentIndex, questions } = session.pendingQuestionSet;
347
- if (currentIndex >= questions.length)
348
- return;
349
- const q = questions[currentIndex];
350
- const total = questions.length;
351
- // Format the question message
352
- let message = `❓ **Question** *(${currentIndex + 1}/${total})*\n`;
353
- message += `**${q.header}:** ${q.question}\n\n`;
354
- for (let i = 0; i < q.options.length && i < 4; i++) {
355
- const emoji = ['1️⃣', '2️⃣', '3️⃣', '4️⃣'][i];
356
- message += `${emoji} **${q.options[i].label}**`;
357
- if (q.options[i].description) {
358
- message += ` - ${q.options[i].description}`;
359
- }
360
- message += '\n';
361
- }
362
- // Post the question with reaction options
363
- const reactionOptions = NUMBER_EMOJIS.slice(0, q.options.length);
364
- const post = await session.platform.createInteractivePost(message, reactionOptions, session.threadId);
365
- session.pendingQuestionSet.currentPostId = post.id;
366
- // Register post for reaction routing
367
- ctx.registerPost(post.id, session.threadId);
368
- }
@@ -1,7 +0,0 @@
1
- /**
2
- * Session module public exports
3
- *
4
- * Provides SessionManager for managing multiple concurrent Claude Code sessions.
5
- */
6
- export { SessionManager } from './manager.js';
7
- export type { Session, PendingApproval, PendingQuestionSet, PendingMessageApproval, } from './types.js';
@@ -1,6 +0,0 @@
1
- /**
2
- * Session module public exports
3
- *
4
- * Provides SessionManager for managing multiple concurrent Claude Code sessions.
5
- */
6
- export { SessionManager } from './manager.js';
@@ -1,70 +0,0 @@
1
- /**
2
- * Session lifecycle management module
3
- *
4
- * Handles session start, resume, exit, cleanup, and shutdown.
5
- */
6
- import type { Session } from './types.js';
7
- import type { PlatformClient, PlatformFile } from '../platform/index.js';
8
- import type { ClaudeEvent, ContentBlock } from '../claude/cli.js';
9
- import type { PersistedSession, SessionStore } from '../persistence/session-store.js';
10
- export interface LifecycleContext {
11
- workingDir: string;
12
- skipPermissions: boolean;
13
- chromeEnabled: boolean;
14
- debug: boolean;
15
- maxSessions: number;
16
- sessions: Map<string, Session>;
17
- postIndex: Map<string, string>;
18
- platforms: Map<string, PlatformClient>;
19
- sessionStore: SessionStore;
20
- isShuttingDown: boolean;
21
- getSessionId: (platformId: string, threadId: string) => string;
22
- findSessionByThreadId: (threadId: string) => Session | undefined;
23
- handleEvent: (sessionId: string, event: ClaudeEvent) => void;
24
- handleExit: (sessionId: string, code: number) => Promise<void>;
25
- registerPost: (postId: string, threadId: string) => void;
26
- startTyping: (session: Session) => void;
27
- stopTyping: (session: Session) => void;
28
- flush: (session: Session) => Promise<void>;
29
- persistSession: (session: Session) => void;
30
- unpersistSession: (sessionId: string) => void;
31
- updateSessionHeader: (session: Session) => Promise<void>;
32
- shouldPromptForWorktree: (session: Session) => Promise<string | null>;
33
- postWorktreePrompt: (session: Session, reason: string) => Promise<void>;
34
- buildMessageContent: (text: string, platform: PlatformClient, files?: PlatformFile[]) => Promise<string | ContentBlock[]>;
35
- }
36
- /**
37
- * Create a new session for a thread.
38
- */
39
- export declare function startSession(options: {
40
- prompt: string;
41
- files?: PlatformFile[];
42
- }, username: string, replyToPostId: string | undefined, platformId: string, ctx: LifecycleContext): Promise<void>;
43
- /**
44
- * Resume a session from persisted state.
45
- */
46
- export declare function resumeSession(state: PersistedSession, ctx: LifecycleContext): Promise<void>;
47
- /**
48
- * Send a follow-up message to an existing session.
49
- */
50
- export declare function sendFollowUp(session: Session, message: string, files: PlatformFile[] | undefined, ctx: LifecycleContext): Promise<void>;
51
- /**
52
- * Resume a paused session and send a message to it.
53
- */
54
- export declare function resumePausedSession(threadId: string, message: string, files: PlatformFile[] | undefined, ctx: LifecycleContext): Promise<void>;
55
- /**
56
- * Handle Claude CLI exit event.
57
- */
58
- export declare function handleExit(sessionId: string, code: number, ctx: LifecycleContext): Promise<void>;
59
- /**
60
- * Kill a specific session.
61
- */
62
- export declare function killSession(session: Session, unpersist: boolean, ctx: LifecycleContext): void;
63
- /**
64
- * Kill all active sessions.
65
- */
66
- export declare function killAllSessions(ctx: LifecycleContext): void;
67
- /**
68
- * Clean up idle sessions that have timed out.
69
- */
70
- export declare function cleanupIdleSessions(timeoutMs: number, warningMs: number, ctx: LifecycleContext): void;