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.
- package/CHANGELOG.md +28 -0
- package/README.md +5 -5
- package/dist/index.js +20410 -387
- package/dist/mcp/permission-server.js +34038 -139
- package/package.json +14 -18
- package/dist/changelog.d.ts +0 -20
- package/dist/changelog.js +0 -134
- package/dist/claude/cli.d.ts +0 -50
- package/dist/claude/cli.js +0 -181
- package/dist/config/migration.d.ts +0 -45
- package/dist/config/migration.js +0 -35
- package/dist/config.d.ts +0 -21
- package/dist/config.js +0 -7
- package/dist/git/worktree.d.ts +0 -46
- package/dist/git/worktree.js +0 -228
- package/dist/index.d.ts +0 -2
- package/dist/logo.d.ts +0 -14
- package/dist/logo.js +0 -41
- package/dist/mattermost/api.d.ts +0 -85
- package/dist/mattermost/api.js +0 -124
- package/dist/mattermost/api.test.d.ts +0 -1
- package/dist/mattermost/api.test.js +0 -319
- package/dist/mcp/permission-server.d.ts +0 -2
- package/dist/onboarding.d.ts +0 -1
- package/dist/onboarding.js +0 -318
- package/dist/persistence/session-store.d.ts +0 -71
- package/dist/persistence/session-store.js +0 -152
- package/dist/platform/client.d.ts +0 -140
- package/dist/platform/client.js +0 -1
- package/dist/platform/formatter.d.ts +0 -74
- package/dist/platform/formatter.js +0 -1
- package/dist/platform/index.d.ts +0 -11
- package/dist/platform/index.js +0 -8
- package/dist/platform/mattermost/client.d.ts +0 -70
- package/dist/platform/mattermost/client.js +0 -404
- package/dist/platform/mattermost/formatter.d.ts +0 -20
- package/dist/platform/mattermost/formatter.js +0 -46
- package/dist/platform/mattermost/permission-api.d.ts +0 -10
- package/dist/platform/mattermost/permission-api.js +0 -139
- package/dist/platform/mattermost/types.d.ts +0 -71
- package/dist/platform/mattermost/types.js +0 -1
- package/dist/platform/permission-api-factory.d.ts +0 -11
- package/dist/platform/permission-api-factory.js +0 -21
- package/dist/platform/permission-api.d.ts +0 -67
- package/dist/platform/permission-api.js +0 -8
- package/dist/platform/types.d.ts +0 -70
- package/dist/platform/types.js +0 -7
- package/dist/session/commands.d.ts +0 -52
- package/dist/session/commands.js +0 -323
- package/dist/session/events.d.ts +0 -25
- package/dist/session/events.js +0 -368
- package/dist/session/index.d.ts +0 -7
- package/dist/session/index.js +0 -6
- package/dist/session/lifecycle.d.ts +0 -70
- package/dist/session/lifecycle.js +0 -456
- package/dist/session/manager.d.ts +0 -96
- package/dist/session/manager.js +0 -537
- package/dist/session/reactions.d.ts +0 -25
- package/dist/session/reactions.js +0 -151
- package/dist/session/streaming.d.ts +0 -47
- package/dist/session/streaming.js +0 -152
- package/dist/session/types.d.ts +0 -78
- package/dist/session/types.js +0 -9
- package/dist/session/worktree.d.ts +0 -56
- package/dist/session/worktree.js +0 -339
- package/dist/update-notifier.d.ts +0 -3
- package/dist/update-notifier.js +0 -41
- package/dist/utils/emoji.d.ts +0 -43
- package/dist/utils/emoji.js +0 -65
- package/dist/utils/emoji.test.d.ts +0 -1
- package/dist/utils/emoji.test.js +0 -131
- package/dist/utils/logger.d.ts +0 -34
- package/dist/utils/logger.js +0 -42
- package/dist/utils/logger.test.d.ts +0 -1
- package/dist/utils/logger.test.js +0 -121
- package/dist/utils/tool-formatter.d.ts +0 -53
- package/dist/utils/tool-formatter.js +0 -252
- package/dist/utils/tool-formatter.test.d.ts +0 -1
- 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
|
-
});
|
package/dist/onboarding.d.ts
DELETED
|
@@ -1 +0,0 @@
|
|
|
1
|
-
export declare function runOnboarding(reconfigure?: boolean): Promise<void>;
|
package/dist/onboarding.js
DELETED
|
@@ -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
|
-
}
|