claude-threads 0.13.0 → 0.14.1
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/update-notifier.js +10 -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 +4 -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
|
@@ -0,0 +1,339 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Git worktree management utilities
|
|
3
|
+
*
|
|
4
|
+
* Handles worktree prompts, creation, switching, and cleanup.
|
|
5
|
+
*/
|
|
6
|
+
import { isGitRepository, getRepositoryRoot, hasUncommittedChanges, listWorktrees as listGitWorktrees, createWorktree as createGitWorktree, removeWorktree as removeGitWorktree, getWorktreeDir, findWorktreeByBranch, isValidBranchName, } from '../git/worktree.js';
|
|
7
|
+
import { ClaudeCli } from '../claude/cli.js';
|
|
8
|
+
import { randomUUID } from 'crypto';
|
|
9
|
+
/**
|
|
10
|
+
* Check if we should prompt the user to create a worktree.
|
|
11
|
+
* Returns the reason for prompting, or null if we shouldn't prompt.
|
|
12
|
+
*/
|
|
13
|
+
export async function shouldPromptForWorktree(session, worktreeMode, hasOtherSessionInRepo) {
|
|
14
|
+
// Skip if worktree mode is off
|
|
15
|
+
if (worktreeMode === 'off')
|
|
16
|
+
return null;
|
|
17
|
+
// Skip if user disabled prompts for this session
|
|
18
|
+
if (session.worktreePromptDisabled)
|
|
19
|
+
return null;
|
|
20
|
+
// Skip if already in a worktree
|
|
21
|
+
if (session.worktreeInfo)
|
|
22
|
+
return null;
|
|
23
|
+
// Check if we're in a git repository
|
|
24
|
+
const isRepo = await isGitRepository(session.workingDir);
|
|
25
|
+
if (!isRepo)
|
|
26
|
+
return null;
|
|
27
|
+
// For 'require' mode, always prompt
|
|
28
|
+
if (worktreeMode === 'require') {
|
|
29
|
+
return 'require';
|
|
30
|
+
}
|
|
31
|
+
// For 'prompt' mode, check conditions
|
|
32
|
+
// Condition 1: uncommitted changes
|
|
33
|
+
const hasChanges = await hasUncommittedChanges(session.workingDir);
|
|
34
|
+
if (hasChanges)
|
|
35
|
+
return 'uncommitted';
|
|
36
|
+
// Condition 2: another session using the same repo
|
|
37
|
+
const repoRoot = await getRepositoryRoot(session.workingDir);
|
|
38
|
+
const hasConcurrent = hasOtherSessionInRepo(repoRoot, session.threadId);
|
|
39
|
+
if (hasConcurrent)
|
|
40
|
+
return 'concurrent';
|
|
41
|
+
return null;
|
|
42
|
+
}
|
|
43
|
+
/**
|
|
44
|
+
* Post the worktree prompt message to the user.
|
|
45
|
+
*/
|
|
46
|
+
export async function postWorktreePrompt(session, reason, registerPost) {
|
|
47
|
+
let message;
|
|
48
|
+
switch (reason) {
|
|
49
|
+
case 'uncommitted':
|
|
50
|
+
message = `🌿 **This repo has uncommitted changes.**\n` +
|
|
51
|
+
`Reply with a branch name to work in an isolated worktree, or react with ❌ to continue in the main repo.`;
|
|
52
|
+
break;
|
|
53
|
+
case 'concurrent':
|
|
54
|
+
message = `⚠️ **Another session is already using this repo.**\n` +
|
|
55
|
+
`Reply with a branch name to work in an isolated worktree, or react with ❌ to continue anyway.`;
|
|
56
|
+
break;
|
|
57
|
+
case 'require':
|
|
58
|
+
message = `🌿 **This deployment requires working in a worktree.**\n` +
|
|
59
|
+
`Please reply with a branch name to continue.`;
|
|
60
|
+
break;
|
|
61
|
+
default:
|
|
62
|
+
message = `🌿 **Would you like to work in an isolated worktree?**\n` +
|
|
63
|
+
`Reply with a branch name, or react with ❌ to continue in the main repo.`;
|
|
64
|
+
}
|
|
65
|
+
// Create post with ❌ reaction option (except for 'require' mode)
|
|
66
|
+
// Use 'x' emoji name, not Unicode ❌ character
|
|
67
|
+
const reactionOptions = reason === 'require' ? [] : ['x'];
|
|
68
|
+
const post = await session.platform.createInteractivePost(message, reactionOptions, session.threadId);
|
|
69
|
+
// Track the post for reaction handling
|
|
70
|
+
session.worktreePromptPostId = post.id;
|
|
71
|
+
registerPost(post.id, session.threadId);
|
|
72
|
+
}
|
|
73
|
+
/**
|
|
74
|
+
* Handle user providing a branch name in response to worktree prompt.
|
|
75
|
+
* Returns true if handled (whether successful or not).
|
|
76
|
+
*/
|
|
77
|
+
export async function handleWorktreeBranchResponse(session, branchName, username, createAndSwitch) {
|
|
78
|
+
if (!session.pendingWorktreePrompt)
|
|
79
|
+
return false;
|
|
80
|
+
// Only session owner can respond
|
|
81
|
+
if (session.startedBy !== username && !session.platform.isUserAllowed(username)) {
|
|
82
|
+
return false;
|
|
83
|
+
}
|
|
84
|
+
// Validate branch name
|
|
85
|
+
if (!isValidBranchName(branchName)) {
|
|
86
|
+
await session.platform.createPost(`❌ Invalid branch name: \`${branchName}\`. Please provide a valid git branch name.`, session.threadId);
|
|
87
|
+
return true; // We handled it, but need another response
|
|
88
|
+
}
|
|
89
|
+
// Create and switch to worktree
|
|
90
|
+
await createAndSwitch(session.threadId, branchName, username);
|
|
91
|
+
return true;
|
|
92
|
+
}
|
|
93
|
+
/**
|
|
94
|
+
* Handle ❌ reaction on worktree prompt - skip worktree and continue in main repo.
|
|
95
|
+
*/
|
|
96
|
+
export async function handleWorktreeSkip(session, username, persistSession, startTyping) {
|
|
97
|
+
if (!session.pendingWorktreePrompt)
|
|
98
|
+
return;
|
|
99
|
+
// Only session owner can skip
|
|
100
|
+
if (session.startedBy !== username && !session.platform.isUserAllowed(username)) {
|
|
101
|
+
return;
|
|
102
|
+
}
|
|
103
|
+
// Update the prompt post
|
|
104
|
+
if (session.worktreePromptPostId) {
|
|
105
|
+
try {
|
|
106
|
+
await session.platform.updatePost(session.worktreePromptPostId, `✅ Continuing in main repo (skipped by @${username})`);
|
|
107
|
+
}
|
|
108
|
+
catch (err) {
|
|
109
|
+
console.error(' ⚠️ Failed to update worktree prompt:', err);
|
|
110
|
+
}
|
|
111
|
+
}
|
|
112
|
+
// Clear pending state
|
|
113
|
+
session.pendingWorktreePrompt = false;
|
|
114
|
+
session.worktreePromptPostId = undefined;
|
|
115
|
+
const queuedPrompt = session.queuedPrompt;
|
|
116
|
+
session.queuedPrompt = undefined;
|
|
117
|
+
// Persist updated state
|
|
118
|
+
persistSession(session);
|
|
119
|
+
// Now send the queued message to Claude
|
|
120
|
+
if (queuedPrompt && session.claude.isRunning()) {
|
|
121
|
+
session.claude.sendMessage(queuedPrompt);
|
|
122
|
+
startTyping(session);
|
|
123
|
+
}
|
|
124
|
+
}
|
|
125
|
+
/**
|
|
126
|
+
* Create a new worktree and switch the session to it.
|
|
127
|
+
*/
|
|
128
|
+
export async function createAndSwitchToWorktree(session, branch, username, options) {
|
|
129
|
+
// Only session owner or admins can manage worktrees
|
|
130
|
+
if (session.startedBy !== username && !session.platform.isUserAllowed(username)) {
|
|
131
|
+
await session.platform.createPost(`⚠️ Only @${session.startedBy} or allowed users can manage worktrees`, session.threadId);
|
|
132
|
+
return;
|
|
133
|
+
}
|
|
134
|
+
// Check if we're in a git repo
|
|
135
|
+
const isRepo = await isGitRepository(session.workingDir);
|
|
136
|
+
if (!isRepo) {
|
|
137
|
+
await session.platform.createPost(`❌ Current directory is not a git repository`, session.threadId);
|
|
138
|
+
return;
|
|
139
|
+
}
|
|
140
|
+
// Get repo root
|
|
141
|
+
const repoRoot = await getRepositoryRoot(session.workingDir);
|
|
142
|
+
// Check if worktree already exists for this branch
|
|
143
|
+
const existing = await findWorktreeByBranch(repoRoot, branch);
|
|
144
|
+
if (existing && !existing.isMain) {
|
|
145
|
+
await session.platform.createPost(`⚠️ Worktree for branch \`${branch}\` already exists at \`${existing.path}\`. Use \`!worktree switch ${branch}\` to switch to it.`, session.threadId);
|
|
146
|
+
return;
|
|
147
|
+
}
|
|
148
|
+
const shortId = session.threadId.substring(0, 8);
|
|
149
|
+
console.log(` 🌿 Session (${shortId}…) creating worktree for branch ${branch}`);
|
|
150
|
+
// Generate worktree path
|
|
151
|
+
const worktreePath = getWorktreeDir(repoRoot, branch);
|
|
152
|
+
try {
|
|
153
|
+
// Create the worktree
|
|
154
|
+
await createGitWorktree(repoRoot, branch, worktreePath);
|
|
155
|
+
// Update the prompt post if it exists
|
|
156
|
+
if (session.worktreePromptPostId) {
|
|
157
|
+
try {
|
|
158
|
+
await session.platform.updatePost(session.worktreePromptPostId, `✅ Created worktree for \`${branch}\``);
|
|
159
|
+
}
|
|
160
|
+
catch (err) {
|
|
161
|
+
console.error(' ⚠️ Failed to update worktree prompt:', err);
|
|
162
|
+
}
|
|
163
|
+
}
|
|
164
|
+
// Clear pending state
|
|
165
|
+
const wasPending = session.pendingWorktreePrompt;
|
|
166
|
+
session.pendingWorktreePrompt = false;
|
|
167
|
+
session.worktreePromptPostId = undefined;
|
|
168
|
+
const queuedPrompt = session.queuedPrompt;
|
|
169
|
+
session.queuedPrompt = undefined;
|
|
170
|
+
// Store worktree info
|
|
171
|
+
session.worktreeInfo = {
|
|
172
|
+
repoRoot,
|
|
173
|
+
worktreePath,
|
|
174
|
+
branch,
|
|
175
|
+
};
|
|
176
|
+
// Update working directory
|
|
177
|
+
session.workingDir = worktreePath;
|
|
178
|
+
// If Claude is already running, restart it in the new directory
|
|
179
|
+
if (session.claude.isRunning()) {
|
|
180
|
+
options.stopTyping(session);
|
|
181
|
+
session.isRestarting = true;
|
|
182
|
+
session.claude.kill();
|
|
183
|
+
// Flush any pending content
|
|
184
|
+
await options.flush(session);
|
|
185
|
+
session.currentPostId = null;
|
|
186
|
+
session.pendingContent = '';
|
|
187
|
+
// Generate new session ID for fresh start in new directory
|
|
188
|
+
// (Claude CLI sessions are tied to working directory, can't resume across directories)
|
|
189
|
+
const newSessionId = randomUUID();
|
|
190
|
+
session.claudeSessionId = newSessionId;
|
|
191
|
+
// Create new CLI with new working directory
|
|
192
|
+
const cliOptions = {
|
|
193
|
+
workingDir: worktreePath,
|
|
194
|
+
threadId: session.threadId,
|
|
195
|
+
skipPermissions: options.skipPermissions || !session.forceInteractivePermissions,
|
|
196
|
+
sessionId: newSessionId,
|
|
197
|
+
resume: false, // Fresh start - can't resume across directories
|
|
198
|
+
chrome: options.chromeEnabled,
|
|
199
|
+
platformConfig: session.platform.getMcpConfig(),
|
|
200
|
+
};
|
|
201
|
+
session.claude = new ClaudeCli(cliOptions);
|
|
202
|
+
// Rebind event handlers (use sessionId which is the composite key)
|
|
203
|
+
session.claude.on('event', (e) => options.handleEvent(session.sessionId, e));
|
|
204
|
+
session.claude.on('exit', (code) => options.handleExit(session.sessionId, code));
|
|
205
|
+
// Start the new CLI
|
|
206
|
+
session.claude.start();
|
|
207
|
+
}
|
|
208
|
+
// Update session header
|
|
209
|
+
await options.updateSessionHeader(session);
|
|
210
|
+
// Post confirmation
|
|
211
|
+
const shortWorktreePath = worktreePath.replace(process.env.HOME || '', '~');
|
|
212
|
+
await session.platform.createPost(`✅ **Created worktree** for branch \`${branch}\`\n📁 Working directory: \`${shortWorktreePath}\`\n*Claude Code restarted in the new worktree*`, session.threadId);
|
|
213
|
+
// Update activity and persist
|
|
214
|
+
session.lastActivityAt = new Date();
|
|
215
|
+
session.timeoutWarningPosted = false;
|
|
216
|
+
options.persistSession(session);
|
|
217
|
+
// If there was a queued prompt (from initial session start), send it now
|
|
218
|
+
if (wasPending && queuedPrompt && session.claude.isRunning()) {
|
|
219
|
+
session.claude.sendMessage(queuedPrompt);
|
|
220
|
+
options.startTyping(session);
|
|
221
|
+
}
|
|
222
|
+
console.log(` 🌿 Session (${shortId}…) switched to worktree ${branch} at ${shortWorktreePath}`);
|
|
223
|
+
}
|
|
224
|
+
catch (err) {
|
|
225
|
+
console.error(` ❌ Failed to create worktree:`, err);
|
|
226
|
+
await session.platform.createPost(`❌ Failed to create worktree: ${err instanceof Error ? err.message : String(err)}`, session.threadId);
|
|
227
|
+
}
|
|
228
|
+
}
|
|
229
|
+
/**
|
|
230
|
+
* Switch to an existing worktree.
|
|
231
|
+
*/
|
|
232
|
+
export async function switchToWorktree(session, branchOrPath, username, changeDirectory) {
|
|
233
|
+
// Only session owner or admins can manage worktrees
|
|
234
|
+
if (session.startedBy !== username && !session.platform.isUserAllowed(username)) {
|
|
235
|
+
await session.platform.createPost(`⚠️ Only @${session.startedBy} or allowed users can manage worktrees`, session.threadId);
|
|
236
|
+
return;
|
|
237
|
+
}
|
|
238
|
+
// Get current repo root
|
|
239
|
+
const repoRoot = session.worktreeInfo?.repoRoot || await getRepositoryRoot(session.workingDir);
|
|
240
|
+
// Find the worktree
|
|
241
|
+
const worktrees = await listGitWorktrees(repoRoot);
|
|
242
|
+
const target = worktrees.find(wt => wt.branch === branchOrPath ||
|
|
243
|
+
wt.path === branchOrPath ||
|
|
244
|
+
wt.path.endsWith(branchOrPath));
|
|
245
|
+
if (!target) {
|
|
246
|
+
await session.platform.createPost(`❌ Worktree not found: \`${branchOrPath}\`. Use \`!worktree list\` to see available worktrees.`, session.threadId);
|
|
247
|
+
return;
|
|
248
|
+
}
|
|
249
|
+
// Use changeDirectory logic to switch
|
|
250
|
+
await changeDirectory(session.threadId, target.path, username);
|
|
251
|
+
// Update worktree info
|
|
252
|
+
session.worktreeInfo = {
|
|
253
|
+
repoRoot,
|
|
254
|
+
worktreePath: target.path,
|
|
255
|
+
branch: target.branch,
|
|
256
|
+
};
|
|
257
|
+
}
|
|
258
|
+
/**
|
|
259
|
+
* List all worktrees for the current repository.
|
|
260
|
+
*/
|
|
261
|
+
export async function listWorktreesCommand(session) {
|
|
262
|
+
// Check if we're in a git repo
|
|
263
|
+
const isRepo = await isGitRepository(session.workingDir);
|
|
264
|
+
if (!isRepo) {
|
|
265
|
+
await session.platform.createPost(`❌ Current directory is not a git repository`, session.threadId);
|
|
266
|
+
return;
|
|
267
|
+
}
|
|
268
|
+
// Get repo root (either from worktree info or current dir)
|
|
269
|
+
const repoRoot = session.worktreeInfo?.repoRoot || await getRepositoryRoot(session.workingDir);
|
|
270
|
+
const worktrees = await listGitWorktrees(repoRoot);
|
|
271
|
+
if (worktrees.length === 0) {
|
|
272
|
+
await session.platform.createPost(`📋 No worktrees found for this repository`, session.threadId);
|
|
273
|
+
return;
|
|
274
|
+
}
|
|
275
|
+
const shortRepoRoot = repoRoot.replace(process.env.HOME || '', '~');
|
|
276
|
+
let message = `📋 **Worktrees for** \`${shortRepoRoot}\`:\n\n`;
|
|
277
|
+
for (const wt of worktrees) {
|
|
278
|
+
const shortPath = wt.path.replace(process.env.HOME || '', '~');
|
|
279
|
+
const isCurrent = session.workingDir === wt.path;
|
|
280
|
+
const marker = isCurrent ? ' ← current' : '';
|
|
281
|
+
const label = wt.isMain ? '(main repository)' : '';
|
|
282
|
+
message += `• \`${wt.branch}\` → \`${shortPath}\` ${label}${marker}\n`;
|
|
283
|
+
}
|
|
284
|
+
await session.platform.createPost(message, session.threadId);
|
|
285
|
+
}
|
|
286
|
+
/**
|
|
287
|
+
* Remove a worktree.
|
|
288
|
+
*/
|
|
289
|
+
export async function removeWorktreeCommand(session, branchOrPath, username) {
|
|
290
|
+
// Only session owner or admins can manage worktrees
|
|
291
|
+
if (session.startedBy !== username && !session.platform.isUserAllowed(username)) {
|
|
292
|
+
await session.platform.createPost(`⚠️ Only @${session.startedBy} or allowed users can manage worktrees`, session.threadId);
|
|
293
|
+
return;
|
|
294
|
+
}
|
|
295
|
+
// Get current repo root
|
|
296
|
+
const repoRoot = session.worktreeInfo?.repoRoot || await getRepositoryRoot(session.workingDir);
|
|
297
|
+
// Find the worktree
|
|
298
|
+
const worktrees = await listGitWorktrees(repoRoot);
|
|
299
|
+
const target = worktrees.find(wt => wt.branch === branchOrPath ||
|
|
300
|
+
wt.path === branchOrPath ||
|
|
301
|
+
wt.path.endsWith(branchOrPath));
|
|
302
|
+
if (!target) {
|
|
303
|
+
await session.platform.createPost(`❌ Worktree not found: \`${branchOrPath}\`. Use \`!worktree list\` to see available worktrees.`, session.threadId);
|
|
304
|
+
return;
|
|
305
|
+
}
|
|
306
|
+
// Can't remove the main repository
|
|
307
|
+
if (target.isMain) {
|
|
308
|
+
await session.platform.createPost(`❌ Cannot remove the main repository. Use \`!worktree remove\` only for worktrees.`, session.threadId);
|
|
309
|
+
return;
|
|
310
|
+
}
|
|
311
|
+
// Can't remove the current working directory
|
|
312
|
+
if (session.workingDir === target.path) {
|
|
313
|
+
await session.platform.createPost(`❌ Cannot remove the current working directory. Switch to another worktree first.`, session.threadId);
|
|
314
|
+
return;
|
|
315
|
+
}
|
|
316
|
+
try {
|
|
317
|
+
await removeGitWorktree(repoRoot, target.path);
|
|
318
|
+
const shortPath = target.path.replace(process.env.HOME || '', '~');
|
|
319
|
+
await session.platform.createPost(`✅ Removed worktree \`${target.branch}\` at \`${shortPath}\``, session.threadId);
|
|
320
|
+
console.log(` 🗑️ Removed worktree ${target.branch} at ${shortPath}`);
|
|
321
|
+
}
|
|
322
|
+
catch (err) {
|
|
323
|
+
console.error(` ❌ Failed to remove worktree:`, err);
|
|
324
|
+
await session.platform.createPost(`❌ Failed to remove worktree: ${err instanceof Error ? err.message : String(err)}`, session.threadId);
|
|
325
|
+
}
|
|
326
|
+
}
|
|
327
|
+
/**
|
|
328
|
+
* Disable worktree prompts for a session.
|
|
329
|
+
*/
|
|
330
|
+
export async function disableWorktreePrompt(session, username, persistSession) {
|
|
331
|
+
// Only session owner or admins can manage worktrees
|
|
332
|
+
if (session.startedBy !== username && !session.platform.isUserAllowed(username)) {
|
|
333
|
+
await session.platform.createPost(`⚠️ Only @${session.startedBy} or allowed users can manage worktrees`, session.threadId);
|
|
334
|
+
return;
|
|
335
|
+
}
|
|
336
|
+
session.worktreePromptDisabled = true;
|
|
337
|
+
persistSession(session);
|
|
338
|
+
await session.platform.createPost(`✅ Worktree prompts disabled for this session`, session.threadId);
|
|
339
|
+
}
|
package/dist/update-notifier.js
CHANGED
|
@@ -2,6 +2,7 @@ import updateNotifier from 'update-notifier';
|
|
|
2
2
|
import { readFileSync } from 'fs';
|
|
3
3
|
import { resolve } from 'path';
|
|
4
4
|
import { fileURLToPath } from 'url';
|
|
5
|
+
import semver from 'semver';
|
|
5
6
|
const __dirname = fileURLToPath(new URL('.', import.meta.url));
|
|
6
7
|
let cachedUpdateInfo;
|
|
7
8
|
export function checkForUpdates() {
|
|
@@ -26,6 +27,15 @@ Run: npm install -g claude-threads`,
|
|
|
26
27
|
}
|
|
27
28
|
}
|
|
28
29
|
// Returns update info if available, for posting to Mattermost
|
|
30
|
+
// Only returns if latest > current (handles stale cache edge case)
|
|
29
31
|
export function getUpdateInfo() {
|
|
32
|
+
if (!cachedUpdateInfo)
|
|
33
|
+
return undefined;
|
|
34
|
+
// Sanity check: only show update if latest is actually newer
|
|
35
|
+
const current = cachedUpdateInfo.current;
|
|
36
|
+
const latest = cachedUpdateInfo.latest;
|
|
37
|
+
if (current && latest && semver.gte(current, latest)) {
|
|
38
|
+
return undefined; // Current is same or newer, no update needed
|
|
39
|
+
}
|
|
30
40
|
return cachedUpdateInfo;
|
|
31
41
|
}
|
|
@@ -1,8 +1,8 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* Emoji constants and helpers for
|
|
2
|
+
* Emoji constants and helpers for chat platform reactions
|
|
3
3
|
*
|
|
4
|
-
*
|
|
5
|
-
*
|
|
4
|
+
* Platform-agnostic emoji utilities used across session management,
|
|
5
|
+
* permission handling, and user interactions.
|
|
6
6
|
*/
|
|
7
7
|
/** Emoji names that indicate approval */
|
|
8
8
|
export declare const APPROVAL_EMOJIS: readonly ["+1", "thumbsup"];
|
|
@@ -1,8 +1,8 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* Emoji constants and helpers for
|
|
2
|
+
* Emoji constants and helpers for chat platform reactions
|
|
3
3
|
*
|
|
4
|
-
*
|
|
5
|
-
*
|
|
4
|
+
* Platform-agnostic emoji utilities used across session management,
|
|
5
|
+
* permission handling, and user interactions.
|
|
6
6
|
*/
|
|
7
7
|
/** Emoji names that indicate approval */
|
|
8
8
|
export const APPROVAL_EMOJIS = ['+1', 'thumbsup'];
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|
|
@@ -1,10 +1,14 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* Tool formatting utilities for displaying Claude tool calls in
|
|
2
|
+
* Tool formatting utilities for displaying Claude tool calls in chat platforms
|
|
3
3
|
*
|
|
4
4
|
* This module provides shared formatting logic used by both:
|
|
5
|
-
* - src/
|
|
5
|
+
* - src/session/events.ts (main bot)
|
|
6
6
|
* - src/mcp/permission-server.ts (MCP permission handler)
|
|
7
|
+
*
|
|
8
|
+
* Uses PlatformFormatter abstraction to support different markdown dialects
|
|
9
|
+
* (e.g., Mattermost standard markdown vs Slack mrkdwn).
|
|
7
10
|
*/
|
|
11
|
+
import type { PlatformFormatter } from '../platform/formatter.js';
|
|
8
12
|
export interface ToolInput {
|
|
9
13
|
[key: string]: unknown;
|
|
10
14
|
}
|
|
@@ -30,27 +34,20 @@ export declare function parseMcpToolName(toolName: string): {
|
|
|
30
34
|
tool: string;
|
|
31
35
|
} | null;
|
|
32
36
|
/**
|
|
33
|
-
* Format a tool use for display in
|
|
37
|
+
* Format a tool use for display in chat platforms
|
|
34
38
|
*
|
|
35
39
|
* @param toolName - The name of the tool being called
|
|
36
40
|
* @param input - The tool input parameters
|
|
37
41
|
* @param options - Formatting options
|
|
38
42
|
* @returns Formatted string or null if the tool should not be displayed
|
|
39
43
|
*/
|
|
40
|
-
export declare function formatToolUse(toolName: string, input: ToolInput, options?: FormatOptions): string | null;
|
|
44
|
+
export declare function formatToolUse(toolName: string, input: ToolInput, formatter: PlatformFormatter, options?: FormatOptions): string | null;
|
|
41
45
|
/**
|
|
42
46
|
* Format tool info for permission prompts (simpler format)
|
|
43
47
|
*
|
|
44
48
|
* @param toolName - The name of the tool
|
|
45
49
|
* @param input - The tool input parameters
|
|
50
|
+
* @param formatter - Platform-specific markdown formatter
|
|
46
51
|
* @returns Formatted string for permission prompts
|
|
47
52
|
*/
|
|
48
|
-
export declare function formatToolForPermission(toolName: string, input: ToolInput): string;
|
|
49
|
-
/**
|
|
50
|
-
* Format Claude in Chrome tool calls
|
|
51
|
-
*
|
|
52
|
-
* @param tool - The Chrome tool name (after mcp__claude-in-chrome__)
|
|
53
|
-
* @param input - The tool input parameters
|
|
54
|
-
* @returns Formatted string for display
|
|
55
|
-
*/
|
|
56
|
-
export declare function formatChromeToolUse(tool: string, input: ToolInput): string;
|
|
53
|
+
export declare function formatToolForPermission(toolName: string, input: ToolInput, formatter: PlatformFormatter): string;
|
|
@@ -1,9 +1,12 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* Tool formatting utilities for displaying Claude tool calls in
|
|
2
|
+
* Tool formatting utilities for displaying Claude tool calls in chat platforms
|
|
3
3
|
*
|
|
4
4
|
* This module provides shared formatting logic used by both:
|
|
5
|
-
* - src/
|
|
5
|
+
* - src/session/events.ts (main bot)
|
|
6
6
|
* - src/mcp/permission-server.ts (MCP permission handler)
|
|
7
|
+
*
|
|
8
|
+
* Uses PlatformFormatter abstraction to support different markdown dialects
|
|
9
|
+
* (e.g., Mattermost standard markdown vs Slack mrkdwn).
|
|
7
10
|
*/
|
|
8
11
|
import * as Diff from 'diff';
|
|
9
12
|
const DEFAULT_OPTIONS = {
|
|
@@ -39,19 +42,19 @@ export function parseMcpToolName(toolName) {
|
|
|
39
42
|
};
|
|
40
43
|
}
|
|
41
44
|
/**
|
|
42
|
-
* Format a tool use for display in
|
|
45
|
+
* Format a tool use for display in chat platforms
|
|
43
46
|
*
|
|
44
47
|
* @param toolName - The name of the tool being called
|
|
45
48
|
* @param input - The tool input parameters
|
|
46
49
|
* @param options - Formatting options
|
|
47
50
|
* @returns Formatted string or null if the tool should not be displayed
|
|
48
51
|
*/
|
|
49
|
-
export function formatToolUse(toolName, input, options = {}) {
|
|
52
|
+
export function formatToolUse(toolName, input, formatter, options = {}) {
|
|
50
53
|
const opts = { ...DEFAULT_OPTIONS, ...options };
|
|
51
54
|
const short = (p) => shortenPath(p);
|
|
52
55
|
switch (toolName) {
|
|
53
56
|
case 'Read':
|
|
54
|
-
return `📄
|
|
57
|
+
return `📄 ${formatter.formatBold('Read')} ${formatter.formatCode(short(input.file_path))}`;
|
|
55
58
|
case 'Edit': {
|
|
56
59
|
const filePath = short(input.file_path);
|
|
57
60
|
const oldStr = input.old_string || '';
|
|
@@ -84,15 +87,14 @@ export function formatToolUse(toolName, input, options = {}) {
|
|
|
84
87
|
break;
|
|
85
88
|
}
|
|
86
89
|
const totalLines = changes.reduce((sum, c) => sum + c.value.split('\n').length - 1, 0);
|
|
87
|
-
let diff = `✏️
|
|
88
|
-
diff += diffLines.join('\n');
|
|
90
|
+
let diff = `✏️ ${formatter.formatBold('Edit')} ${formatter.formatCode(filePath)}\n${formatter.formatCodeBlock(diffLines.join('\n'), 'diff')}`;
|
|
89
91
|
if (totalLines > maxLines) {
|
|
90
|
-
diff
|
|
92
|
+
diff = `✏️ ${formatter.formatBold('Edit')} ${formatter.formatCode(filePath)}\n`;
|
|
93
|
+
diff += formatter.formatCodeBlock(diffLines.join('\n') + `\n... (+${totalLines - maxLines} more lines)`, 'diff');
|
|
91
94
|
}
|
|
92
|
-
diff += '\n```';
|
|
93
95
|
return diff;
|
|
94
96
|
}
|
|
95
|
-
return `✏️
|
|
97
|
+
return `✏️ ${formatter.formatBold('Edit')} ${formatter.formatCode(filePath)}`;
|
|
96
98
|
}
|
|
97
99
|
case 'Write': {
|
|
98
100
|
const filePath = short(input.file_path);
|
|
@@ -103,29 +105,30 @@ export function formatToolUse(toolName, input, options = {}) {
|
|
|
103
105
|
if (opts.detailed && content && lineCount > 0) {
|
|
104
106
|
const maxLines = 6;
|
|
105
107
|
const previewLines = lines.slice(0, maxLines);
|
|
106
|
-
let preview = `📝
|
|
107
|
-
preview += previewLines.join('\n');
|
|
108
|
+
let preview = `📝 ${formatter.formatBold('Write')} ${formatter.formatCode(filePath)} ${formatter.formatItalic(`(${lineCount} lines)`)}\n`;
|
|
108
109
|
if (lineCount > maxLines) {
|
|
109
|
-
preview += `\n... (${lineCount - maxLines} more lines)
|
|
110
|
+
preview += formatter.formatCodeBlock(previewLines.join('\n') + `\n... (${lineCount - maxLines} more lines)`);
|
|
111
|
+
}
|
|
112
|
+
else {
|
|
113
|
+
preview += formatter.formatCodeBlock(previewLines.join('\n'));
|
|
110
114
|
}
|
|
111
|
-
preview += '\n```';
|
|
112
115
|
return preview;
|
|
113
116
|
}
|
|
114
|
-
return `📝
|
|
117
|
+
return `📝 ${formatter.formatBold('Write')} ${formatter.formatCode(filePath)}`;
|
|
115
118
|
}
|
|
116
119
|
case 'Bash': {
|
|
117
120
|
const cmd = (input.command || '').substring(0, opts.maxCommandLength);
|
|
118
121
|
const truncated = cmd.length >= opts.maxCommandLength;
|
|
119
|
-
return `💻
|
|
122
|
+
return `💻 ${formatter.formatBold('Bash')} ${formatter.formatCode(cmd + (truncated ? '...' : ''))}`;
|
|
120
123
|
}
|
|
121
124
|
case 'Glob':
|
|
122
|
-
return `🔍
|
|
125
|
+
return `🔍 ${formatter.formatBold('Glob')} ${formatter.formatCode(input.pattern)}`;
|
|
123
126
|
case 'Grep':
|
|
124
|
-
return `🔎
|
|
127
|
+
return `🔎 ${formatter.formatBold('Grep')} ${formatter.formatCode(input.pattern)}`;
|
|
125
128
|
case 'Task':
|
|
126
129
|
return null; // Handled specially with subagent display
|
|
127
130
|
case 'EnterPlanMode':
|
|
128
|
-
return `📋
|
|
131
|
+
return `📋 ${formatter.formatBold('Planning...')}`;
|
|
129
132
|
case 'ExitPlanMode':
|
|
130
133
|
return null; // Handled specially with approval buttons
|
|
131
134
|
case 'AskUserQuestion':
|
|
@@ -134,21 +137,21 @@ export function formatToolUse(toolName, input, options = {}) {
|
|
|
134
137
|
return null; // Handled specially with task list display
|
|
135
138
|
case 'WebFetch': {
|
|
136
139
|
const url = (input.url || '').substring(0, 40);
|
|
137
|
-
return `🌐
|
|
140
|
+
return `🌐 ${formatter.formatBold('Fetching')} ${formatter.formatCode(url)}`;
|
|
138
141
|
}
|
|
139
142
|
case 'WebSearch':
|
|
140
|
-
return `🔍
|
|
143
|
+
return `🔍 ${formatter.formatBold('Searching')} ${formatter.formatCode(input.query)}`;
|
|
141
144
|
default: {
|
|
142
145
|
// Handle MCP tools: mcp__server__tool
|
|
143
146
|
const mcpParts = parseMcpToolName(toolName);
|
|
144
147
|
if (mcpParts) {
|
|
145
148
|
// Special formatting for Claude in Chrome tools
|
|
146
149
|
if (mcpParts.server === 'claude-in-chrome') {
|
|
147
|
-
return formatChromeToolUse(mcpParts.tool, input);
|
|
150
|
+
return formatChromeToolUse(mcpParts.tool, input, formatter);
|
|
148
151
|
}
|
|
149
|
-
return `🔌
|
|
152
|
+
return `🔌 ${formatter.formatBold(mcpParts.tool)} ${formatter.formatItalic(`(${mcpParts.server})`)}`;
|
|
150
153
|
}
|
|
151
|
-
return `●
|
|
154
|
+
return `● ${formatter.formatBold(toolName)}`;
|
|
152
155
|
}
|
|
153
156
|
}
|
|
154
157
|
}
|
|
@@ -157,27 +160,28 @@ export function formatToolUse(toolName, input, options = {}) {
|
|
|
157
160
|
*
|
|
158
161
|
* @param toolName - The name of the tool
|
|
159
162
|
* @param input - The tool input parameters
|
|
163
|
+
* @param formatter - Platform-specific markdown formatter
|
|
160
164
|
* @returns Formatted string for permission prompts
|
|
161
165
|
*/
|
|
162
|
-
export function formatToolForPermission(toolName, input) {
|
|
166
|
+
export function formatToolForPermission(toolName, input, formatter) {
|
|
163
167
|
const short = (p) => shortenPath(p);
|
|
164
168
|
switch (toolName) {
|
|
165
169
|
case 'Read':
|
|
166
|
-
return `📄
|
|
170
|
+
return `📄 ${formatter.formatBold('Read')} ${formatter.formatCode(short(input.file_path))}`;
|
|
167
171
|
case 'Write':
|
|
168
|
-
return `📝
|
|
172
|
+
return `📝 ${formatter.formatBold('Write')} ${formatter.formatCode(short(input.file_path))}`;
|
|
169
173
|
case 'Edit':
|
|
170
|
-
return `✏️
|
|
174
|
+
return `✏️ ${formatter.formatBold('Edit')} ${formatter.formatCode(short(input.file_path))}`;
|
|
171
175
|
case 'Bash': {
|
|
172
176
|
const cmd = (input.command || '').substring(0, 100);
|
|
173
|
-
return `💻
|
|
177
|
+
return `💻 ${formatter.formatBold('Bash')} ${formatter.formatCode(cmd + (cmd.length >= 100 ? '...' : ''))}`;
|
|
174
178
|
}
|
|
175
179
|
default: {
|
|
176
180
|
const mcpParts = parseMcpToolName(toolName);
|
|
177
181
|
if (mcpParts) {
|
|
178
|
-
return `🔌
|
|
182
|
+
return `🔌 ${formatter.formatBold(mcpParts.tool)} ${formatter.formatItalic(`(${mcpParts.server})`)}`;
|
|
179
183
|
}
|
|
180
|
-
return `●
|
|
184
|
+
return `● ${formatter.formatBold(toolName)}`;
|
|
181
185
|
}
|
|
182
186
|
}
|
|
183
187
|
}
|
|
@@ -186,9 +190,10 @@ export function formatToolForPermission(toolName, input) {
|
|
|
186
190
|
*
|
|
187
191
|
* @param tool - The Chrome tool name (after mcp__claude-in-chrome__)
|
|
188
192
|
* @param input - The tool input parameters
|
|
193
|
+
* @param formatter - Platform-specific markdown formatter
|
|
189
194
|
* @returns Formatted string for display
|
|
190
195
|
*/
|
|
191
|
-
|
|
196
|
+
function formatChromeToolUse(tool, input, formatter) {
|
|
192
197
|
const action = input.action || '';
|
|
193
198
|
const coord = input.coordinate;
|
|
194
199
|
const url = input.url || '';
|
|
@@ -221,27 +226,27 @@ export function formatChromeToolUse(tool, input) {
|
|
|
221
226
|
default:
|
|
222
227
|
detail = action || 'action';
|
|
223
228
|
}
|
|
224
|
-
return `🌐
|
|
229
|
+
return `🌐 ${formatter.formatBold('Chrome')}[computer] ${formatter.formatCode(detail)}`;
|
|
225
230
|
}
|
|
226
231
|
case 'navigate':
|
|
227
|
-
return `🌐
|
|
232
|
+
return `🌐 ${formatter.formatBold('Chrome')}[navigate] ${formatter.formatCode(url.substring(0, 50) + (url.length > 50 ? '...' : ''))}`;
|
|
228
233
|
case 'tabs_context_mcp':
|
|
229
|
-
return `🌐
|
|
234
|
+
return `🌐 ${formatter.formatBold('Chrome')}[tabs] reading context`;
|
|
230
235
|
case 'tabs_create_mcp':
|
|
231
|
-
return `🌐
|
|
236
|
+
return `🌐 ${formatter.formatBold('Chrome')}[tabs] creating new tab`;
|
|
232
237
|
case 'read_page':
|
|
233
|
-
return `🌐
|
|
238
|
+
return `🌐 ${formatter.formatBold('Chrome')}[read_page] ${input.filter === 'interactive' ? 'interactive elements' : 'accessibility tree'}`;
|
|
234
239
|
case 'find':
|
|
235
|
-
return `🌐
|
|
240
|
+
return `🌐 ${formatter.formatBold('Chrome')}[find] ${formatter.formatCode(input.query || '')}`;
|
|
236
241
|
case 'form_input':
|
|
237
|
-
return `🌐
|
|
242
|
+
return `🌐 ${formatter.formatBold('Chrome')}[form_input] setting value`;
|
|
238
243
|
case 'get_page_text':
|
|
239
|
-
return `🌐
|
|
244
|
+
return `🌐 ${formatter.formatBold('Chrome')}[get_page_text] extracting content`;
|
|
240
245
|
case 'javascript_tool':
|
|
241
|
-
return `🌐
|
|
246
|
+
return `🌐 ${formatter.formatBold('Chrome')}[javascript] executing script`;
|
|
242
247
|
case 'gif_creator':
|
|
243
|
-
return `🌐
|
|
248
|
+
return `🌐 ${formatter.formatBold('Chrome')}[gif] ${action}`;
|
|
244
249
|
default:
|
|
245
|
-
return `🌐
|
|
250
|
+
return `🌐 ${formatter.formatBold('Chrome')}[${tool}]`;
|
|
246
251
|
}
|
|
247
252
|
}
|