claude-threads 0.15.0 → 0.16.3

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 +20410 -387
  4. package/dist/mcp/permission-server.js +34038 -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,319 +0,0 @@
1
- import { describe, it, expect, vi, beforeEach, afterEach } from 'vitest';
2
- import { mattermostApi, getMe, getUser, createPost, updatePost, addReaction, isUserAllowed, createInteractivePost, } from './api.js';
3
- const mockConfig = {
4
- url: 'https://mattermost.example.com',
5
- token: 'test-token',
6
- };
7
- describe('mattermostApi', () => {
8
- beforeEach(() => {
9
- vi.stubGlobal('fetch', vi.fn());
10
- });
11
- afterEach(() => {
12
- vi.unstubAllGlobals();
13
- });
14
- it('adds authorization header', async () => {
15
- const mockFetch = vi.fn().mockResolvedValue({
16
- ok: true,
17
- json: () => Promise.resolve({ id: '123' }),
18
- });
19
- vi.stubGlobal('fetch', mockFetch);
20
- await mattermostApi(mockConfig, 'GET', '/users/me');
21
- expect(mockFetch).toHaveBeenCalledWith('https://mattermost.example.com/api/v4/users/me', expect.objectContaining({
22
- headers: expect.objectContaining({
23
- Authorization: 'Bearer test-token',
24
- }),
25
- }));
26
- });
27
- it('includes Content-Type header', async () => {
28
- const mockFetch = vi.fn().mockResolvedValue({
29
- ok: true,
30
- json: () => Promise.resolve({}),
31
- });
32
- vi.stubGlobal('fetch', mockFetch);
33
- await mattermostApi(mockConfig, 'POST', '/posts', { message: 'test' });
34
- expect(mockFetch).toHaveBeenCalledWith(expect.any(String), expect.objectContaining({
35
- headers: expect.objectContaining({
36
- 'Content-Type': 'application/json',
37
- }),
38
- }));
39
- });
40
- it('stringifies body for POST requests', async () => {
41
- const mockFetch = vi.fn().mockResolvedValue({
42
- ok: true,
43
- json: () => Promise.resolve({}),
44
- });
45
- vi.stubGlobal('fetch', mockFetch);
46
- await mattermostApi(mockConfig, 'POST', '/posts', { message: 'hello' });
47
- expect(mockFetch).toHaveBeenCalledWith(expect.any(String), expect.objectContaining({
48
- body: JSON.stringify({ message: 'hello' }),
49
- }));
50
- });
51
- it('throws on non-ok response', async () => {
52
- vi.stubGlobal('fetch', vi.fn().mockResolvedValue({
53
- ok: false,
54
- status: 401,
55
- text: () => Promise.resolve('Unauthorized'),
56
- }));
57
- await expect(mattermostApi(mockConfig, 'GET', '/users/me')).rejects.toThrow('Mattermost API error 401');
58
- });
59
- it('includes error details in thrown error', async () => {
60
- vi.stubGlobal('fetch', vi.fn().mockResolvedValue({
61
- ok: false,
62
- status: 403,
63
- text: () => Promise.resolve('Access denied'),
64
- }));
65
- await expect(mattermostApi(mockConfig, 'GET', '/users/me')).rejects.toThrow('Access denied');
66
- });
67
- });
68
- describe('getMe', () => {
69
- beforeEach(() => {
70
- vi.stubGlobal('fetch', vi.fn());
71
- });
72
- afterEach(() => {
73
- vi.unstubAllGlobals();
74
- });
75
- it('returns the current user', async () => {
76
- const mockUser = { id: 'bot123', username: 'bot' };
77
- vi.stubGlobal('fetch', vi.fn().mockResolvedValue({
78
- ok: true,
79
- json: () => Promise.resolve(mockUser),
80
- }));
81
- const result = await getMe(mockConfig);
82
- expect(result).toEqual(mockUser);
83
- });
84
- });
85
- describe('getUser', () => {
86
- beforeEach(() => {
87
- vi.stubGlobal('fetch', vi.fn());
88
- });
89
- afterEach(() => {
90
- vi.unstubAllGlobals();
91
- });
92
- it('returns user when found', async () => {
93
- const mockUser = { id: 'user123', username: 'testuser' };
94
- vi.stubGlobal('fetch', vi.fn().mockResolvedValue({
95
- ok: true,
96
- json: () => Promise.resolve(mockUser),
97
- }));
98
- const result = await getUser(mockConfig, 'user123');
99
- expect(result).toEqual(mockUser);
100
- });
101
- it('returns null when user not found', async () => {
102
- vi.stubGlobal('fetch', vi.fn().mockResolvedValue({
103
- ok: false,
104
- status: 404,
105
- text: () => Promise.resolve('Not found'),
106
- }));
107
- const result = await getUser(mockConfig, 'nonexistent');
108
- expect(result).toBeNull();
109
- });
110
- });
111
- describe('createPost', () => {
112
- beforeEach(() => {
113
- vi.stubGlobal('fetch', vi.fn());
114
- });
115
- afterEach(() => {
116
- vi.unstubAllGlobals();
117
- });
118
- it('creates a post with correct parameters', async () => {
119
- const mockPost = { id: 'post123', channel_id: 'channel1', message: 'Hello' };
120
- const mockFetch = vi.fn().mockResolvedValue({
121
- ok: true,
122
- json: () => Promise.resolve(mockPost),
123
- });
124
- vi.stubGlobal('fetch', mockFetch);
125
- await createPost(mockConfig, 'channel1', 'Hello');
126
- expect(mockFetch).toHaveBeenCalledWith('https://mattermost.example.com/api/v4/posts', expect.objectContaining({
127
- method: 'POST',
128
- body: JSON.stringify({
129
- channel_id: 'channel1',
130
- message: 'Hello',
131
- root_id: undefined,
132
- }),
133
- }));
134
- });
135
- it('includes root_id for thread replies', async () => {
136
- const mockFetch = vi.fn().mockResolvedValue({
137
- ok: true,
138
- json: () => Promise.resolve({}),
139
- });
140
- vi.stubGlobal('fetch', mockFetch);
141
- await createPost(mockConfig, 'channel1', 'Reply', 'thread123');
142
- expect(mockFetch).toHaveBeenCalledWith(expect.any(String), expect.objectContaining({
143
- body: JSON.stringify({
144
- channel_id: 'channel1',
145
- message: 'Reply',
146
- root_id: 'thread123',
147
- }),
148
- }));
149
- });
150
- });
151
- describe('updatePost', () => {
152
- beforeEach(() => {
153
- vi.stubGlobal('fetch', vi.fn());
154
- });
155
- afterEach(() => {
156
- vi.unstubAllGlobals();
157
- });
158
- it('updates a post with correct parameters', async () => {
159
- const mockFetch = vi.fn().mockResolvedValue({
160
- ok: true,
161
- json: () => Promise.resolve({ id: 'post123', message: 'Updated' }),
162
- });
163
- vi.stubGlobal('fetch', mockFetch);
164
- await updatePost(mockConfig, 'post123', 'Updated');
165
- expect(mockFetch).toHaveBeenCalledWith('https://mattermost.example.com/api/v4/posts/post123', expect.objectContaining({
166
- method: 'PUT',
167
- body: JSON.stringify({
168
- id: 'post123',
169
- message: 'Updated',
170
- }),
171
- }));
172
- });
173
- });
174
- describe('addReaction', () => {
175
- beforeEach(() => {
176
- vi.stubGlobal('fetch', vi.fn());
177
- });
178
- afterEach(() => {
179
- vi.unstubAllGlobals();
180
- });
181
- it('adds a reaction with correct parameters', async () => {
182
- const mockFetch = vi.fn().mockResolvedValue({
183
- ok: true,
184
- json: () => Promise.resolve({}),
185
- });
186
- vi.stubGlobal('fetch', mockFetch);
187
- await addReaction(mockConfig, 'post123', 'bot123', '+1');
188
- expect(mockFetch).toHaveBeenCalledWith('https://mattermost.example.com/api/v4/reactions', expect.objectContaining({
189
- method: 'POST',
190
- body: JSON.stringify({
191
- user_id: 'bot123',
192
- post_id: 'post123',
193
- emoji_name: '+1',
194
- }),
195
- }));
196
- });
197
- });
198
- describe('isUserAllowed', () => {
199
- it('returns true when allowlist is empty', () => {
200
- expect(isUserAllowed('anyone', [])).toBe(true);
201
- });
202
- it('returns true when user is in allowlist', () => {
203
- expect(isUserAllowed('alice', ['alice', 'bob'])).toBe(true);
204
- });
205
- it('returns false when user is not in allowlist', () => {
206
- expect(isUserAllowed('eve', ['alice', 'bob'])).toBe(false);
207
- });
208
- it('is case-sensitive', () => {
209
- expect(isUserAllowed('Alice', ['alice'])).toBe(false);
210
- });
211
- });
212
- describe('createInteractivePost', () => {
213
- let consoleSpy;
214
- beforeEach(() => {
215
- vi.stubGlobal('fetch', vi.fn());
216
- consoleSpy = vi.spyOn(console, 'error').mockImplementation(() => { });
217
- });
218
- afterEach(() => {
219
- vi.unstubAllGlobals();
220
- consoleSpy.mockRestore();
221
- });
222
- it('creates a post and adds reactions', async () => {
223
- const mockPost = { id: 'post123', channel_id: 'channel1', message: 'Hello' };
224
- const mockFetch = vi.fn().mockResolvedValue({
225
- ok: true,
226
- json: () => Promise.resolve(mockPost),
227
- });
228
- vi.stubGlobal('fetch', mockFetch);
229
- const result = await createInteractivePost(mockConfig, 'channel1', 'Hello', ['+1', '-1'], undefined, 'bot123');
230
- expect(result).toEqual(mockPost);
231
- // First call: createPost, second+third: addReaction
232
- expect(mockFetch).toHaveBeenCalledTimes(3);
233
- });
234
- it('creates post in a thread when rootId is provided', async () => {
235
- const mockPost = { id: 'post123', channel_id: 'channel1', message: 'Reply' };
236
- const mockFetch = vi.fn().mockResolvedValue({
237
- ok: true,
238
- json: () => Promise.resolve(mockPost),
239
- });
240
- vi.stubGlobal('fetch', mockFetch);
241
- await createInteractivePost(mockConfig, 'channel1', 'Reply', ['+1'], 'thread123', 'bot123');
242
- // Check the first call (createPost) includes root_id
243
- expect(mockFetch).toHaveBeenNthCalledWith(1, 'https://mattermost.example.com/api/v4/posts', expect.objectContaining({
244
- body: JSON.stringify({
245
- channel_id: 'channel1',
246
- message: 'Reply',
247
- root_id: 'thread123',
248
- }),
249
- }));
250
- });
251
- it('continues adding reactions even if one fails', async () => {
252
- const mockPost = { id: 'post123', channel_id: 'channel1', message: 'Hello' };
253
- let callCount = 0;
254
- const mockFetch = vi.fn().mockImplementation(() => {
255
- callCount++;
256
- if (callCount === 1) {
257
- // createPost succeeds
258
- return Promise.resolve({
259
- ok: true,
260
- json: () => Promise.resolve(mockPost),
261
- });
262
- }
263
- else if (callCount === 2) {
264
- // First reaction fails
265
- return Promise.resolve({
266
- ok: false,
267
- status: 500,
268
- text: () => Promise.resolve('Server error'),
269
- });
270
- }
271
- else {
272
- // Second reaction succeeds
273
- return Promise.resolve({
274
- ok: true,
275
- json: () => Promise.resolve({}),
276
- });
277
- }
278
- });
279
- vi.stubGlobal('fetch', mockFetch);
280
- const result = await createInteractivePost(mockConfig, 'channel1', 'Hello', ['+1', '-1'], undefined, 'bot123');
281
- // Should still return the post
282
- expect(result).toEqual(mockPost);
283
- // All three calls should have been made
284
- expect(mockFetch).toHaveBeenCalledTimes(3);
285
- // Error should have been logged
286
- expect(consoleSpy).toHaveBeenCalledWith(expect.stringContaining('Failed to add reaction'), expect.any(Error));
287
- });
288
- it('returns the post even with no reactions', async () => {
289
- const mockPost = { id: 'post123', channel_id: 'channel1', message: 'Hello' };
290
- const mockFetch = vi.fn().mockResolvedValue({
291
- ok: true,
292
- json: () => Promise.resolve(mockPost),
293
- });
294
- vi.stubGlobal('fetch', mockFetch);
295
- const result = await createInteractivePost(mockConfig, 'channel1', 'Hello', [], undefined, 'bot123');
296
- expect(result).toEqual(mockPost);
297
- // Only createPost call, no reactions
298
- expect(mockFetch).toHaveBeenCalledTimes(1);
299
- });
300
- it('adds reactions in the correct order', async () => {
301
- const mockPost = { id: 'post123', channel_id: 'channel1', message: 'Hello' };
302
- const mockFetch = vi.fn().mockResolvedValue({
303
- ok: true,
304
- json: () => Promise.resolve(mockPost),
305
- });
306
- vi.stubGlobal('fetch', mockFetch);
307
- await createInteractivePost(mockConfig, 'channel1', 'Hello', ['+1', 'white_check_mark', '-1'], undefined, 'bot123');
308
- // Check reaction calls are in order
309
- expect(mockFetch).toHaveBeenNthCalledWith(2, 'https://mattermost.example.com/api/v4/reactions', expect.objectContaining({
310
- body: expect.stringContaining('+1'),
311
- }));
312
- expect(mockFetch).toHaveBeenNthCalledWith(3, 'https://mattermost.example.com/api/v4/reactions', expect.objectContaining({
313
- body: expect.stringContaining('white_check_mark'),
314
- }));
315
- expect(mockFetch).toHaveBeenNthCalledWith(4, 'https://mattermost.example.com/api/v4/reactions', expect.objectContaining({
316
- body: expect.stringContaining('-1'),
317
- }));
318
- });
319
- });
@@ -1,2 +0,0 @@
1
- #!/usr/bin/env node
2
- export {};
@@ -1 +0,0 @@
1
- export declare function runOnboarding(reconfigure?: boolean): Promise<void>;
@@ -1,318 +0,0 @@
1
- import prompts from 'prompts';
2
- import { existsSync } from 'fs';
3
- import { CONFIG_PATH, saveConfig, } from './config/migration.js';
4
- import YAML from 'yaml';
5
- import { readFileSync } from 'fs';
6
- const bold = (s) => `\x1b[1m${s}\x1b[0m`;
7
- const dim = (s) => `\x1b[2m${s}\x1b[0m`;
8
- const green = (s) => `\x1b[32m${s}\x1b[0m`;
9
- const onCancel = () => {
10
- console.log('');
11
- console.log(dim(' Setup cancelled.'));
12
- process.exit(0);
13
- };
14
- export async function runOnboarding(reconfigure = false) {
15
- console.log('');
16
- console.log(bold(' claude-threads setup'));
17
- console.log(dim(' ─────────────────────────────────'));
18
- console.log('');
19
- // Load existing config if reconfiguring
20
- let existingConfig = null;
21
- if (reconfigure && existsSync(CONFIG_PATH)) {
22
- try {
23
- const content = readFileSync(CONFIG_PATH, 'utf-8');
24
- existingConfig = YAML.parse(content);
25
- console.log(dim(' Reconfiguring existing setup.'));
26
- }
27
- catch {
28
- console.log(dim(' Could not load existing config, starting fresh.'));
29
- }
30
- }
31
- else {
32
- console.log(' Welcome! Let\'s configure claude-threads.');
33
- }
34
- console.log('');
35
- // Step 1: Global settings
36
- const globalSettings = await prompts([
37
- {
38
- type: 'text',
39
- name: 'workingDir',
40
- message: 'Default working directory',
41
- initial: existingConfig?.workingDir || process.cwd(),
42
- hint: 'Where Claude Code runs by default',
43
- },
44
- {
45
- type: 'confirm',
46
- name: 'chrome',
47
- message: 'Enable Chrome integration?',
48
- initial: existingConfig?.chrome || false,
49
- hint: 'Requires Claude in Chrome extension',
50
- },
51
- {
52
- type: 'select',
53
- name: 'worktreeMode',
54
- message: 'Git worktree mode',
55
- choices: [
56
- { title: 'Prompt', value: 'prompt', description: 'Ask when starting sessions' },
57
- { title: 'Off', value: 'off', description: 'Never use worktrees' },
58
- { title: 'Require', value: 'require', description: 'Always require branch name' },
59
- ],
60
- initial: existingConfig?.worktreeMode === 'off' ? 1 :
61
- existingConfig?.worktreeMode === 'require' ? 2 : 0,
62
- },
63
- ], { onCancel });
64
- const config = {
65
- version: 2,
66
- ...globalSettings,
67
- platforms: [],
68
- };
69
- // Step 2: Add platforms (loop)
70
- console.log('');
71
- console.log(dim(' Now let\'s add your platform connections.'));
72
- console.log('');
73
- let platformNumber = 1;
74
- let addMore = true;
75
- while (addMore) {
76
- const isFirst = platformNumber === 1;
77
- const existingPlatform = existingConfig?.platforms[platformNumber - 1];
78
- // Ask what platform type
79
- const { platformType } = await prompts({
80
- type: 'select',
81
- name: 'platformType',
82
- message: isFirst ? 'First platform' : `Platform #${platformNumber}`,
83
- choices: [
84
- { title: 'Mattermost', value: 'mattermost' },
85
- { title: 'Slack', value: 'slack' },
86
- ...(isFirst ? [] : [{ title: '(Done - finish setup)', value: 'done' }]),
87
- ],
88
- initial: existingPlatform?.type === 'slack' ? 1 : 0,
89
- }, { onCancel });
90
- if (platformType === 'done') {
91
- addMore = false;
92
- break;
93
- }
94
- // Get platform ID and name
95
- const { platformId, displayName } = await prompts([
96
- {
97
- type: 'text',
98
- name: 'platformId',
99
- message: 'Platform ID',
100
- initial: existingPlatform?.id ||
101
- (config.platforms.length === 0 ? 'default' : `${platformType}-${platformNumber}`),
102
- hint: 'Unique identifier (e.g., mattermost-main, slack-eng)',
103
- validate: (v) => {
104
- if (!v.match(/^[a-z0-9-]+$/))
105
- return 'Use lowercase letters, numbers, hyphens only';
106
- if (config.platforms.some(p => p.id === v))
107
- return 'ID already in use';
108
- return true;
109
- },
110
- },
111
- {
112
- type: 'text',
113
- name: 'displayName',
114
- message: 'Display name',
115
- initial: existingPlatform?.displayName ||
116
- (platformType === 'mattermost' ? 'Mattermost' : 'Slack'),
117
- hint: 'Human-readable name (e.g., "Internal Team", "Engineering")',
118
- },
119
- ], { onCancel });
120
- // Configure the platform
121
- if (platformType === 'mattermost') {
122
- const platform = await setupMattermostPlatform(platformId, displayName, existingPlatform);
123
- config.platforms.push(platform);
124
- }
125
- else {
126
- const platform = await setupSlackPlatform(platformId, displayName, existingPlatform);
127
- config.platforms.push(platform);
128
- }
129
- console.log(green(` ✓ Added ${displayName}`));
130
- console.log('');
131
- // Ask to add more (after first one)
132
- if (platformNumber === 1) {
133
- const { addAnother } = await prompts({
134
- type: 'confirm',
135
- name: 'addAnother',
136
- message: 'Add another platform?',
137
- initial: (existingConfig?.platforms.length || 0) > 1,
138
- }, { onCancel });
139
- addMore = addAnother;
140
- }
141
- platformNumber++;
142
- }
143
- // Validate at least one platform
144
- if (config.platforms.length === 0) {
145
- console.log('');
146
- console.log(dim(' ⚠️ No platforms configured. Setup cancelled.'));
147
- process.exit(1);
148
- }
149
- // Save config
150
- saveConfig(config);
151
- console.log('');
152
- console.log(green(' ✓ Configuration saved!'));
153
- console.log(dim(` ${CONFIG_PATH}`));
154
- console.log('');
155
- console.log(dim(` Configured ${config.platforms.length} platform(s):`));
156
- for (const platform of config.platforms) {
157
- console.log(dim(` • ${platform.displayName} (${platform.type})`));
158
- }
159
- console.log('');
160
- console.log(dim(' Starting claude-threads...'));
161
- console.log('');
162
- }
163
- async function setupMattermostPlatform(id, displayName, existing) {
164
- console.log('');
165
- console.log(dim(' Mattermost setup:'));
166
- console.log('');
167
- const existingMattermost = existing?.type === 'mattermost' ? existing : undefined;
168
- const response = await prompts([
169
- {
170
- type: 'text',
171
- name: 'url',
172
- message: 'Server URL',
173
- initial: existingMattermost?.url || 'https://chat.example.com',
174
- validate: (v) => v.startsWith('http') ? true : 'Must start with http(s)://',
175
- },
176
- {
177
- type: 'password',
178
- name: 'token',
179
- message: 'Bot token',
180
- initial: existingMattermost?.token,
181
- hint: existingMattermost?.token ? 'Enter to keep existing, or type new token' : 'Create at: Integrations > Bot Accounts',
182
- validate: (v) => {
183
- // Allow empty if we have existing token
184
- if (!v && existingMattermost?.token)
185
- return true;
186
- return v.length > 0 ? true : 'Token is required';
187
- },
188
- },
189
- {
190
- type: 'text',
191
- name: 'channelId',
192
- message: 'Channel ID',
193
- initial: existingMattermost?.channelId || '',
194
- hint: 'Click channel > View Info > copy ID from URL',
195
- validate: (v) => v.length > 0 ? true : 'Channel ID is required',
196
- },
197
- {
198
- type: 'text',
199
- name: 'botName',
200
- message: 'Bot mention name',
201
- initial: existingMattermost?.botName || 'claude-code',
202
- hint: 'Users will @mention this name',
203
- },
204
- {
205
- type: 'text',
206
- name: 'allowedUsers',
207
- message: 'Allowed usernames (optional)',
208
- initial: existingMattermost?.allowedUsers?.join(',') || '',
209
- hint: 'Comma-separated, or empty to allow everyone',
210
- },
211
- {
212
- type: 'confirm',
213
- name: 'skipPermissions',
214
- message: 'Auto-approve all actions?',
215
- initial: existingMattermost?.skipPermissions || false,
216
- hint: 'If no, you\'ll approve via emoji reactions',
217
- },
218
- ], { onCancel });
219
- // Use existing token if user left it empty
220
- const finalToken = response.token || existingMattermost?.token;
221
- if (!finalToken) {
222
- console.log('');
223
- console.log(dim(' ⚠️ Token is required. Setup cancelled.'));
224
- process.exit(1);
225
- }
226
- return {
227
- id,
228
- type: 'mattermost',
229
- displayName,
230
- url: response.url,
231
- token: finalToken,
232
- channelId: response.channelId,
233
- botName: response.botName,
234
- allowedUsers: response.allowedUsers?.split(',').map((u) => u.trim()).filter((u) => u) || [],
235
- skipPermissions: response.skipPermissions,
236
- };
237
- }
238
- async function setupSlackPlatform(id, displayName, existing) {
239
- console.log('');
240
- console.log(dim(' Slack setup (requires Socket Mode):'));
241
- console.log(dim(' Create app at: api.slack.com/apps'));
242
- console.log('');
243
- const existingSlack = existing?.type === 'slack' ? existing : undefined;
244
- const response = await prompts([
245
- {
246
- type: 'password',
247
- name: 'botToken',
248
- message: 'Bot User OAuth Token',
249
- initial: existingSlack?.botToken,
250
- hint: existingSlack?.botToken ? 'Enter to keep existing' : 'Starts with xoxb-',
251
- validate: (v) => {
252
- if (!v && existingSlack?.botToken)
253
- return true;
254
- return v.startsWith('xoxb-') ? true : 'Must start with xoxb-';
255
- },
256
- },
257
- {
258
- type: 'password',
259
- name: 'appToken',
260
- message: 'App-Level Token',
261
- initial: existingSlack?.appToken,
262
- hint: existingSlack?.appToken ? 'Enter to keep existing' : 'Starts with xapp- (enable Socket Mode first)',
263
- validate: (v) => {
264
- if (!v && existingSlack?.appToken)
265
- return true;
266
- return v.startsWith('xapp-') ? true : 'Must start with xapp-';
267
- },
268
- },
269
- {
270
- type: 'text',
271
- name: 'channelId',
272
- message: 'Channel ID',
273
- initial: existingSlack?.channelId || '',
274
- hint: 'Right-click channel > View details > copy ID',
275
- validate: (v) => v.length > 0 ? true : 'Channel ID is required',
276
- },
277
- {
278
- type: 'text',
279
- name: 'botName',
280
- message: 'Bot mention name',
281
- initial: existingSlack?.botName || 'claude',
282
- hint: 'Users will @mention this name',
283
- },
284
- {
285
- type: 'text',
286
- name: 'allowedUsers',
287
- message: 'Allowed usernames (optional)',
288
- initial: existingSlack?.allowedUsers?.join(',') || '',
289
- hint: 'Comma-separated, or empty for everyone',
290
- },
291
- {
292
- type: 'confirm',
293
- name: 'skipPermissions',
294
- message: 'Auto-approve all actions?',
295
- initial: existingSlack?.skipPermissions || false,
296
- hint: 'If no, you\'ll approve via emoji reactions',
297
- },
298
- ], { onCancel });
299
- // Use existing tokens if user left them empty
300
- const finalBotToken = response.botToken || existingSlack?.botToken;
301
- const finalAppToken = response.appToken || existingSlack?.appToken;
302
- if (!finalBotToken || !finalAppToken) {
303
- console.log('');
304
- console.log(dim(' ⚠️ Both tokens are required. Setup cancelled.'));
305
- process.exit(1);
306
- }
307
- return {
308
- id,
309
- type: 'slack',
310
- displayName,
311
- botToken: finalBotToken,
312
- appToken: finalAppToken,
313
- channelId: response.channelId,
314
- botName: response.botName,
315
- allowedUsers: response.allowedUsers?.split(',').map((u) => u.trim()).filter((u) => u) || [],
316
- skipPermissions: response.skipPermissions,
317
- };
318
- }