claude-threads 0.13.0 → 0.14.0
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 +32 -0
- package/README.md +78 -28
- package/dist/claude/cli.d.ts +8 -0
- package/dist/claude/cli.js +16 -8
- package/dist/config/migration.d.ts +45 -0
- package/dist/config/migration.js +35 -0
- package/dist/config.d.ts +12 -18
- package/dist/config.js +7 -94
- package/dist/git/worktree.d.ts +0 -4
- package/dist/git/worktree.js +1 -1
- package/dist/index.js +31 -13
- package/dist/logo.d.ts +3 -20
- package/dist/logo.js +7 -23
- package/dist/mcp/permission-server.js +61 -112
- package/dist/onboarding.js +262 -137
- package/dist/persistence/session-store.d.ts +8 -2
- package/dist/persistence/session-store.js +41 -16
- package/dist/platform/client.d.ts +140 -0
- package/dist/platform/formatter.d.ts +74 -0
- package/dist/platform/index.d.ts +11 -0
- package/dist/platform/index.js +8 -0
- package/dist/platform/mattermost/client.d.ts +70 -0
- package/dist/{mattermost → platform/mattermost}/client.js +117 -34
- package/dist/platform/mattermost/formatter.d.ts +20 -0
- package/dist/platform/mattermost/formatter.js +46 -0
- package/dist/platform/mattermost/permission-api.d.ts +10 -0
- package/dist/platform/mattermost/permission-api.js +139 -0
- package/dist/platform/mattermost/types.js +1 -0
- package/dist/platform/permission-api-factory.d.ts +11 -0
- package/dist/platform/permission-api-factory.js +21 -0
- package/dist/platform/permission-api.d.ts +67 -0
- package/dist/platform/permission-api.js +8 -0
- package/dist/platform/types.d.ts +70 -0
- package/dist/platform/types.js +7 -0
- package/dist/session/commands.d.ts +52 -0
- package/dist/session/commands.js +323 -0
- package/dist/session/events.d.ts +25 -0
- package/dist/session/events.js +368 -0
- package/dist/session/index.d.ts +7 -0
- package/dist/session/index.js +6 -0
- package/dist/session/lifecycle.d.ts +70 -0
- package/dist/session/lifecycle.js +456 -0
- package/dist/session/manager.d.ts +96 -0
- package/dist/session/manager.js +537 -0
- package/dist/session/reactions.d.ts +25 -0
- package/dist/session/reactions.js +151 -0
- package/dist/session/streaming.d.ts +47 -0
- package/dist/session/streaming.js +152 -0
- package/dist/session/types.d.ts +78 -0
- package/dist/session/types.js +9 -0
- package/dist/session/worktree.d.ts +56 -0
- package/dist/session/worktree.js +339 -0
- package/dist/{mattermost → utils}/emoji.d.ts +3 -3
- package/dist/{mattermost → utils}/emoji.js +3 -3
- package/dist/utils/emoji.test.d.ts +1 -0
- package/dist/utils/tool-formatter.d.ts +10 -13
- package/dist/utils/tool-formatter.js +48 -43
- package/dist/utils/tool-formatter.test.js +67 -52
- package/package.json +2 -3
- package/dist/claude/session.d.ts +0 -256
- package/dist/claude/session.js +0 -1964
- package/dist/mattermost/client.d.ts +0 -56
- /package/dist/{mattermost/emoji.test.d.ts → platform/client.js} +0 -0
- /package/dist/{mattermost/types.js → platform/formatter.js} +0 -0
- /package/dist/{mattermost → platform/mattermost}/types.d.ts +0 -0
- /package/dist/{mattermost → utils}/emoji.test.js +0 -0
|
@@ -1,5 +1,20 @@
|
|
|
1
1
|
import { describe, it, expect, beforeEach, afterEach } from 'vitest';
|
|
2
2
|
import { shortenPath, parseMcpToolName, formatToolUse, formatToolForPermission, } from './tool-formatter.js';
|
|
3
|
+
// Mock formatter for tests - uses standard markdown syntax
|
|
4
|
+
const formatter = {
|
|
5
|
+
formatBold: (text) => `**${text}**`,
|
|
6
|
+
formatItalic: (text) => `_${text}_`,
|
|
7
|
+
formatCode: (text) => `\`${text}\``,
|
|
8
|
+
formatCodeBlock: (code, language) => language ? `\`\`\`${language}\n${code}\n\`\`\`` : `\`\`\`\n${code}\n\`\`\``,
|
|
9
|
+
formatUserMention: (username) => `@${username}`,
|
|
10
|
+
formatLink: (text, url) => `[${text}](${url})`,
|
|
11
|
+
formatListItem: (text) => `- ${text}`,
|
|
12
|
+
formatNumberedListItem: (num, text) => `${num}. ${text}`,
|
|
13
|
+
formatBlockquote: (text) => `> ${text}`,
|
|
14
|
+
formatHorizontalRule: () => '---',
|
|
15
|
+
formatHeading: (text, level) => `${'#'.repeat(level)} ${text}`,
|
|
16
|
+
escapeText: (text) => text,
|
|
17
|
+
};
|
|
3
18
|
describe('shortenPath', () => {
|
|
4
19
|
const originalHome = process.env.HOME;
|
|
5
20
|
beforeEach(() => {
|
|
@@ -65,11 +80,11 @@ describe('formatToolUse', () => {
|
|
|
65
80
|
it('formats Read with file path', () => {
|
|
66
81
|
const result = formatToolUse('Read', {
|
|
67
82
|
file_path: '/Users/testuser/file.ts',
|
|
68
|
-
});
|
|
83
|
+
}, formatter);
|
|
69
84
|
expect(result).toBe('📄 **Read** `~/file.ts`');
|
|
70
85
|
});
|
|
71
86
|
it('shows full path when not under home', () => {
|
|
72
|
-
const result = formatToolUse('Read', { file_path: '/var/log/app.log' });
|
|
87
|
+
const result = formatToolUse('Read', { file_path: '/var/log/app.log' }, formatter);
|
|
73
88
|
expect(result).toBe('📄 **Read** `/var/log/app.log`');
|
|
74
89
|
});
|
|
75
90
|
});
|
|
@@ -79,7 +94,7 @@ describe('formatToolUse', () => {
|
|
|
79
94
|
file_path: '/Users/testuser/file.ts',
|
|
80
95
|
old_string: 'old',
|
|
81
96
|
new_string: 'new',
|
|
82
|
-
});
|
|
97
|
+
}, formatter);
|
|
83
98
|
expect(result).toBe('✏️ **Edit** `~/file.ts`');
|
|
84
99
|
});
|
|
85
100
|
it('formats Edit with diff in detailed mode', () => {
|
|
@@ -87,7 +102,7 @@ describe('formatToolUse', () => {
|
|
|
87
102
|
file_path: '/Users/testuser/file.ts',
|
|
88
103
|
old_string: 'old line',
|
|
89
104
|
new_string: 'new line',
|
|
90
|
-
}, { detailed: true });
|
|
105
|
+
}, formatter, { detailed: true });
|
|
91
106
|
expect(result).toContain('✏️ **Edit** `~/file.ts`');
|
|
92
107
|
expect(result).toContain('```diff');
|
|
93
108
|
expect(result).toContain('- old line');
|
|
@@ -100,7 +115,7 @@ describe('formatToolUse', () => {
|
|
|
100
115
|
file_path: '/Users/testuser/file.ts',
|
|
101
116
|
old_string: oldLines,
|
|
102
117
|
new_string: newLines,
|
|
103
|
-
}, { detailed: true, maxPreviewLines: 10 });
|
|
118
|
+
}, formatter, { detailed: true, maxPreviewLines: 10 });
|
|
104
119
|
expect(result).toContain('more lines');
|
|
105
120
|
});
|
|
106
121
|
});
|
|
@@ -109,16 +124,16 @@ describe('formatToolUse', () => {
|
|
|
109
124
|
const result = formatToolUse('Write', {
|
|
110
125
|
file_path: '/Users/testuser/file.ts',
|
|
111
126
|
content: 'hello world',
|
|
112
|
-
});
|
|
127
|
+
}, formatter);
|
|
113
128
|
expect(result).toBe('📝 **Write** `~/file.ts`');
|
|
114
129
|
});
|
|
115
130
|
it('formats Write with preview in detailed mode', () => {
|
|
116
131
|
const result = formatToolUse('Write', {
|
|
117
132
|
file_path: '/Users/testuser/file.ts',
|
|
118
133
|
content: 'line 1\nline 2\nline 3',
|
|
119
|
-
}, { detailed: true });
|
|
134
|
+
}, formatter, { detailed: true });
|
|
120
135
|
expect(result).toContain('📝 **Write** `~/file.ts`');
|
|
121
|
-
expect(result).toContain('
|
|
136
|
+
expect(result).toContain('_(3 lines)_');
|
|
122
137
|
expect(result).toContain('line 1');
|
|
123
138
|
});
|
|
124
139
|
it('truncates long content previews', () => {
|
|
@@ -126,189 +141,189 @@ describe('formatToolUse', () => {
|
|
|
126
141
|
const result = formatToolUse('Write', {
|
|
127
142
|
file_path: '/Users/testuser/file.ts',
|
|
128
143
|
content,
|
|
129
|
-
}, { detailed: true });
|
|
144
|
+
}, formatter, { detailed: true });
|
|
130
145
|
expect(result).toContain('more lines');
|
|
131
146
|
});
|
|
132
147
|
});
|
|
133
148
|
describe('Bash tool', () => {
|
|
134
149
|
it('formats short commands', () => {
|
|
135
|
-
const result = formatToolUse('Bash', { command: 'ls -la' });
|
|
150
|
+
const result = formatToolUse('Bash', { command: 'ls -la' }, formatter);
|
|
136
151
|
expect(result).toBe('💻 **Bash** `ls -la`');
|
|
137
152
|
});
|
|
138
153
|
it('truncates long commands', () => {
|
|
139
154
|
const longCmd = 'x'.repeat(100);
|
|
140
|
-
const result = formatToolUse('Bash', { command: longCmd });
|
|
155
|
+
const result = formatToolUse('Bash', { command: longCmd }, formatter);
|
|
141
156
|
expect(result).not.toBeNull();
|
|
142
157
|
expect(result).toContain('...');
|
|
143
158
|
expect(result.length).toBeLessThan(120);
|
|
144
159
|
});
|
|
145
160
|
it('respects custom maxCommandLength', () => {
|
|
146
|
-
const result = formatToolUse('Bash', { command: '1234567890' }, { maxCommandLength: 5 });
|
|
161
|
+
const result = formatToolUse('Bash', { command: '1234567890' }, formatter, { maxCommandLength: 5 });
|
|
147
162
|
expect(result).toBe('💻 **Bash** `12345...`');
|
|
148
163
|
});
|
|
149
164
|
});
|
|
150
165
|
describe('Other tools', () => {
|
|
151
166
|
it('formats Glob', () => {
|
|
152
|
-
const result = formatToolUse('Glob', { pattern: '**/*.ts' });
|
|
167
|
+
const result = formatToolUse('Glob', { pattern: '**/*.ts' }, formatter);
|
|
153
168
|
expect(result).toBe('🔍 **Glob** `**/*.ts`');
|
|
154
169
|
});
|
|
155
170
|
it('formats Grep', () => {
|
|
156
|
-
const result = formatToolUse('Grep', { pattern: 'TODO' });
|
|
171
|
+
const result = formatToolUse('Grep', { pattern: 'TODO' }, formatter);
|
|
157
172
|
expect(result).toBe('🔎 **Grep** `TODO`');
|
|
158
173
|
});
|
|
159
174
|
it('formats EnterPlanMode', () => {
|
|
160
|
-
const result = formatToolUse('EnterPlanMode', {});
|
|
175
|
+
const result = formatToolUse('EnterPlanMode', {}, formatter);
|
|
161
176
|
expect(result).toBe('📋 **Planning...**');
|
|
162
177
|
});
|
|
163
178
|
it('formats WebFetch', () => {
|
|
164
179
|
const result = formatToolUse('WebFetch', {
|
|
165
180
|
url: 'https://example.com/page',
|
|
166
|
-
});
|
|
181
|
+
}, formatter);
|
|
167
182
|
expect(result).toBe('🌐 **Fetching** `https://example.com/page`');
|
|
168
183
|
});
|
|
169
184
|
it('formats WebSearch', () => {
|
|
170
|
-
const result = formatToolUse('WebSearch', { query: 'typescript guide' });
|
|
185
|
+
const result = formatToolUse('WebSearch', { query: 'typescript guide' }, formatter);
|
|
171
186
|
expect(result).toBe('🔍 **Searching** `typescript guide`');
|
|
172
187
|
});
|
|
173
188
|
});
|
|
174
189
|
describe('Tools that return null', () => {
|
|
175
190
|
it('returns null for Task', () => {
|
|
176
|
-
expect(formatToolUse('Task', {})).toBeNull();
|
|
191
|
+
expect(formatToolUse('Task', {}, formatter)).toBeNull();
|
|
177
192
|
});
|
|
178
193
|
it('returns null for ExitPlanMode', () => {
|
|
179
|
-
expect(formatToolUse('ExitPlanMode', {})).toBeNull();
|
|
194
|
+
expect(formatToolUse('ExitPlanMode', {}, formatter)).toBeNull();
|
|
180
195
|
});
|
|
181
196
|
it('returns null for AskUserQuestion', () => {
|
|
182
|
-
expect(formatToolUse('AskUserQuestion', {})).toBeNull();
|
|
197
|
+
expect(formatToolUse('AskUserQuestion', {}, formatter)).toBeNull();
|
|
183
198
|
});
|
|
184
199
|
it('returns null for TodoWrite', () => {
|
|
185
|
-
expect(formatToolUse('TodoWrite', {})).toBeNull();
|
|
200
|
+
expect(formatToolUse('TodoWrite', {}, formatter)).toBeNull();
|
|
186
201
|
});
|
|
187
202
|
});
|
|
188
203
|
describe('MCP tools', () => {
|
|
189
204
|
it('formats MCP tools', () => {
|
|
190
|
-
const result = formatToolUse('mcp__myserver__mytool', { arg: 'value' });
|
|
191
|
-
expect(result).toBe('🔌 **mytool**
|
|
205
|
+
const result = formatToolUse('mcp__myserver__mytool', { arg: 'value' }, formatter);
|
|
206
|
+
expect(result).toBe('🔌 **mytool** _(myserver)_');
|
|
192
207
|
});
|
|
193
208
|
it('formats MCP tools with underscores in name', () => {
|
|
194
|
-
const result = formatToolUse('mcp__my_server__my_tool', { arg: 'value' });
|
|
195
|
-
expect(result).toBe('🔌 **my_tool**
|
|
209
|
+
const result = formatToolUse('mcp__my_server__my_tool', { arg: 'value' }, formatter);
|
|
210
|
+
expect(result).toBe('🔌 **my_tool** _(my_server)_');
|
|
196
211
|
});
|
|
197
212
|
});
|
|
198
213
|
describe('Claude in Chrome tools', () => {
|
|
199
214
|
it('formats computer screenshot action', () => {
|
|
200
|
-
const result = formatToolUse('mcp__claude-in-chrome__computer', { action: 'screenshot' });
|
|
215
|
+
const result = formatToolUse('mcp__claude-in-chrome__computer', { action: 'screenshot' }, formatter);
|
|
201
216
|
expect(result).toBe('🌐 **Chrome**[computer] `screenshot`');
|
|
202
217
|
});
|
|
203
218
|
it('formats computer click actions with coordinates', () => {
|
|
204
219
|
const result = formatToolUse('mcp__claude-in-chrome__computer', {
|
|
205
220
|
action: 'left_click',
|
|
206
221
|
coordinate: [100, 200],
|
|
207
|
-
});
|
|
222
|
+
}, formatter);
|
|
208
223
|
expect(result).toBe('🌐 **Chrome**[computer] `left_click at (100, 200)`');
|
|
209
224
|
});
|
|
210
225
|
it('formats computer type action', () => {
|
|
211
226
|
const result = formatToolUse('mcp__claude-in-chrome__computer', {
|
|
212
227
|
action: 'type',
|
|
213
228
|
text: 'hello world',
|
|
214
|
-
});
|
|
229
|
+
}, formatter);
|
|
215
230
|
expect(result).toBe('🌐 **Chrome**[computer] `type "hello world"`');
|
|
216
231
|
});
|
|
217
232
|
it('truncates long type text', () => {
|
|
218
233
|
const result = formatToolUse('mcp__claude-in-chrome__computer', {
|
|
219
234
|
action: 'type',
|
|
220
235
|
text: 'this is a very long text that should be truncated',
|
|
221
|
-
});
|
|
236
|
+
}, formatter);
|
|
222
237
|
expect(result).toContain('...');
|
|
223
238
|
});
|
|
224
239
|
it('formats computer key action', () => {
|
|
225
240
|
const result = formatToolUse('mcp__claude-in-chrome__computer', {
|
|
226
241
|
action: 'key',
|
|
227
242
|
text: 'Enter',
|
|
228
|
-
});
|
|
243
|
+
}, formatter);
|
|
229
244
|
expect(result).toBe('🌐 **Chrome**[computer] `key Enter`');
|
|
230
245
|
});
|
|
231
246
|
it('formats computer scroll action', () => {
|
|
232
247
|
const result = formatToolUse('mcp__claude-in-chrome__computer', {
|
|
233
248
|
action: 'scroll',
|
|
234
249
|
scroll_direction: 'up',
|
|
235
|
-
});
|
|
250
|
+
}, formatter);
|
|
236
251
|
expect(result).toBe('🌐 **Chrome**[computer] `scroll up`');
|
|
237
252
|
});
|
|
238
253
|
it('formats computer wait action', () => {
|
|
239
254
|
const result = formatToolUse('mcp__claude-in-chrome__computer', {
|
|
240
255
|
action: 'wait',
|
|
241
256
|
duration: 2,
|
|
242
|
-
});
|
|
257
|
+
}, formatter);
|
|
243
258
|
expect(result).toBe('🌐 **Chrome**[computer] `wait 2s`');
|
|
244
259
|
});
|
|
245
260
|
it('formats navigate tool', () => {
|
|
246
261
|
const result = formatToolUse('mcp__claude-in-chrome__navigate', {
|
|
247
262
|
url: 'https://example.com/page',
|
|
248
|
-
});
|
|
263
|
+
}, formatter);
|
|
249
264
|
expect(result).toBe('🌐 **Chrome**[navigate] `https://example.com/page`');
|
|
250
265
|
});
|
|
251
266
|
it('truncates long URLs in navigate', () => {
|
|
252
267
|
const result = formatToolUse('mcp__claude-in-chrome__navigate', {
|
|
253
268
|
url: 'https://example.com/' + 'x'.repeat(100),
|
|
254
|
-
});
|
|
269
|
+
}, formatter);
|
|
255
270
|
expect(result).toContain('...');
|
|
256
271
|
});
|
|
257
272
|
it('formats tabs_context_mcp tool', () => {
|
|
258
|
-
const result = formatToolUse('mcp__claude-in-chrome__tabs_context_mcp', {});
|
|
273
|
+
const result = formatToolUse('mcp__claude-in-chrome__tabs_context_mcp', {}, formatter);
|
|
259
274
|
expect(result).toBe('🌐 **Chrome**[tabs] reading context');
|
|
260
275
|
});
|
|
261
276
|
it('formats tabs_create_mcp tool', () => {
|
|
262
|
-
const result = formatToolUse('mcp__claude-in-chrome__tabs_create_mcp', {});
|
|
277
|
+
const result = formatToolUse('mcp__claude-in-chrome__tabs_create_mcp', {}, formatter);
|
|
263
278
|
expect(result).toBe('🌐 **Chrome**[tabs] creating new tab');
|
|
264
279
|
});
|
|
265
280
|
it('formats read_page tool', () => {
|
|
266
|
-
const result = formatToolUse('mcp__claude-in-chrome__read_page', {});
|
|
281
|
+
const result = formatToolUse('mcp__claude-in-chrome__read_page', {}, formatter);
|
|
267
282
|
expect(result).toBe('🌐 **Chrome**[read_page] accessibility tree');
|
|
268
283
|
});
|
|
269
284
|
it('formats read_page tool with interactive filter', () => {
|
|
270
285
|
const result = formatToolUse('mcp__claude-in-chrome__read_page', {
|
|
271
286
|
filter: 'interactive',
|
|
272
|
-
});
|
|
287
|
+
}, formatter);
|
|
273
288
|
expect(result).toBe('🌐 **Chrome**[read_page] interactive elements');
|
|
274
289
|
});
|
|
275
290
|
it('formats find tool', () => {
|
|
276
291
|
const result = formatToolUse('mcp__claude-in-chrome__find', {
|
|
277
292
|
query: 'login button',
|
|
278
|
-
});
|
|
293
|
+
}, formatter);
|
|
279
294
|
expect(result).toBe('🌐 **Chrome**[find] `login button`');
|
|
280
295
|
});
|
|
281
296
|
it('formats form_input tool', () => {
|
|
282
297
|
const result = formatToolUse('mcp__claude-in-chrome__form_input', {
|
|
283
298
|
ref: 'ref_1',
|
|
284
299
|
value: 'test',
|
|
285
|
-
});
|
|
300
|
+
}, formatter);
|
|
286
301
|
expect(result).toBe('🌐 **Chrome**[form_input] setting value');
|
|
287
302
|
});
|
|
288
303
|
it('formats get_page_text tool', () => {
|
|
289
|
-
const result = formatToolUse('mcp__claude-in-chrome__get_page_text', {});
|
|
304
|
+
const result = formatToolUse('mcp__claude-in-chrome__get_page_text', {}, formatter);
|
|
290
305
|
expect(result).toBe('🌐 **Chrome**[get_page_text] extracting content');
|
|
291
306
|
});
|
|
292
307
|
it('formats javascript_tool', () => {
|
|
293
308
|
const result = formatToolUse('mcp__claude-in-chrome__javascript_tool', {
|
|
294
309
|
text: 'document.title',
|
|
295
|
-
});
|
|
310
|
+
}, formatter);
|
|
296
311
|
expect(result).toBe('🌐 **Chrome**[javascript] executing script');
|
|
297
312
|
});
|
|
298
313
|
it('formats gif_creator tool', () => {
|
|
299
314
|
const result = formatToolUse('mcp__claude-in-chrome__gif_creator', {
|
|
300
315
|
action: 'start_recording',
|
|
301
|
-
});
|
|
316
|
+
}, formatter);
|
|
302
317
|
expect(result).toBe('🌐 **Chrome**[gif] start_recording');
|
|
303
318
|
});
|
|
304
319
|
it('formats unknown Chrome tools', () => {
|
|
305
|
-
const result = formatToolUse('mcp__claude-in-chrome__new_tool', {});
|
|
320
|
+
const result = formatToolUse('mcp__claude-in-chrome__new_tool', {}, formatter);
|
|
306
321
|
expect(result).toBe('🌐 **Chrome**[new_tool]');
|
|
307
322
|
});
|
|
308
323
|
});
|
|
309
324
|
describe('Unknown tools', () => {
|
|
310
325
|
it('formats unknown tools with bullet', () => {
|
|
311
|
-
const result = formatToolUse('CustomTool', {});
|
|
326
|
+
const result = formatToolUse('CustomTool', {}, formatter);
|
|
312
327
|
expect(result).toBe('● **CustomTool**');
|
|
313
328
|
});
|
|
314
329
|
});
|
|
@@ -324,34 +339,34 @@ describe('formatToolForPermission', () => {
|
|
|
324
339
|
it('formats Read tool', () => {
|
|
325
340
|
const result = formatToolForPermission('Read', {
|
|
326
341
|
file_path: '/Users/testuser/file.ts',
|
|
327
|
-
});
|
|
342
|
+
}, formatter);
|
|
328
343
|
expect(result).toBe('📄 **Read** `~/file.ts`');
|
|
329
344
|
});
|
|
330
345
|
it('formats Write tool', () => {
|
|
331
346
|
const result = formatToolForPermission('Write', {
|
|
332
347
|
file_path: '/Users/testuser/file.ts',
|
|
333
|
-
});
|
|
348
|
+
}, formatter);
|
|
334
349
|
expect(result).toBe('📝 **Write** `~/file.ts`');
|
|
335
350
|
});
|
|
336
351
|
it('formats Edit tool', () => {
|
|
337
352
|
const result = formatToolForPermission('Edit', {
|
|
338
353
|
file_path: '/Users/testuser/file.ts',
|
|
339
|
-
});
|
|
354
|
+
}, formatter);
|
|
340
355
|
expect(result).toBe('✏️ **Edit** `~/file.ts`');
|
|
341
356
|
});
|
|
342
357
|
it('formats Bash with longer truncation limit (100 chars)', () => {
|
|
343
358
|
const cmd = 'x'.repeat(100);
|
|
344
|
-
const result = formatToolForPermission('Bash', { command: cmd });
|
|
359
|
+
const result = formatToolForPermission('Bash', { command: cmd }, formatter);
|
|
345
360
|
// Should truncate at 100
|
|
346
361
|
expect(result).toContain('...');
|
|
347
362
|
expect(result.length).toBeLessThan(150);
|
|
348
363
|
});
|
|
349
364
|
it('formats MCP tools', () => {
|
|
350
|
-
const result = formatToolForPermission('mcp__server__tool', {});
|
|
351
|
-
expect(result).toBe('🔌 **tool**
|
|
365
|
+
const result = formatToolForPermission('mcp__server__tool', {}, formatter);
|
|
366
|
+
expect(result).toBe('🔌 **tool** _(server)_');
|
|
352
367
|
});
|
|
353
368
|
it('formats unknown tools', () => {
|
|
354
|
-
const result = formatToolForPermission('CustomTool', {});
|
|
369
|
+
const result = formatToolForPermission('CustomTool', {}, formatter);
|
|
355
370
|
expect(result).toBe('● **CustomTool**');
|
|
356
371
|
});
|
|
357
372
|
});
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "claude-threads",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.14.0",
|
|
4
4
|
"description": "Share Claude Code sessions live in a Mattermost channel with interactive features",
|
|
5
5
|
"main": "dist/index.js",
|
|
6
6
|
"type": "module",
|
|
@@ -51,15 +51,14 @@
|
|
|
51
51
|
"@modelcontextprotocol/sdk": "^1.25.1",
|
|
52
52
|
"commander": "^14.0.2",
|
|
53
53
|
"diff": "^8.0.2",
|
|
54
|
-
"dotenv": "^16.4.7",
|
|
55
54
|
"prompts": "^2.4.2",
|
|
56
55
|
"update-notifier": "^7.3.1",
|
|
57
56
|
"ws": "^8.18.0",
|
|
57
|
+
"yaml": "^2.8.2",
|
|
58
58
|
"zod": "^4.2.1"
|
|
59
59
|
},
|
|
60
60
|
"devDependencies": {
|
|
61
61
|
"@eslint/js": "^9.39.2",
|
|
62
|
-
"@types/diff": "^7.0.2",
|
|
63
62
|
"@types/node": "^22.19.3",
|
|
64
63
|
"@types/prompts": "^2.4.9",
|
|
65
64
|
"@types/update-notifier": "^6.0.8",
|
package/dist/claude/session.d.ts
DELETED
|
@@ -1,256 +0,0 @@
|
|
|
1
|
-
import { ClaudeCli } from './cli.js';
|
|
2
|
-
import { MattermostClient } from '../mattermost/client.js';
|
|
3
|
-
import { MattermostFile } from '../mattermost/types.js';
|
|
4
|
-
import { PersistedSession, WorktreeInfo } from '../persistence/session-store.js';
|
|
5
|
-
import { WorktreeMode } from '../config.js';
|
|
6
|
-
interface QuestionOption {
|
|
7
|
-
label: string;
|
|
8
|
-
description: string;
|
|
9
|
-
}
|
|
10
|
-
interface PendingQuestionSet {
|
|
11
|
-
toolUseId: string;
|
|
12
|
-
currentIndex: number;
|
|
13
|
-
currentPostId: string | null;
|
|
14
|
-
questions: Array<{
|
|
15
|
-
header: string;
|
|
16
|
-
question: string;
|
|
17
|
-
options: QuestionOption[];
|
|
18
|
-
answer: string | null;
|
|
19
|
-
}>;
|
|
20
|
-
}
|
|
21
|
-
interface PendingApproval {
|
|
22
|
-
postId: string;
|
|
23
|
-
type: 'plan' | 'action';
|
|
24
|
-
toolUseId: string;
|
|
25
|
-
}
|
|
26
|
-
/**
|
|
27
|
-
* Pending message from unauthorized user awaiting approval
|
|
28
|
-
*/
|
|
29
|
-
interface PendingMessageApproval {
|
|
30
|
-
postId: string;
|
|
31
|
-
originalMessage: string;
|
|
32
|
-
fromUser: string;
|
|
33
|
-
}
|
|
34
|
-
/**
|
|
35
|
-
* Represents a single Claude Code session tied to a Mattermost thread.
|
|
36
|
-
* Each session has its own Claude CLI process and state.
|
|
37
|
-
*/
|
|
38
|
-
interface Session {
|
|
39
|
-
threadId: string;
|
|
40
|
-
claudeSessionId: string;
|
|
41
|
-
startedBy: string;
|
|
42
|
-
startedAt: Date;
|
|
43
|
-
lastActivityAt: Date;
|
|
44
|
-
sessionNumber: number;
|
|
45
|
-
workingDir: string;
|
|
46
|
-
claude: ClaudeCli;
|
|
47
|
-
currentPostId: string | null;
|
|
48
|
-
pendingContent: string;
|
|
49
|
-
pendingApproval: PendingApproval | null;
|
|
50
|
-
pendingQuestionSet: PendingQuestionSet | null;
|
|
51
|
-
pendingMessageApproval: PendingMessageApproval | null;
|
|
52
|
-
planApproved: boolean;
|
|
53
|
-
sessionAllowedUsers: Set<string>;
|
|
54
|
-
forceInteractivePermissions: boolean;
|
|
55
|
-
sessionStartPostId: string | null;
|
|
56
|
-
tasksPostId: string | null;
|
|
57
|
-
activeSubagents: Map<string, string>;
|
|
58
|
-
updateTimer: ReturnType<typeof setTimeout> | null;
|
|
59
|
-
typingTimer: ReturnType<typeof setInterval> | null;
|
|
60
|
-
timeoutWarningPosted: boolean;
|
|
61
|
-
isRestarting: boolean;
|
|
62
|
-
isResumed: boolean;
|
|
63
|
-
wasInterrupted: boolean;
|
|
64
|
-
inProgressTaskStart: number | null;
|
|
65
|
-
activeToolStarts: Map<string, number>;
|
|
66
|
-
worktreeInfo?: WorktreeInfo;
|
|
67
|
-
pendingWorktreePrompt?: boolean;
|
|
68
|
-
worktreePromptDisabled?: boolean;
|
|
69
|
-
queuedPrompt?: string;
|
|
70
|
-
worktreePromptPostId?: string;
|
|
71
|
-
}
|
|
72
|
-
export declare class SessionManager {
|
|
73
|
-
private mattermost;
|
|
74
|
-
private workingDir;
|
|
75
|
-
private skipPermissions;
|
|
76
|
-
private chromeEnabled;
|
|
77
|
-
private worktreeMode;
|
|
78
|
-
private debug;
|
|
79
|
-
private sessions;
|
|
80
|
-
private postIndex;
|
|
81
|
-
private sessionStore;
|
|
82
|
-
private cleanupTimer;
|
|
83
|
-
private isShuttingDown;
|
|
84
|
-
constructor(mattermost: MattermostClient, workingDir: string, skipPermissions?: boolean, chromeEnabled?: boolean, worktreeMode?: WorktreeMode);
|
|
85
|
-
/**
|
|
86
|
-
* Initialize session manager by resuming any persisted sessions.
|
|
87
|
-
* Should be called before starting to listen for new messages.
|
|
88
|
-
*/
|
|
89
|
-
initialize(): Promise<void>;
|
|
90
|
-
/**
|
|
91
|
-
* Resume a single session from persisted state
|
|
92
|
-
*/
|
|
93
|
-
private resumeSession;
|
|
94
|
-
/**
|
|
95
|
-
* Persist a session to disk
|
|
96
|
-
*/
|
|
97
|
-
private persistSession;
|
|
98
|
-
/**
|
|
99
|
-
* Remove a session from persistence
|
|
100
|
-
*/
|
|
101
|
-
private unpersistSession;
|
|
102
|
-
/** Get a session by thread ID */
|
|
103
|
-
getSession(threadId: string): Session | undefined;
|
|
104
|
-
/** Check if a session exists for this thread */
|
|
105
|
-
hasSession(threadId: string): boolean;
|
|
106
|
-
/** Get the number of active sessions */
|
|
107
|
-
getSessionCount(): number;
|
|
108
|
-
/** Get all active session thread IDs */
|
|
109
|
-
getActiveThreadIds(): string[];
|
|
110
|
-
/** Mark that we're shutting down (prevents cleanup of persisted sessions) */
|
|
111
|
-
setShuttingDown(): void;
|
|
112
|
-
/** Register a post for reaction routing */
|
|
113
|
-
private registerPost;
|
|
114
|
-
/** Find session by post ID (for reaction routing) */
|
|
115
|
-
private getSessionByPost;
|
|
116
|
-
/**
|
|
117
|
-
* Check if a user is allowed in a specific session.
|
|
118
|
-
* Checks global allowlist first, then session-specific allowlist.
|
|
119
|
-
*/
|
|
120
|
-
isUserAllowedInSession(threadId: string, username: string): boolean;
|
|
121
|
-
startSession(options: {
|
|
122
|
-
prompt: string;
|
|
123
|
-
files?: MattermostFile[];
|
|
124
|
-
}, username: string, replyToPostId?: string): Promise<void>;
|
|
125
|
-
/**
|
|
126
|
-
* Start a session with an initial worktree specified.
|
|
127
|
-
* Used when user specifies "on branch X" or "!worktree X" in their initial message.
|
|
128
|
-
*/
|
|
129
|
-
startSessionWithWorktree(options: {
|
|
130
|
-
prompt: string;
|
|
131
|
-
files?: MattermostFile[];
|
|
132
|
-
}, branch: string, username: string, replyToPostId?: string): Promise<void>;
|
|
133
|
-
/**
|
|
134
|
-
* Check if we should prompt for a worktree before starting work.
|
|
135
|
-
* Returns the reason string if we should prompt, or null if not.
|
|
136
|
-
*/
|
|
137
|
-
private shouldPromptForWorktree;
|
|
138
|
-
/**
|
|
139
|
-
* Check if another session is using the same repository
|
|
140
|
-
*/
|
|
141
|
-
private hasOtherSessionInRepo;
|
|
142
|
-
/**
|
|
143
|
-
* Post the worktree prompt message
|
|
144
|
-
*/
|
|
145
|
-
private postWorktreePrompt;
|
|
146
|
-
private handleEvent;
|
|
147
|
-
private handleTaskComplete;
|
|
148
|
-
private handleExitPlanMode;
|
|
149
|
-
private handleTodoWrite;
|
|
150
|
-
private handleTaskStart;
|
|
151
|
-
private handleAskUserQuestion;
|
|
152
|
-
private postCurrentQuestion;
|
|
153
|
-
private handleReaction;
|
|
154
|
-
private handleQuestionReaction;
|
|
155
|
-
private handleApprovalReaction;
|
|
156
|
-
private handleMessageApprovalReaction;
|
|
157
|
-
private formatEvent;
|
|
158
|
-
private appendContent;
|
|
159
|
-
private scheduleUpdate;
|
|
160
|
-
/**
|
|
161
|
-
* Build message content for Claude, including images if present.
|
|
162
|
-
* Returns either a string or an array of content blocks.
|
|
163
|
-
*/
|
|
164
|
-
private buildMessageContent;
|
|
165
|
-
private startTyping;
|
|
166
|
-
private stopTyping;
|
|
167
|
-
private flush;
|
|
168
|
-
private handleExit;
|
|
169
|
-
/** Check if any sessions are active */
|
|
170
|
-
isSessionActive(): boolean;
|
|
171
|
-
/** Check if a session exists for this thread */
|
|
172
|
-
isInSessionThread(threadRoot: string): boolean;
|
|
173
|
-
/** Send a follow-up message to an existing session */
|
|
174
|
-
sendFollowUp(threadId: string, message: string, files?: MattermostFile[]): Promise<void>;
|
|
175
|
-
/**
|
|
176
|
-
* Check if there's a paused (persisted but not active) session for this thread.
|
|
177
|
-
* This is used to detect when we should resume a session instead of ignoring the message.
|
|
178
|
-
*/
|
|
179
|
-
hasPausedSession(threadId: string): boolean;
|
|
180
|
-
/**
|
|
181
|
-
* Resume a paused session and send a message to it.
|
|
182
|
-
* Called when a user sends a message to a thread with a paused session.
|
|
183
|
-
*/
|
|
184
|
-
resumePausedSession(threadId: string, message: string, files?: MattermostFile[]): Promise<void>;
|
|
185
|
-
/**
|
|
186
|
-
* Get persisted session info for access control checks
|
|
187
|
-
*/
|
|
188
|
-
getPersistedSession(threadId: string): PersistedSession | undefined;
|
|
189
|
-
/** Kill a specific session */
|
|
190
|
-
killSession(threadId: string, unpersist?: boolean): void;
|
|
191
|
-
/** Cancel a session with user feedback */
|
|
192
|
-
cancelSession(threadId: string, username: string): Promise<void>;
|
|
193
|
-
/** Interrupt current processing but keep session alive (like Escape in CLI) */
|
|
194
|
-
interruptSession(threadId: string, username: string): Promise<void>;
|
|
195
|
-
/** Change working directory for a session (restarts Claude CLI) */
|
|
196
|
-
changeDirectory(threadId: string, newDir: string, username: string): Promise<void>;
|
|
197
|
-
/** Invite a user to participate in a specific session */
|
|
198
|
-
inviteUser(threadId: string, invitedUser: string, invitedBy: string): Promise<void>;
|
|
199
|
-
/** Kick a user from a specific session */
|
|
200
|
-
kickUser(threadId: string, kickedUser: string, kickedBy: string): Promise<void>;
|
|
201
|
-
/**
|
|
202
|
-
* Enable interactive permissions for a session.
|
|
203
|
-
* Can only downgrade (skip → interactive), not upgrade.
|
|
204
|
-
*/
|
|
205
|
-
enableInteractivePermissions(threadId: string, username: string): Promise<void>;
|
|
206
|
-
/** Check if a session should use interactive permissions */
|
|
207
|
-
isSessionInteractive(threadId: string): boolean;
|
|
208
|
-
/** Update the session header post with current participants */
|
|
209
|
-
private updateSessionHeader;
|
|
210
|
-
/** Request approval for a message from an unauthorized user */
|
|
211
|
-
requestMessageApproval(threadId: string, username: string, message: string): Promise<void>;
|
|
212
|
-
/**
|
|
213
|
-
* Handle a worktree branch response from user.
|
|
214
|
-
* Called when user replies with a branch name to the worktree prompt.
|
|
215
|
-
*/
|
|
216
|
-
handleWorktreeBranchResponse(threadId: string, branchName: string, username: string): Promise<boolean>;
|
|
217
|
-
/**
|
|
218
|
-
* Handle ❌ reaction on worktree prompt - skip worktree and continue in main repo.
|
|
219
|
-
*/
|
|
220
|
-
handleWorktreeSkip(threadId: string, username: string): Promise<void>;
|
|
221
|
-
/**
|
|
222
|
-
* Create a new worktree and switch the session to it.
|
|
223
|
-
*/
|
|
224
|
-
createAndSwitchToWorktree(threadId: string, branch: string, username: string): Promise<void>;
|
|
225
|
-
/**
|
|
226
|
-
* Switch to an existing worktree.
|
|
227
|
-
*/
|
|
228
|
-
switchToWorktree(threadId: string, branchOrPath: string, username: string): Promise<void>;
|
|
229
|
-
/**
|
|
230
|
-
* List all worktrees for the current repository.
|
|
231
|
-
*/
|
|
232
|
-
listWorktreesCommand(threadId: string, _username: string): Promise<void>;
|
|
233
|
-
/**
|
|
234
|
-
* Remove a worktree.
|
|
235
|
-
*/
|
|
236
|
-
removeWorktreeCommand(threadId: string, branchOrPath: string, username: string): Promise<void>;
|
|
237
|
-
/**
|
|
238
|
-
* Disable worktree prompts for a session.
|
|
239
|
-
*/
|
|
240
|
-
disableWorktreePrompt(threadId: string, username: string): Promise<void>;
|
|
241
|
-
/**
|
|
242
|
-
* Check if a session has a pending worktree prompt.
|
|
243
|
-
*/
|
|
244
|
-
hasPendingWorktreePrompt(threadId: string): boolean;
|
|
245
|
-
/**
|
|
246
|
-
* Get the worktree prompt post ID for a session.
|
|
247
|
-
*/
|
|
248
|
-
getWorktreePromptPostId(threadId: string): string | undefined;
|
|
249
|
-
/** Kill all active sessions (for graceful shutdown) */
|
|
250
|
-
killAllSessions(): void;
|
|
251
|
-
/** Kill all sessions AND unpersist them (for emergency shutdown - no resume) */
|
|
252
|
-
killAllSessionsAndUnpersist(): void;
|
|
253
|
-
/** Cleanup idle sessions that have exceeded timeout */
|
|
254
|
-
private cleanupIdleSessions;
|
|
255
|
-
}
|
|
256
|
-
export {};
|