claude-threads 0.12.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 +473 -0
- package/LICENSE +21 -0
- package/README.md +303 -0
- package/dist/changelog.d.ts +20 -0
- package/dist/changelog.js +134 -0
- package/dist/claude/cli.d.ts +42 -0
- package/dist/claude/cli.js +173 -0
- package/dist/claude/session.d.ts +256 -0
- package/dist/claude/session.js +1964 -0
- package/dist/config.d.ts +27 -0
- package/dist/config.js +94 -0
- package/dist/git/worktree.d.ts +50 -0
- package/dist/git/worktree.js +228 -0
- package/dist/index.d.ts +2 -0
- package/dist/index.js +371 -0
- package/dist/logo.d.ts +31 -0
- package/dist/logo.js +57 -0
- package/dist/mattermost/api.d.ts +85 -0
- package/dist/mattermost/api.js +124 -0
- package/dist/mattermost/api.test.d.ts +1 -0
- package/dist/mattermost/api.test.js +319 -0
- package/dist/mattermost/client.d.ts +56 -0
- package/dist/mattermost/client.js +321 -0
- package/dist/mattermost/emoji.d.ts +43 -0
- package/dist/mattermost/emoji.js +65 -0
- package/dist/mattermost/emoji.test.d.ts +1 -0
- package/dist/mattermost/emoji.test.js +131 -0
- package/dist/mattermost/types.d.ts +71 -0
- package/dist/mattermost/types.js +1 -0
- package/dist/mcp/permission-server.d.ts +2 -0
- package/dist/mcp/permission-server.js +201 -0
- package/dist/onboarding.d.ts +1 -0
- package/dist/onboarding.js +116 -0
- package/dist/persistence/session-store.d.ts +65 -0
- package/dist/persistence/session-store.js +127 -0
- package/dist/update-notifier.d.ts +3 -0
- package/dist/update-notifier.js +31 -0
- package/dist/utils/logger.d.ts +34 -0
- package/dist/utils/logger.js +42 -0
- package/dist/utils/logger.test.d.ts +1 -0
- package/dist/utils/logger.test.js +121 -0
- package/dist/utils/tool-formatter.d.ts +56 -0
- package/dist/utils/tool-formatter.js +247 -0
- package/dist/utils/tool-formatter.test.d.ts +1 -0
- package/dist/utils/tool-formatter.test.js +357 -0
- package/package.json +85 -0
|
@@ -0,0 +1,247 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Tool formatting utilities for displaying Claude tool calls in Mattermost
|
|
3
|
+
*
|
|
4
|
+
* This module provides shared formatting logic used by both:
|
|
5
|
+
* - src/claude/session.ts (main bot)
|
|
6
|
+
* - src/mcp/permission-server.ts (MCP permission handler)
|
|
7
|
+
*/
|
|
8
|
+
import * as Diff from 'diff';
|
|
9
|
+
const DEFAULT_OPTIONS = {
|
|
10
|
+
detailed: false,
|
|
11
|
+
maxCommandLength: 50,
|
|
12
|
+
maxPathLength: 60,
|
|
13
|
+
maxPreviewLines: 20,
|
|
14
|
+
};
|
|
15
|
+
/**
|
|
16
|
+
* Shorten a file path for display by replacing home directory with ~
|
|
17
|
+
*/
|
|
18
|
+
export function shortenPath(path, homeDir) {
|
|
19
|
+
if (!path)
|
|
20
|
+
return '';
|
|
21
|
+
const home = homeDir ?? process.env.HOME ?? '';
|
|
22
|
+
if (home && path.startsWith(home)) {
|
|
23
|
+
return '~' + path.slice(home.length);
|
|
24
|
+
}
|
|
25
|
+
return path;
|
|
26
|
+
}
|
|
27
|
+
/**
|
|
28
|
+
* Check if a tool name is an MCP tool and extract server/tool parts
|
|
29
|
+
*/
|
|
30
|
+
export function parseMcpToolName(toolName) {
|
|
31
|
+
if (!toolName.startsWith('mcp__'))
|
|
32
|
+
return null;
|
|
33
|
+
const parts = toolName.split('__');
|
|
34
|
+
if (parts.length < 3)
|
|
35
|
+
return null;
|
|
36
|
+
return {
|
|
37
|
+
server: parts[1],
|
|
38
|
+
tool: parts.slice(2).join('__'),
|
|
39
|
+
};
|
|
40
|
+
}
|
|
41
|
+
/**
|
|
42
|
+
* Format a tool use for display in Mattermost
|
|
43
|
+
*
|
|
44
|
+
* @param toolName - The name of the tool being called
|
|
45
|
+
* @param input - The tool input parameters
|
|
46
|
+
* @param options - Formatting options
|
|
47
|
+
* @returns Formatted string or null if the tool should not be displayed
|
|
48
|
+
*/
|
|
49
|
+
export function formatToolUse(toolName, input, options = {}) {
|
|
50
|
+
const opts = { ...DEFAULT_OPTIONS, ...options };
|
|
51
|
+
const short = (p) => shortenPath(p);
|
|
52
|
+
switch (toolName) {
|
|
53
|
+
case 'Read':
|
|
54
|
+
return `📄 **Read** \`${short(input.file_path)}\``;
|
|
55
|
+
case 'Edit': {
|
|
56
|
+
const filePath = short(input.file_path);
|
|
57
|
+
const oldStr = input.old_string || '';
|
|
58
|
+
const newStr = input.new_string || '';
|
|
59
|
+
// Show diff if detailed mode and we have old/new strings
|
|
60
|
+
if (opts.detailed && (oldStr || newStr)) {
|
|
61
|
+
const changes = Diff.diffLines(oldStr, newStr);
|
|
62
|
+
const maxLines = opts.maxPreviewLines;
|
|
63
|
+
let lineCount = 0;
|
|
64
|
+
const diffLines = [];
|
|
65
|
+
for (const change of changes) {
|
|
66
|
+
const lines = change.value.replace(/\n$/, '').split('\n');
|
|
67
|
+
for (const line of lines) {
|
|
68
|
+
if (lineCount >= maxLines)
|
|
69
|
+
break;
|
|
70
|
+
if (change.added) {
|
|
71
|
+
diffLines.push(`+ ${line}`);
|
|
72
|
+
lineCount++;
|
|
73
|
+
}
|
|
74
|
+
else if (change.removed) {
|
|
75
|
+
diffLines.push(`- ${line}`);
|
|
76
|
+
lineCount++;
|
|
77
|
+
}
|
|
78
|
+
else {
|
|
79
|
+
diffLines.push(` ${line}`);
|
|
80
|
+
lineCount++;
|
|
81
|
+
}
|
|
82
|
+
}
|
|
83
|
+
if (lineCount >= maxLines)
|
|
84
|
+
break;
|
|
85
|
+
}
|
|
86
|
+
const totalLines = changes.reduce((sum, c) => sum + c.value.split('\n').length - 1, 0);
|
|
87
|
+
let diff = `✏️ **Edit** \`${filePath}\`\n\`\`\`diff\n`;
|
|
88
|
+
diff += diffLines.join('\n');
|
|
89
|
+
if (totalLines > maxLines) {
|
|
90
|
+
diff += `\n... (+${totalLines - maxLines} more lines)`;
|
|
91
|
+
}
|
|
92
|
+
diff += '\n```';
|
|
93
|
+
return diff;
|
|
94
|
+
}
|
|
95
|
+
return `✏️ **Edit** \`${filePath}\``;
|
|
96
|
+
}
|
|
97
|
+
case 'Write': {
|
|
98
|
+
const filePath = short(input.file_path);
|
|
99
|
+
const content = input.content || '';
|
|
100
|
+
const lines = content.split('\n');
|
|
101
|
+
const lineCount = lines.length;
|
|
102
|
+
// Show preview if detailed mode
|
|
103
|
+
if (opts.detailed && content && lineCount > 0) {
|
|
104
|
+
const maxLines = 6;
|
|
105
|
+
const previewLines = lines.slice(0, maxLines);
|
|
106
|
+
let preview = `📝 **Write** \`${filePath}\` *(${lineCount} lines)*\n\`\`\`\n`;
|
|
107
|
+
preview += previewLines.join('\n');
|
|
108
|
+
if (lineCount > maxLines) {
|
|
109
|
+
preview += `\n... (${lineCount - maxLines} more lines)`;
|
|
110
|
+
}
|
|
111
|
+
preview += '\n```';
|
|
112
|
+
return preview;
|
|
113
|
+
}
|
|
114
|
+
return `📝 **Write** \`${filePath}\``;
|
|
115
|
+
}
|
|
116
|
+
case 'Bash': {
|
|
117
|
+
const cmd = (input.command || '').substring(0, opts.maxCommandLength);
|
|
118
|
+
const truncated = cmd.length >= opts.maxCommandLength;
|
|
119
|
+
return `💻 **Bash** \`${cmd}${truncated ? '...' : ''}\``;
|
|
120
|
+
}
|
|
121
|
+
case 'Glob':
|
|
122
|
+
return `🔍 **Glob** \`${input.pattern}\``;
|
|
123
|
+
case 'Grep':
|
|
124
|
+
return `🔎 **Grep** \`${input.pattern}\``;
|
|
125
|
+
case 'Task':
|
|
126
|
+
return null; // Handled specially with subagent display
|
|
127
|
+
case 'EnterPlanMode':
|
|
128
|
+
return `📋 **Planning...**`;
|
|
129
|
+
case 'ExitPlanMode':
|
|
130
|
+
return null; // Handled specially with approval buttons
|
|
131
|
+
case 'AskUserQuestion':
|
|
132
|
+
return null; // Don't show, the question text follows
|
|
133
|
+
case 'TodoWrite':
|
|
134
|
+
return null; // Handled specially with task list display
|
|
135
|
+
case 'WebFetch': {
|
|
136
|
+
const url = (input.url || '').substring(0, 40);
|
|
137
|
+
return `🌐 **Fetching** \`${url}\``;
|
|
138
|
+
}
|
|
139
|
+
case 'WebSearch':
|
|
140
|
+
return `🔍 **Searching** \`${input.query}\``;
|
|
141
|
+
default: {
|
|
142
|
+
// Handle MCP tools: mcp__server__tool
|
|
143
|
+
const mcpParts = parseMcpToolName(toolName);
|
|
144
|
+
if (mcpParts) {
|
|
145
|
+
// Special formatting for Claude in Chrome tools
|
|
146
|
+
if (mcpParts.server === 'claude-in-chrome') {
|
|
147
|
+
return formatChromeToolUse(mcpParts.tool, input);
|
|
148
|
+
}
|
|
149
|
+
return `🔌 **${mcpParts.tool}** *(${mcpParts.server})*`;
|
|
150
|
+
}
|
|
151
|
+
return `● **${toolName}**`;
|
|
152
|
+
}
|
|
153
|
+
}
|
|
154
|
+
}
|
|
155
|
+
/**
|
|
156
|
+
* Format tool info for permission prompts (simpler format)
|
|
157
|
+
*
|
|
158
|
+
* @param toolName - The name of the tool
|
|
159
|
+
* @param input - The tool input parameters
|
|
160
|
+
* @returns Formatted string for permission prompts
|
|
161
|
+
*/
|
|
162
|
+
export function formatToolForPermission(toolName, input) {
|
|
163
|
+
const short = (p) => shortenPath(p);
|
|
164
|
+
switch (toolName) {
|
|
165
|
+
case 'Read':
|
|
166
|
+
return `📄 **Read** \`${short(input.file_path)}\``;
|
|
167
|
+
case 'Write':
|
|
168
|
+
return `📝 **Write** \`${short(input.file_path)}\``;
|
|
169
|
+
case 'Edit':
|
|
170
|
+
return `✏️ **Edit** \`${short(input.file_path)}\``;
|
|
171
|
+
case 'Bash': {
|
|
172
|
+
const cmd = (input.command || '').substring(0, 100);
|
|
173
|
+
return `💻 **Bash** \`${cmd}${cmd.length >= 100 ? '...' : ''}\``;
|
|
174
|
+
}
|
|
175
|
+
default: {
|
|
176
|
+
const mcpParts = parseMcpToolName(toolName);
|
|
177
|
+
if (mcpParts) {
|
|
178
|
+
return `🔌 **${mcpParts.tool}** *(${mcpParts.server})*`;
|
|
179
|
+
}
|
|
180
|
+
return `● **${toolName}**`;
|
|
181
|
+
}
|
|
182
|
+
}
|
|
183
|
+
}
|
|
184
|
+
/**
|
|
185
|
+
* Format Claude in Chrome tool calls
|
|
186
|
+
*
|
|
187
|
+
* @param tool - The Chrome tool name (after mcp__claude-in-chrome__)
|
|
188
|
+
* @param input - The tool input parameters
|
|
189
|
+
* @returns Formatted string for display
|
|
190
|
+
*/
|
|
191
|
+
export function formatChromeToolUse(tool, input) {
|
|
192
|
+
const action = input.action || '';
|
|
193
|
+
const coord = input.coordinate;
|
|
194
|
+
const url = input.url || '';
|
|
195
|
+
const text = input.text || '';
|
|
196
|
+
switch (tool) {
|
|
197
|
+
case 'computer': {
|
|
198
|
+
let detail = '';
|
|
199
|
+
switch (action) {
|
|
200
|
+
case 'screenshot':
|
|
201
|
+
detail = 'screenshot';
|
|
202
|
+
break;
|
|
203
|
+
case 'left_click':
|
|
204
|
+
case 'right_click':
|
|
205
|
+
case 'double_click':
|
|
206
|
+
case 'triple_click':
|
|
207
|
+
detail = coord ? `${action} at (${coord[0]}, ${coord[1]})` : action;
|
|
208
|
+
break;
|
|
209
|
+
case 'type':
|
|
210
|
+
detail = `type "${text.substring(0, 30)}${text.length > 30 ? '...' : ''}"`;
|
|
211
|
+
break;
|
|
212
|
+
case 'key':
|
|
213
|
+
detail = `key ${text}`;
|
|
214
|
+
break;
|
|
215
|
+
case 'scroll':
|
|
216
|
+
detail = `scroll ${input.scroll_direction || 'down'}`;
|
|
217
|
+
break;
|
|
218
|
+
case 'wait':
|
|
219
|
+
detail = `wait ${input.duration}s`;
|
|
220
|
+
break;
|
|
221
|
+
default:
|
|
222
|
+
detail = action || 'action';
|
|
223
|
+
}
|
|
224
|
+
return `🌐 **Chrome**[computer] \`${detail}\``;
|
|
225
|
+
}
|
|
226
|
+
case 'navigate':
|
|
227
|
+
return `🌐 **Chrome**[navigate] \`${url.substring(0, 50)}${url.length > 50 ? '...' : ''}\``;
|
|
228
|
+
case 'tabs_context_mcp':
|
|
229
|
+
return `🌐 **Chrome**[tabs] reading context`;
|
|
230
|
+
case 'tabs_create_mcp':
|
|
231
|
+
return `🌐 **Chrome**[tabs] creating new tab`;
|
|
232
|
+
case 'read_page':
|
|
233
|
+
return `🌐 **Chrome**[read_page] ${input.filter === 'interactive' ? 'interactive elements' : 'accessibility tree'}`;
|
|
234
|
+
case 'find':
|
|
235
|
+
return `🌐 **Chrome**[find] \`${input.query || ''}\``;
|
|
236
|
+
case 'form_input':
|
|
237
|
+
return `🌐 **Chrome**[form_input] setting value`;
|
|
238
|
+
case 'get_page_text':
|
|
239
|
+
return `🌐 **Chrome**[get_page_text] extracting content`;
|
|
240
|
+
case 'javascript_tool':
|
|
241
|
+
return `🌐 **Chrome**[javascript] executing script`;
|
|
242
|
+
case 'gif_creator':
|
|
243
|
+
return `🌐 **Chrome**[gif] ${action}`;
|
|
244
|
+
default:
|
|
245
|
+
return `🌐 **Chrome**[${tool}]`;
|
|
246
|
+
}
|
|
247
|
+
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|
|
@@ -0,0 +1,357 @@
|
|
|
1
|
+
import { describe, it, expect, beforeEach, afterEach } from 'vitest';
|
|
2
|
+
import { shortenPath, parseMcpToolName, formatToolUse, formatToolForPermission, } from './tool-formatter.js';
|
|
3
|
+
describe('shortenPath', () => {
|
|
4
|
+
const originalHome = process.env.HOME;
|
|
5
|
+
beforeEach(() => {
|
|
6
|
+
process.env.HOME = '/Users/testuser';
|
|
7
|
+
});
|
|
8
|
+
afterEach(() => {
|
|
9
|
+
process.env.HOME = originalHome;
|
|
10
|
+
});
|
|
11
|
+
it('replaces home directory with ~', () => {
|
|
12
|
+
expect(shortenPath('/Users/testuser/projects/file.ts')).toBe('~/projects/file.ts');
|
|
13
|
+
});
|
|
14
|
+
it('leaves paths not under home unchanged', () => {
|
|
15
|
+
expect(shortenPath('/var/log/file.log')).toBe('/var/log/file.log');
|
|
16
|
+
});
|
|
17
|
+
it('handles empty path', () => {
|
|
18
|
+
expect(shortenPath('')).toBe('');
|
|
19
|
+
});
|
|
20
|
+
it('uses provided homeDir over env', () => {
|
|
21
|
+
expect(shortenPath('/custom/home/file.ts', '/custom/home')).toBe('~/file.ts');
|
|
22
|
+
});
|
|
23
|
+
it('handles path equal to home', () => {
|
|
24
|
+
expect(shortenPath('/Users/testuser')).toBe('~');
|
|
25
|
+
});
|
|
26
|
+
});
|
|
27
|
+
describe('parseMcpToolName', () => {
|
|
28
|
+
it('parses valid MCP tool names', () => {
|
|
29
|
+
expect(parseMcpToolName('mcp__server__tool')).toEqual({
|
|
30
|
+
server: 'server',
|
|
31
|
+
tool: 'tool',
|
|
32
|
+
});
|
|
33
|
+
});
|
|
34
|
+
it('handles tool names with underscores', () => {
|
|
35
|
+
expect(parseMcpToolName('mcp__my-server__my_complex__tool')).toEqual({
|
|
36
|
+
server: 'my-server',
|
|
37
|
+
tool: 'my_complex__tool',
|
|
38
|
+
});
|
|
39
|
+
});
|
|
40
|
+
it('returns null for non-MCP tools', () => {
|
|
41
|
+
expect(parseMcpToolName('Read')).toBeNull();
|
|
42
|
+
expect(parseMcpToolName('Write')).toBeNull();
|
|
43
|
+
expect(parseMcpToolName('Bash')).toBeNull();
|
|
44
|
+
});
|
|
45
|
+
it('returns null for invalid MCP format', () => {
|
|
46
|
+
expect(parseMcpToolName('mcp__')).toBeNull();
|
|
47
|
+
expect(parseMcpToolName('mcp__server')).toBeNull();
|
|
48
|
+
});
|
|
49
|
+
it('handles claude-in-chrome tools', () => {
|
|
50
|
+
expect(parseMcpToolName('mcp__claude-in-chrome__computer')).toEqual({
|
|
51
|
+
server: 'claude-in-chrome',
|
|
52
|
+
tool: 'computer',
|
|
53
|
+
});
|
|
54
|
+
});
|
|
55
|
+
});
|
|
56
|
+
describe('formatToolUse', () => {
|
|
57
|
+
const originalHome = process.env.HOME;
|
|
58
|
+
beforeEach(() => {
|
|
59
|
+
process.env.HOME = '/Users/testuser';
|
|
60
|
+
});
|
|
61
|
+
afterEach(() => {
|
|
62
|
+
process.env.HOME = originalHome;
|
|
63
|
+
});
|
|
64
|
+
describe('Read tool', () => {
|
|
65
|
+
it('formats Read with file path', () => {
|
|
66
|
+
const result = formatToolUse('Read', {
|
|
67
|
+
file_path: '/Users/testuser/file.ts',
|
|
68
|
+
});
|
|
69
|
+
expect(result).toBe('📄 **Read** `~/file.ts`');
|
|
70
|
+
});
|
|
71
|
+
it('shows full path when not under home', () => {
|
|
72
|
+
const result = formatToolUse('Read', { file_path: '/var/log/app.log' });
|
|
73
|
+
expect(result).toBe('📄 **Read** `/var/log/app.log`');
|
|
74
|
+
});
|
|
75
|
+
});
|
|
76
|
+
describe('Edit tool', () => {
|
|
77
|
+
it('formats Edit without diff in non-detailed mode', () => {
|
|
78
|
+
const result = formatToolUse('Edit', {
|
|
79
|
+
file_path: '/Users/testuser/file.ts',
|
|
80
|
+
old_string: 'old',
|
|
81
|
+
new_string: 'new',
|
|
82
|
+
});
|
|
83
|
+
expect(result).toBe('✏️ **Edit** `~/file.ts`');
|
|
84
|
+
});
|
|
85
|
+
it('formats Edit with diff in detailed mode', () => {
|
|
86
|
+
const result = formatToolUse('Edit', {
|
|
87
|
+
file_path: '/Users/testuser/file.ts',
|
|
88
|
+
old_string: 'old line',
|
|
89
|
+
new_string: 'new line',
|
|
90
|
+
}, { detailed: true });
|
|
91
|
+
expect(result).toContain('✏️ **Edit** `~/file.ts`');
|
|
92
|
+
expect(result).toContain('```diff');
|
|
93
|
+
expect(result).toContain('- old line');
|
|
94
|
+
expect(result).toContain('+ new line');
|
|
95
|
+
});
|
|
96
|
+
it('truncates long diffs', () => {
|
|
97
|
+
const oldLines = Array(30).fill('old line').join('\n');
|
|
98
|
+
const newLines = Array(30).fill('new line').join('\n');
|
|
99
|
+
const result = formatToolUse('Edit', {
|
|
100
|
+
file_path: '/Users/testuser/file.ts',
|
|
101
|
+
old_string: oldLines,
|
|
102
|
+
new_string: newLines,
|
|
103
|
+
}, { detailed: true, maxPreviewLines: 10 });
|
|
104
|
+
expect(result).toContain('more lines');
|
|
105
|
+
});
|
|
106
|
+
});
|
|
107
|
+
describe('Write tool', () => {
|
|
108
|
+
it('formats Write without preview in non-detailed mode', () => {
|
|
109
|
+
const result = formatToolUse('Write', {
|
|
110
|
+
file_path: '/Users/testuser/file.ts',
|
|
111
|
+
content: 'hello world',
|
|
112
|
+
});
|
|
113
|
+
expect(result).toBe('📝 **Write** `~/file.ts`');
|
|
114
|
+
});
|
|
115
|
+
it('formats Write with preview in detailed mode', () => {
|
|
116
|
+
const result = formatToolUse('Write', {
|
|
117
|
+
file_path: '/Users/testuser/file.ts',
|
|
118
|
+
content: 'line 1\nline 2\nline 3',
|
|
119
|
+
}, { detailed: true });
|
|
120
|
+
expect(result).toContain('📝 **Write** `~/file.ts`');
|
|
121
|
+
expect(result).toContain('*(3 lines)*');
|
|
122
|
+
expect(result).toContain('line 1');
|
|
123
|
+
});
|
|
124
|
+
it('truncates long content previews', () => {
|
|
125
|
+
const content = Array(20).fill('line').join('\n');
|
|
126
|
+
const result = formatToolUse('Write', {
|
|
127
|
+
file_path: '/Users/testuser/file.ts',
|
|
128
|
+
content,
|
|
129
|
+
}, { detailed: true });
|
|
130
|
+
expect(result).toContain('more lines');
|
|
131
|
+
});
|
|
132
|
+
});
|
|
133
|
+
describe('Bash tool', () => {
|
|
134
|
+
it('formats short commands', () => {
|
|
135
|
+
const result = formatToolUse('Bash', { command: 'ls -la' });
|
|
136
|
+
expect(result).toBe('💻 **Bash** `ls -la`');
|
|
137
|
+
});
|
|
138
|
+
it('truncates long commands', () => {
|
|
139
|
+
const longCmd = 'x'.repeat(100);
|
|
140
|
+
const result = formatToolUse('Bash', { command: longCmd });
|
|
141
|
+
expect(result).not.toBeNull();
|
|
142
|
+
expect(result).toContain('...');
|
|
143
|
+
expect(result.length).toBeLessThan(120);
|
|
144
|
+
});
|
|
145
|
+
it('respects custom maxCommandLength', () => {
|
|
146
|
+
const result = formatToolUse('Bash', { command: '1234567890' }, { maxCommandLength: 5 });
|
|
147
|
+
expect(result).toBe('💻 **Bash** `12345...`');
|
|
148
|
+
});
|
|
149
|
+
});
|
|
150
|
+
describe('Other tools', () => {
|
|
151
|
+
it('formats Glob', () => {
|
|
152
|
+
const result = formatToolUse('Glob', { pattern: '**/*.ts' });
|
|
153
|
+
expect(result).toBe('🔍 **Glob** `**/*.ts`');
|
|
154
|
+
});
|
|
155
|
+
it('formats Grep', () => {
|
|
156
|
+
const result = formatToolUse('Grep', { pattern: 'TODO' });
|
|
157
|
+
expect(result).toBe('🔎 **Grep** `TODO`');
|
|
158
|
+
});
|
|
159
|
+
it('formats EnterPlanMode', () => {
|
|
160
|
+
const result = formatToolUse('EnterPlanMode', {});
|
|
161
|
+
expect(result).toBe('📋 **Planning...**');
|
|
162
|
+
});
|
|
163
|
+
it('formats WebFetch', () => {
|
|
164
|
+
const result = formatToolUse('WebFetch', {
|
|
165
|
+
url: 'https://example.com/page',
|
|
166
|
+
});
|
|
167
|
+
expect(result).toBe('🌐 **Fetching** `https://example.com/page`');
|
|
168
|
+
});
|
|
169
|
+
it('formats WebSearch', () => {
|
|
170
|
+
const result = formatToolUse('WebSearch', { query: 'typescript guide' });
|
|
171
|
+
expect(result).toBe('🔍 **Searching** `typescript guide`');
|
|
172
|
+
});
|
|
173
|
+
});
|
|
174
|
+
describe('Tools that return null', () => {
|
|
175
|
+
it('returns null for Task', () => {
|
|
176
|
+
expect(formatToolUse('Task', {})).toBeNull();
|
|
177
|
+
});
|
|
178
|
+
it('returns null for ExitPlanMode', () => {
|
|
179
|
+
expect(formatToolUse('ExitPlanMode', {})).toBeNull();
|
|
180
|
+
});
|
|
181
|
+
it('returns null for AskUserQuestion', () => {
|
|
182
|
+
expect(formatToolUse('AskUserQuestion', {})).toBeNull();
|
|
183
|
+
});
|
|
184
|
+
it('returns null for TodoWrite', () => {
|
|
185
|
+
expect(formatToolUse('TodoWrite', {})).toBeNull();
|
|
186
|
+
});
|
|
187
|
+
});
|
|
188
|
+
describe('MCP tools', () => {
|
|
189
|
+
it('formats MCP tools', () => {
|
|
190
|
+
const result = formatToolUse('mcp__myserver__mytool', { arg: 'value' });
|
|
191
|
+
expect(result).toBe('🔌 **mytool** *(myserver)*');
|
|
192
|
+
});
|
|
193
|
+
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** *(my_server)*');
|
|
196
|
+
});
|
|
197
|
+
});
|
|
198
|
+
describe('Claude in Chrome tools', () => {
|
|
199
|
+
it('formats computer screenshot action', () => {
|
|
200
|
+
const result = formatToolUse('mcp__claude-in-chrome__computer', { action: 'screenshot' });
|
|
201
|
+
expect(result).toBe('🌐 **Chrome**[computer] `screenshot`');
|
|
202
|
+
});
|
|
203
|
+
it('formats computer click actions with coordinates', () => {
|
|
204
|
+
const result = formatToolUse('mcp__claude-in-chrome__computer', {
|
|
205
|
+
action: 'left_click',
|
|
206
|
+
coordinate: [100, 200],
|
|
207
|
+
});
|
|
208
|
+
expect(result).toBe('🌐 **Chrome**[computer] `left_click at (100, 200)`');
|
|
209
|
+
});
|
|
210
|
+
it('formats computer type action', () => {
|
|
211
|
+
const result = formatToolUse('mcp__claude-in-chrome__computer', {
|
|
212
|
+
action: 'type',
|
|
213
|
+
text: 'hello world',
|
|
214
|
+
});
|
|
215
|
+
expect(result).toBe('🌐 **Chrome**[computer] `type "hello world"`');
|
|
216
|
+
});
|
|
217
|
+
it('truncates long type text', () => {
|
|
218
|
+
const result = formatToolUse('mcp__claude-in-chrome__computer', {
|
|
219
|
+
action: 'type',
|
|
220
|
+
text: 'this is a very long text that should be truncated',
|
|
221
|
+
});
|
|
222
|
+
expect(result).toContain('...');
|
|
223
|
+
});
|
|
224
|
+
it('formats computer key action', () => {
|
|
225
|
+
const result = formatToolUse('mcp__claude-in-chrome__computer', {
|
|
226
|
+
action: 'key',
|
|
227
|
+
text: 'Enter',
|
|
228
|
+
});
|
|
229
|
+
expect(result).toBe('🌐 **Chrome**[computer] `key Enter`');
|
|
230
|
+
});
|
|
231
|
+
it('formats computer scroll action', () => {
|
|
232
|
+
const result = formatToolUse('mcp__claude-in-chrome__computer', {
|
|
233
|
+
action: 'scroll',
|
|
234
|
+
scroll_direction: 'up',
|
|
235
|
+
});
|
|
236
|
+
expect(result).toBe('🌐 **Chrome**[computer] `scroll up`');
|
|
237
|
+
});
|
|
238
|
+
it('formats computer wait action', () => {
|
|
239
|
+
const result = formatToolUse('mcp__claude-in-chrome__computer', {
|
|
240
|
+
action: 'wait',
|
|
241
|
+
duration: 2,
|
|
242
|
+
});
|
|
243
|
+
expect(result).toBe('🌐 **Chrome**[computer] `wait 2s`');
|
|
244
|
+
});
|
|
245
|
+
it('formats navigate tool', () => {
|
|
246
|
+
const result = formatToolUse('mcp__claude-in-chrome__navigate', {
|
|
247
|
+
url: 'https://example.com/page',
|
|
248
|
+
});
|
|
249
|
+
expect(result).toBe('🌐 **Chrome**[navigate] `https://example.com/page`');
|
|
250
|
+
});
|
|
251
|
+
it('truncates long URLs in navigate', () => {
|
|
252
|
+
const result = formatToolUse('mcp__claude-in-chrome__navigate', {
|
|
253
|
+
url: 'https://example.com/' + 'x'.repeat(100),
|
|
254
|
+
});
|
|
255
|
+
expect(result).toContain('...');
|
|
256
|
+
});
|
|
257
|
+
it('formats tabs_context_mcp tool', () => {
|
|
258
|
+
const result = formatToolUse('mcp__claude-in-chrome__tabs_context_mcp', {});
|
|
259
|
+
expect(result).toBe('🌐 **Chrome**[tabs] reading context');
|
|
260
|
+
});
|
|
261
|
+
it('formats tabs_create_mcp tool', () => {
|
|
262
|
+
const result = formatToolUse('mcp__claude-in-chrome__tabs_create_mcp', {});
|
|
263
|
+
expect(result).toBe('🌐 **Chrome**[tabs] creating new tab');
|
|
264
|
+
});
|
|
265
|
+
it('formats read_page tool', () => {
|
|
266
|
+
const result = formatToolUse('mcp__claude-in-chrome__read_page', {});
|
|
267
|
+
expect(result).toBe('🌐 **Chrome**[read_page] accessibility tree');
|
|
268
|
+
});
|
|
269
|
+
it('formats read_page tool with interactive filter', () => {
|
|
270
|
+
const result = formatToolUse('mcp__claude-in-chrome__read_page', {
|
|
271
|
+
filter: 'interactive',
|
|
272
|
+
});
|
|
273
|
+
expect(result).toBe('🌐 **Chrome**[read_page] interactive elements');
|
|
274
|
+
});
|
|
275
|
+
it('formats find tool', () => {
|
|
276
|
+
const result = formatToolUse('mcp__claude-in-chrome__find', {
|
|
277
|
+
query: 'login button',
|
|
278
|
+
});
|
|
279
|
+
expect(result).toBe('🌐 **Chrome**[find] `login button`');
|
|
280
|
+
});
|
|
281
|
+
it('formats form_input tool', () => {
|
|
282
|
+
const result = formatToolUse('mcp__claude-in-chrome__form_input', {
|
|
283
|
+
ref: 'ref_1',
|
|
284
|
+
value: 'test',
|
|
285
|
+
});
|
|
286
|
+
expect(result).toBe('🌐 **Chrome**[form_input] setting value');
|
|
287
|
+
});
|
|
288
|
+
it('formats get_page_text tool', () => {
|
|
289
|
+
const result = formatToolUse('mcp__claude-in-chrome__get_page_text', {});
|
|
290
|
+
expect(result).toBe('🌐 **Chrome**[get_page_text] extracting content');
|
|
291
|
+
});
|
|
292
|
+
it('formats javascript_tool', () => {
|
|
293
|
+
const result = formatToolUse('mcp__claude-in-chrome__javascript_tool', {
|
|
294
|
+
text: 'document.title',
|
|
295
|
+
});
|
|
296
|
+
expect(result).toBe('🌐 **Chrome**[javascript] executing script');
|
|
297
|
+
});
|
|
298
|
+
it('formats gif_creator tool', () => {
|
|
299
|
+
const result = formatToolUse('mcp__claude-in-chrome__gif_creator', {
|
|
300
|
+
action: 'start_recording',
|
|
301
|
+
});
|
|
302
|
+
expect(result).toBe('🌐 **Chrome**[gif] start_recording');
|
|
303
|
+
});
|
|
304
|
+
it('formats unknown Chrome tools', () => {
|
|
305
|
+
const result = formatToolUse('mcp__claude-in-chrome__new_tool', {});
|
|
306
|
+
expect(result).toBe('🌐 **Chrome**[new_tool]');
|
|
307
|
+
});
|
|
308
|
+
});
|
|
309
|
+
describe('Unknown tools', () => {
|
|
310
|
+
it('formats unknown tools with bullet', () => {
|
|
311
|
+
const result = formatToolUse('CustomTool', {});
|
|
312
|
+
expect(result).toBe('● **CustomTool**');
|
|
313
|
+
});
|
|
314
|
+
});
|
|
315
|
+
});
|
|
316
|
+
describe('formatToolForPermission', () => {
|
|
317
|
+
const originalHome = process.env.HOME;
|
|
318
|
+
beforeEach(() => {
|
|
319
|
+
process.env.HOME = '/Users/testuser';
|
|
320
|
+
});
|
|
321
|
+
afterEach(() => {
|
|
322
|
+
process.env.HOME = originalHome;
|
|
323
|
+
});
|
|
324
|
+
it('formats Read tool', () => {
|
|
325
|
+
const result = formatToolForPermission('Read', {
|
|
326
|
+
file_path: '/Users/testuser/file.ts',
|
|
327
|
+
});
|
|
328
|
+
expect(result).toBe('📄 **Read** `~/file.ts`');
|
|
329
|
+
});
|
|
330
|
+
it('formats Write tool', () => {
|
|
331
|
+
const result = formatToolForPermission('Write', {
|
|
332
|
+
file_path: '/Users/testuser/file.ts',
|
|
333
|
+
});
|
|
334
|
+
expect(result).toBe('📝 **Write** `~/file.ts`');
|
|
335
|
+
});
|
|
336
|
+
it('formats Edit tool', () => {
|
|
337
|
+
const result = formatToolForPermission('Edit', {
|
|
338
|
+
file_path: '/Users/testuser/file.ts',
|
|
339
|
+
});
|
|
340
|
+
expect(result).toBe('✏️ **Edit** `~/file.ts`');
|
|
341
|
+
});
|
|
342
|
+
it('formats Bash with longer truncation limit (100 chars)', () => {
|
|
343
|
+
const cmd = 'x'.repeat(100);
|
|
344
|
+
const result = formatToolForPermission('Bash', { command: cmd });
|
|
345
|
+
// Should truncate at 100
|
|
346
|
+
expect(result).toContain('...');
|
|
347
|
+
expect(result.length).toBeLessThan(150);
|
|
348
|
+
});
|
|
349
|
+
it('formats MCP tools', () => {
|
|
350
|
+
const result = formatToolForPermission('mcp__server__tool', {});
|
|
351
|
+
expect(result).toBe('🔌 **tool** *(server)*');
|
|
352
|
+
});
|
|
353
|
+
it('formats unknown tools', () => {
|
|
354
|
+
const result = formatToolForPermission('CustomTool', {});
|
|
355
|
+
expect(result).toBe('● **CustomTool**');
|
|
356
|
+
});
|
|
357
|
+
});
|