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,339 +0,0 @@
1
- /**
2
- * Git worktree management utilities
3
- *
4
- * Handles worktree prompts, creation, switching, and cleanup.
5
- */
6
- import { isGitRepository, getRepositoryRoot, hasUncommittedChanges, listWorktrees as listGitWorktrees, createWorktree as createGitWorktree, removeWorktree as removeGitWorktree, getWorktreeDir, findWorktreeByBranch, isValidBranchName, } from '../git/worktree.js';
7
- import { ClaudeCli } from '../claude/cli.js';
8
- import { randomUUID } from 'crypto';
9
- /**
10
- * Check if we should prompt the user to create a worktree.
11
- * Returns the reason for prompting, or null if we shouldn't prompt.
12
- */
13
- export async function shouldPromptForWorktree(session, worktreeMode, hasOtherSessionInRepo) {
14
- // Skip if worktree mode is off
15
- if (worktreeMode === 'off')
16
- return null;
17
- // Skip if user disabled prompts for this session
18
- if (session.worktreePromptDisabled)
19
- return null;
20
- // Skip if already in a worktree
21
- if (session.worktreeInfo)
22
- return null;
23
- // Check if we're in a git repository
24
- const isRepo = await isGitRepository(session.workingDir);
25
- if (!isRepo)
26
- return null;
27
- // For 'require' mode, always prompt
28
- if (worktreeMode === 'require') {
29
- return 'require';
30
- }
31
- // For 'prompt' mode, check conditions
32
- // Condition 1: uncommitted changes
33
- const hasChanges = await hasUncommittedChanges(session.workingDir);
34
- if (hasChanges)
35
- return 'uncommitted';
36
- // Condition 2: another session using the same repo
37
- const repoRoot = await getRepositoryRoot(session.workingDir);
38
- const hasConcurrent = hasOtherSessionInRepo(repoRoot, session.threadId);
39
- if (hasConcurrent)
40
- return 'concurrent';
41
- return null;
42
- }
43
- /**
44
- * Post the worktree prompt message to the user.
45
- */
46
- export async function postWorktreePrompt(session, reason, registerPost) {
47
- let message;
48
- switch (reason) {
49
- case 'uncommitted':
50
- message = `🌿 **This repo has uncommitted changes.**\n` +
51
- `Reply with a branch name to work in an isolated worktree, or react with ❌ to continue in the main repo.`;
52
- break;
53
- case 'concurrent':
54
- message = `⚠️ **Another session is already using this repo.**\n` +
55
- `Reply with a branch name to work in an isolated worktree, or react with ❌ to continue anyway.`;
56
- break;
57
- case 'require':
58
- message = `🌿 **This deployment requires working in a worktree.**\n` +
59
- `Please reply with a branch name to continue.`;
60
- break;
61
- default:
62
- message = `🌿 **Would you like to work in an isolated worktree?**\n` +
63
- `Reply with a branch name, or react with ❌ to continue in the main repo.`;
64
- }
65
- // Create post with ❌ reaction option (except for 'require' mode)
66
- // Use 'x' emoji name, not Unicode ❌ character
67
- const reactionOptions = reason === 'require' ? [] : ['x'];
68
- const post = await session.platform.createInteractivePost(message, reactionOptions, session.threadId);
69
- // Track the post for reaction handling
70
- session.worktreePromptPostId = post.id;
71
- registerPost(post.id, session.threadId);
72
- }
73
- /**
74
- * Handle user providing a branch name in response to worktree prompt.
75
- * Returns true if handled (whether successful or not).
76
- */
77
- export async function handleWorktreeBranchResponse(session, branchName, username, createAndSwitch) {
78
- if (!session.pendingWorktreePrompt)
79
- return false;
80
- // Only session owner can respond
81
- if (session.startedBy !== username && !session.platform.isUserAllowed(username)) {
82
- return false;
83
- }
84
- // Validate branch name
85
- if (!isValidBranchName(branchName)) {
86
- await session.platform.createPost(`❌ Invalid branch name: \`${branchName}\`. Please provide a valid git branch name.`, session.threadId);
87
- return true; // We handled it, but need another response
88
- }
89
- // Create and switch to worktree
90
- await createAndSwitch(session.threadId, branchName, username);
91
- return true;
92
- }
93
- /**
94
- * Handle ❌ reaction on worktree prompt - skip worktree and continue in main repo.
95
- */
96
- export async function handleWorktreeSkip(session, username, persistSession, startTyping) {
97
- if (!session.pendingWorktreePrompt)
98
- return;
99
- // Only session owner can skip
100
- if (session.startedBy !== username && !session.platform.isUserAllowed(username)) {
101
- return;
102
- }
103
- // Update the prompt post
104
- if (session.worktreePromptPostId) {
105
- try {
106
- await session.platform.updatePost(session.worktreePromptPostId, `✅ Continuing in main repo (skipped by @${username})`);
107
- }
108
- catch (err) {
109
- console.error(' ⚠️ Failed to update worktree prompt:', err);
110
- }
111
- }
112
- // Clear pending state
113
- session.pendingWorktreePrompt = false;
114
- session.worktreePromptPostId = undefined;
115
- const queuedPrompt = session.queuedPrompt;
116
- session.queuedPrompt = undefined;
117
- // Persist updated state
118
- persistSession(session);
119
- // Now send the queued message to Claude
120
- if (queuedPrompt && session.claude.isRunning()) {
121
- session.claude.sendMessage(queuedPrompt);
122
- startTyping(session);
123
- }
124
- }
125
- /**
126
- * Create a new worktree and switch the session to it.
127
- */
128
- export async function createAndSwitchToWorktree(session, branch, username, options) {
129
- // Only session owner or admins can manage worktrees
130
- if (session.startedBy !== username && !session.platform.isUserAllowed(username)) {
131
- await session.platform.createPost(`⚠️ Only @${session.startedBy} or allowed users can manage worktrees`, session.threadId);
132
- return;
133
- }
134
- // Check if we're in a git repo
135
- const isRepo = await isGitRepository(session.workingDir);
136
- if (!isRepo) {
137
- await session.platform.createPost(`❌ Current directory is not a git repository`, session.threadId);
138
- return;
139
- }
140
- // Get repo root
141
- const repoRoot = await getRepositoryRoot(session.workingDir);
142
- // Check if worktree already exists for this branch
143
- const existing = await findWorktreeByBranch(repoRoot, branch);
144
- if (existing && !existing.isMain) {
145
- await session.platform.createPost(`⚠️ Worktree for branch \`${branch}\` already exists at \`${existing.path}\`. Use \`!worktree switch ${branch}\` to switch to it.`, session.threadId);
146
- return;
147
- }
148
- const shortId = session.threadId.substring(0, 8);
149
- console.log(` 🌿 Session (${shortId}…) creating worktree for branch ${branch}`);
150
- // Generate worktree path
151
- const worktreePath = getWorktreeDir(repoRoot, branch);
152
- try {
153
- // Create the worktree
154
- await createGitWorktree(repoRoot, branch, worktreePath);
155
- // Update the prompt post if it exists
156
- if (session.worktreePromptPostId) {
157
- try {
158
- await session.platform.updatePost(session.worktreePromptPostId, `✅ Created worktree for \`${branch}\``);
159
- }
160
- catch (err) {
161
- console.error(' ⚠️ Failed to update worktree prompt:', err);
162
- }
163
- }
164
- // Clear pending state
165
- const wasPending = session.pendingWorktreePrompt;
166
- session.pendingWorktreePrompt = false;
167
- session.worktreePromptPostId = undefined;
168
- const queuedPrompt = session.queuedPrompt;
169
- session.queuedPrompt = undefined;
170
- // Store worktree info
171
- session.worktreeInfo = {
172
- repoRoot,
173
- worktreePath,
174
- branch,
175
- };
176
- // Update working directory
177
- session.workingDir = worktreePath;
178
- // If Claude is already running, restart it in the new directory
179
- if (session.claude.isRunning()) {
180
- options.stopTyping(session);
181
- session.isRestarting = true;
182
- session.claude.kill();
183
- // Flush any pending content
184
- await options.flush(session);
185
- session.currentPostId = null;
186
- session.pendingContent = '';
187
- // Generate new session ID for fresh start in new directory
188
- // (Claude CLI sessions are tied to working directory, can't resume across directories)
189
- const newSessionId = randomUUID();
190
- session.claudeSessionId = newSessionId;
191
- // Create new CLI with new working directory
192
- const cliOptions = {
193
- workingDir: worktreePath,
194
- threadId: session.threadId,
195
- skipPermissions: options.skipPermissions || !session.forceInteractivePermissions,
196
- sessionId: newSessionId,
197
- resume: false, // Fresh start - can't resume across directories
198
- chrome: options.chromeEnabled,
199
- platformConfig: session.platform.getMcpConfig(),
200
- };
201
- session.claude = new ClaudeCli(cliOptions);
202
- // Rebind event handlers (use sessionId which is the composite key)
203
- session.claude.on('event', (e) => options.handleEvent(session.sessionId, e));
204
- session.claude.on('exit', (code) => options.handleExit(session.sessionId, code));
205
- // Start the new CLI
206
- session.claude.start();
207
- }
208
- // Update session header
209
- await options.updateSessionHeader(session);
210
- // Post confirmation
211
- const shortWorktreePath = worktreePath.replace(process.env.HOME || '', '~');
212
- await session.platform.createPost(`✅ **Created worktree** for branch \`${branch}\`\n📁 Working directory: \`${shortWorktreePath}\`\n*Claude Code restarted in the new worktree*`, session.threadId);
213
- // Update activity and persist
214
- session.lastActivityAt = new Date();
215
- session.timeoutWarningPosted = false;
216
- options.persistSession(session);
217
- // If there was a queued prompt (from initial session start), send it now
218
- if (wasPending && queuedPrompt && session.claude.isRunning()) {
219
- session.claude.sendMessage(queuedPrompt);
220
- options.startTyping(session);
221
- }
222
- console.log(` 🌿 Session (${shortId}…) switched to worktree ${branch} at ${shortWorktreePath}`);
223
- }
224
- catch (err) {
225
- console.error(` ❌ Failed to create worktree:`, err);
226
- await session.platform.createPost(`❌ Failed to create worktree: ${err instanceof Error ? err.message : String(err)}`, session.threadId);
227
- }
228
- }
229
- /**
230
- * Switch to an existing worktree.
231
- */
232
- export async function switchToWorktree(session, branchOrPath, username, changeDirectory) {
233
- // Only session owner or admins can manage worktrees
234
- if (session.startedBy !== username && !session.platform.isUserAllowed(username)) {
235
- await session.platform.createPost(`⚠️ Only @${session.startedBy} or allowed users can manage worktrees`, session.threadId);
236
- return;
237
- }
238
- // Get current repo root
239
- const repoRoot = session.worktreeInfo?.repoRoot || await getRepositoryRoot(session.workingDir);
240
- // Find the worktree
241
- const worktrees = await listGitWorktrees(repoRoot);
242
- const target = worktrees.find(wt => wt.branch === branchOrPath ||
243
- wt.path === branchOrPath ||
244
- wt.path.endsWith(branchOrPath));
245
- if (!target) {
246
- await session.platform.createPost(`❌ Worktree not found: \`${branchOrPath}\`. Use \`!worktree list\` to see available worktrees.`, session.threadId);
247
- return;
248
- }
249
- // Use changeDirectory logic to switch
250
- await changeDirectory(session.threadId, target.path, username);
251
- // Update worktree info
252
- session.worktreeInfo = {
253
- repoRoot,
254
- worktreePath: target.path,
255
- branch: target.branch,
256
- };
257
- }
258
- /**
259
- * List all worktrees for the current repository.
260
- */
261
- export async function listWorktreesCommand(session) {
262
- // Check if we're in a git repo
263
- const isRepo = await isGitRepository(session.workingDir);
264
- if (!isRepo) {
265
- await session.platform.createPost(`❌ Current directory is not a git repository`, session.threadId);
266
- return;
267
- }
268
- // Get repo root (either from worktree info or current dir)
269
- const repoRoot = session.worktreeInfo?.repoRoot || await getRepositoryRoot(session.workingDir);
270
- const worktrees = await listGitWorktrees(repoRoot);
271
- if (worktrees.length === 0) {
272
- await session.platform.createPost(`📋 No worktrees found for this repository`, session.threadId);
273
- return;
274
- }
275
- const shortRepoRoot = repoRoot.replace(process.env.HOME || '', '~');
276
- let message = `📋 **Worktrees for** \`${shortRepoRoot}\`:\n\n`;
277
- for (const wt of worktrees) {
278
- const shortPath = wt.path.replace(process.env.HOME || '', '~');
279
- const isCurrent = session.workingDir === wt.path;
280
- const marker = isCurrent ? ' ← current' : '';
281
- const label = wt.isMain ? '(main repository)' : '';
282
- message += `• \`${wt.branch}\` → \`${shortPath}\` ${label}${marker}\n`;
283
- }
284
- await session.platform.createPost(message, session.threadId);
285
- }
286
- /**
287
- * Remove a worktree.
288
- */
289
- export async function removeWorktreeCommand(session, branchOrPath, username) {
290
- // Only session owner or admins can manage worktrees
291
- if (session.startedBy !== username && !session.platform.isUserAllowed(username)) {
292
- await session.platform.createPost(`⚠️ Only @${session.startedBy} or allowed users can manage worktrees`, session.threadId);
293
- return;
294
- }
295
- // Get current repo root
296
- const repoRoot = session.worktreeInfo?.repoRoot || await getRepositoryRoot(session.workingDir);
297
- // Find the worktree
298
- const worktrees = await listGitWorktrees(repoRoot);
299
- const target = worktrees.find(wt => wt.branch === branchOrPath ||
300
- wt.path === branchOrPath ||
301
- wt.path.endsWith(branchOrPath));
302
- if (!target) {
303
- await session.platform.createPost(`❌ Worktree not found: \`${branchOrPath}\`. Use \`!worktree list\` to see available worktrees.`, session.threadId);
304
- return;
305
- }
306
- // Can't remove the main repository
307
- if (target.isMain) {
308
- await session.platform.createPost(`❌ Cannot remove the main repository. Use \`!worktree remove\` only for worktrees.`, session.threadId);
309
- return;
310
- }
311
- // Can't remove the current working directory
312
- if (session.workingDir === target.path) {
313
- await session.platform.createPost(`❌ Cannot remove the current working directory. Switch to another worktree first.`, session.threadId);
314
- return;
315
- }
316
- try {
317
- await removeGitWorktree(repoRoot, target.path);
318
- const shortPath = target.path.replace(process.env.HOME || '', '~');
319
- await session.platform.createPost(`✅ Removed worktree \`${target.branch}\` at \`${shortPath}\``, session.threadId);
320
- console.log(` 🗑️ Removed worktree ${target.branch} at ${shortPath}`);
321
- }
322
- catch (err) {
323
- console.error(` ❌ Failed to remove worktree:`, err);
324
- await session.platform.createPost(`❌ Failed to remove worktree: ${err instanceof Error ? err.message : String(err)}`, session.threadId);
325
- }
326
- }
327
- /**
328
- * Disable worktree prompts for a session.
329
- */
330
- export async function disableWorktreePrompt(session, username, persistSession) {
331
- // Only session owner or admins can manage worktrees
332
- if (session.startedBy !== username && !session.platform.isUserAllowed(username)) {
333
- await session.platform.createPost(`⚠️ Only @${session.startedBy} or allowed users can manage worktrees`, session.threadId);
334
- return;
335
- }
336
- session.worktreePromptDisabled = true;
337
- persistSession(session);
338
- await session.platform.createPost(`✅ Worktree prompts disabled for this session`, session.threadId);
339
- }
@@ -1,3 +0,0 @@
1
- import { type UpdateInfo } from 'update-notifier';
2
- export declare function checkForUpdates(): void;
3
- export declare function getUpdateInfo(): UpdateInfo | undefined;
@@ -1,41 +0,0 @@
1
- import updateNotifier from 'update-notifier';
2
- import { readFileSync } from 'fs';
3
- import { resolve } from 'path';
4
- import { fileURLToPath } from 'url';
5
- import semver from 'semver';
6
- const __dirname = fileURLToPath(new URL('.', import.meta.url));
7
- let cachedUpdateInfo;
8
- export function checkForUpdates() {
9
- if (process.env.NO_UPDATE_NOTIFIER)
10
- return;
11
- try {
12
- const pkg = JSON.parse(readFileSync(resolve(__dirname, '..', 'package.json'), 'utf-8'));
13
- const notifier = updateNotifier({
14
- pkg,
15
- updateCheckInterval: 1000 * 60 * 30, // Check every 30 minutes
16
- });
17
- // Cache for Mattermost notifications
18
- cachedUpdateInfo = notifier.update;
19
- // Show CLI notification
20
- notifier.notify({
21
- message: `Update available: {currentVersion} → {latestVersion}
22
- Run: npm install -g claude-threads`,
23
- });
24
- }
25
- catch {
26
- // Silently fail - update checking is not critical
27
- }
28
- }
29
- // Returns update info if available, for posting to Mattermost
30
- // Only returns if latest > current (handles stale cache edge case)
31
- export function getUpdateInfo() {
32
- if (!cachedUpdateInfo)
33
- return undefined;
34
- // Sanity check: only show update if latest is actually newer
35
- const current = cachedUpdateInfo.current;
36
- const latest = cachedUpdateInfo.latest;
37
- if (current && latest && semver.gte(current, latest)) {
38
- return undefined; // Current is same or newer, no update needed
39
- }
40
- return cachedUpdateInfo;
41
- }
@@ -1,43 +0,0 @@
1
- /**
2
- * Emoji constants and helpers for chat platform reactions
3
- *
4
- * Platform-agnostic emoji utilities used across session management,
5
- * permission handling, and user interactions.
6
- */
7
- /** Emoji names that indicate approval */
8
- export declare const APPROVAL_EMOJIS: readonly ["+1", "thumbsup"];
9
- /** Emoji names that indicate denial */
10
- export declare const DENIAL_EMOJIS: readonly ["-1", "thumbsdown"];
11
- /** Emoji names that indicate "allow all" / invite / session-wide approval */
12
- export declare const ALLOW_ALL_EMOJIS: readonly ["white_check_mark", "heavy_check_mark"];
13
- /** Number emojis for multi-choice questions (1-4) */
14
- export declare const NUMBER_EMOJIS: readonly ["one", "two", "three", "four"];
15
- /** Emojis for canceling/killing a session */
16
- export declare const CANCEL_EMOJIS: readonly ["x", "octagonal_sign", "stop_sign"];
17
- /** Emojis for escaping/pausing a session */
18
- export declare const ESCAPE_EMOJIS: readonly ["double_vertical_bar", "pause_button"];
19
- /**
20
- * Check if the emoji indicates approval (thumbs up)
21
- */
22
- export declare function isApprovalEmoji(emoji: string): boolean;
23
- /**
24
- * Check if the emoji indicates denial (thumbs down)
25
- */
26
- export declare function isDenialEmoji(emoji: string): boolean;
27
- /**
28
- * Check if the emoji indicates "allow all" or invitation
29
- */
30
- export declare function isAllowAllEmoji(emoji: string): boolean;
31
- /**
32
- * Check if the emoji indicates session cancellation
33
- */
34
- export declare function isCancelEmoji(emoji: string): boolean;
35
- /**
36
- * Check if the emoji indicates escape/pause
37
- */
38
- export declare function isEscapeEmoji(emoji: string): boolean;
39
- /**
40
- * Get the index (0-based) for a number emoji, or -1 if not a number emoji
41
- * Handles both text names ('one', 'two') and unicode variants ('1️⃣', '2️⃣')
42
- */
43
- export declare function getNumberEmojiIndex(emoji: string): number;
@@ -1,65 +0,0 @@
1
- /**
2
- * Emoji constants and helpers for chat platform reactions
3
- *
4
- * Platform-agnostic emoji utilities used across session management,
5
- * permission handling, and user interactions.
6
- */
7
- /** Emoji names that indicate approval */
8
- export const APPROVAL_EMOJIS = ['+1', 'thumbsup'];
9
- /** Emoji names that indicate denial */
10
- export const DENIAL_EMOJIS = ['-1', 'thumbsdown'];
11
- /** Emoji names that indicate "allow all" / invite / session-wide approval */
12
- export const ALLOW_ALL_EMOJIS = ['white_check_mark', 'heavy_check_mark'];
13
- /** Number emojis for multi-choice questions (1-4) */
14
- export const NUMBER_EMOJIS = ['one', 'two', 'three', 'four'];
15
- /** Emojis for canceling/killing a session */
16
- export const CANCEL_EMOJIS = ['x', 'octagonal_sign', 'stop_sign'];
17
- /** Emojis for escaping/pausing a session */
18
- export const ESCAPE_EMOJIS = ['double_vertical_bar', 'pause_button'];
19
- /**
20
- * Check if the emoji indicates approval (thumbs up)
21
- */
22
- export function isApprovalEmoji(emoji) {
23
- return APPROVAL_EMOJIS.includes(emoji);
24
- }
25
- /**
26
- * Check if the emoji indicates denial (thumbs down)
27
- */
28
- export function isDenialEmoji(emoji) {
29
- return DENIAL_EMOJIS.includes(emoji);
30
- }
31
- /**
32
- * Check if the emoji indicates "allow all" or invitation
33
- */
34
- export function isAllowAllEmoji(emoji) {
35
- return ALLOW_ALL_EMOJIS.includes(emoji);
36
- }
37
- /**
38
- * Check if the emoji indicates session cancellation
39
- */
40
- export function isCancelEmoji(emoji) {
41
- return CANCEL_EMOJIS.includes(emoji);
42
- }
43
- /**
44
- * Check if the emoji indicates escape/pause
45
- */
46
- export function isEscapeEmoji(emoji) {
47
- return ESCAPE_EMOJIS.includes(emoji);
48
- }
49
- /** Unicode number emoji variants that also map to indices */
50
- const UNICODE_NUMBER_EMOJIS = {
51
- '1️⃣': 0,
52
- '2️⃣': 1,
53
- '3️⃣': 2,
54
- '4️⃣': 3,
55
- };
56
- /**
57
- * Get the index (0-based) for a number emoji, or -1 if not a number emoji
58
- * Handles both text names ('one', 'two') and unicode variants ('1️⃣', '2️⃣')
59
- */
60
- export function getNumberEmojiIndex(emoji) {
61
- const textIndex = NUMBER_EMOJIS.indexOf(emoji);
62
- if (textIndex >= 0)
63
- return textIndex;
64
- return UNICODE_NUMBER_EMOJIS[emoji] ?? -1;
65
- }
@@ -1 +0,0 @@
1
- export {};
@@ -1,131 +0,0 @@
1
- import { describe, it, expect } from 'vitest';
2
- import { isApprovalEmoji, isDenialEmoji, isAllowAllEmoji, isCancelEmoji, isEscapeEmoji, getNumberEmojiIndex, APPROVAL_EMOJIS, DENIAL_EMOJIS, ALLOW_ALL_EMOJIS, NUMBER_EMOJIS, CANCEL_EMOJIS, ESCAPE_EMOJIS, } from './emoji.js';
3
- describe('emoji helpers', () => {
4
- describe('isApprovalEmoji', () => {
5
- it('returns true for +1', () => {
6
- expect(isApprovalEmoji('+1')).toBe(true);
7
- });
8
- it('returns true for thumbsup', () => {
9
- expect(isApprovalEmoji('thumbsup')).toBe(true);
10
- });
11
- it('returns false for other emojis', () => {
12
- expect(isApprovalEmoji('heart')).toBe(false);
13
- expect(isApprovalEmoji('-1')).toBe(false);
14
- expect(isApprovalEmoji('x')).toBe(false);
15
- });
16
- it('matches all APPROVAL_EMOJIS', () => {
17
- for (const emoji of APPROVAL_EMOJIS) {
18
- expect(isApprovalEmoji(emoji)).toBe(true);
19
- }
20
- });
21
- });
22
- describe('isDenialEmoji', () => {
23
- it('returns true for -1', () => {
24
- expect(isDenialEmoji('-1')).toBe(true);
25
- });
26
- it('returns true for thumbsdown', () => {
27
- expect(isDenialEmoji('thumbsdown')).toBe(true);
28
- });
29
- it('returns false for other emojis', () => {
30
- expect(isDenialEmoji('heart')).toBe(false);
31
- expect(isDenialEmoji('+1')).toBe(false);
32
- expect(isDenialEmoji('thumbsup')).toBe(false);
33
- });
34
- it('matches all DENIAL_EMOJIS', () => {
35
- for (const emoji of DENIAL_EMOJIS) {
36
- expect(isDenialEmoji(emoji)).toBe(true);
37
- }
38
- });
39
- });
40
- describe('isAllowAllEmoji', () => {
41
- it('returns true for white_check_mark', () => {
42
- expect(isAllowAllEmoji('white_check_mark')).toBe(true);
43
- });
44
- it('returns true for heavy_check_mark', () => {
45
- expect(isAllowAllEmoji('heavy_check_mark')).toBe(true);
46
- });
47
- it('returns false for other emojis', () => {
48
- expect(isAllowAllEmoji('heart')).toBe(false);
49
- expect(isAllowAllEmoji('+1')).toBe(false);
50
- expect(isAllowAllEmoji('thumbsup')).toBe(false);
51
- });
52
- it('matches all ALLOW_ALL_EMOJIS', () => {
53
- for (const emoji of ALLOW_ALL_EMOJIS) {
54
- expect(isAllowAllEmoji(emoji)).toBe(true);
55
- }
56
- });
57
- });
58
- describe('isCancelEmoji', () => {
59
- it('returns true for x', () => {
60
- expect(isCancelEmoji('x')).toBe(true);
61
- });
62
- it('returns true for octagonal_sign', () => {
63
- expect(isCancelEmoji('octagonal_sign')).toBe(true);
64
- });
65
- it('returns true for stop_sign', () => {
66
- expect(isCancelEmoji('stop_sign')).toBe(true);
67
- });
68
- it('returns false for other emojis', () => {
69
- expect(isCancelEmoji('heart')).toBe(false);
70
- expect(isCancelEmoji('-1')).toBe(false);
71
- });
72
- it('matches all CANCEL_EMOJIS', () => {
73
- for (const emoji of CANCEL_EMOJIS) {
74
- expect(isCancelEmoji(emoji)).toBe(true);
75
- }
76
- });
77
- });
78
- describe('isEscapeEmoji', () => {
79
- it('returns true for double_vertical_bar', () => {
80
- expect(isEscapeEmoji('double_vertical_bar')).toBe(true);
81
- });
82
- it('returns true for pause_button', () => {
83
- expect(isEscapeEmoji('pause_button')).toBe(true);
84
- });
85
- it('returns false for other emojis', () => {
86
- expect(isEscapeEmoji('heart')).toBe(false);
87
- expect(isEscapeEmoji('x')).toBe(false);
88
- });
89
- it('matches all ESCAPE_EMOJIS', () => {
90
- for (const emoji of ESCAPE_EMOJIS) {
91
- expect(isEscapeEmoji(emoji)).toBe(true);
92
- }
93
- });
94
- });
95
- describe('getNumberEmojiIndex', () => {
96
- it('returns 0 for "one"', () => {
97
- expect(getNumberEmojiIndex('one')).toBe(0);
98
- });
99
- it('returns 1 for "two"', () => {
100
- expect(getNumberEmojiIndex('two')).toBe(1);
101
- });
102
- it('returns 2 for "three"', () => {
103
- expect(getNumberEmojiIndex('three')).toBe(2);
104
- });
105
- it('returns 3 for "four"', () => {
106
- expect(getNumberEmojiIndex('four')).toBe(3);
107
- });
108
- it('returns 0 for "1️⃣" (unicode)', () => {
109
- expect(getNumberEmojiIndex('1️⃣')).toBe(0);
110
- });
111
- it('returns 1 for "2️⃣" (unicode)', () => {
112
- expect(getNumberEmojiIndex('2️⃣')).toBe(1);
113
- });
114
- it('returns 2 for "3️⃣" (unicode)', () => {
115
- expect(getNumberEmojiIndex('3️⃣')).toBe(2);
116
- });
117
- it('returns 3 for "4️⃣" (unicode)', () => {
118
- expect(getNumberEmojiIndex('4️⃣')).toBe(3);
119
- });
120
- it('returns -1 for non-number emojis', () => {
121
- expect(getNumberEmojiIndex('heart')).toBe(-1);
122
- expect(getNumberEmojiIndex('five')).toBe(-1);
123
- expect(getNumberEmojiIndex('+1')).toBe(-1);
124
- });
125
- it('returns correct index for all NUMBER_EMOJIS', () => {
126
- for (let i = 0; i < NUMBER_EMOJIS.length; i++) {
127
- expect(getNumberEmojiIndex(NUMBER_EMOJIS[i])).toBe(i);
128
- }
129
- });
130
- });
131
- });
@@ -1,34 +0,0 @@
1
- /**
2
- * Simple debug logging utility
3
- *
4
- * Provides consistent logging across the codebase with:
5
- * - Configurable prefix for different components
6
- * - DEBUG environment variable check
7
- * - stdout vs stderr routing option
8
- */
9
- export interface Logger {
10
- /** Log a debug message (only when DEBUG=1) */
11
- debug: (msg: string) => void;
12
- /** Log an info message (always shown) */
13
- info: (msg: string) => void;
14
- /** Log an error message (always shown, to stderr) */
15
- error: (msg: string) => void;
16
- }
17
- /**
18
- * Create a logger with a specific prefix
19
- *
20
- * @param prefix - Prefix to add to all messages (e.g., '[MCP]', '[ws]')
21
- * @param useStderr - If true, use stderr for all output (default: false)
22
- * @returns Logger object with debug, info, and error methods
23
- */
24
- export declare function createLogger(prefix: string, useStderr?: boolean): Logger;
25
- /**
26
- * Pre-configured logger for MCP permission server
27
- * Uses stderr (required for MCP stdio communication)
28
- */
29
- export declare const mcpLogger: Logger;
30
- /**
31
- * Pre-configured logger for WebSocket client
32
- * Uses stdout with indentation for visual hierarchy
33
- */
34
- export declare const wsLogger: Logger;