claude-threads 0.49.0 → 0.51.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 CHANGED
@@ -7,6 +7,21 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
7
7
 
8
8
  ## [Unreleased]
9
9
 
10
+ ## [0.51.0] - 2026-01-09
11
+
12
+ ### Added
13
+ - **Image upload for bug reports** - Bug reports can now include screenshots uploaded to Catbox.moe. Use `!bug <description>` with an attached image or paste a screenshot (#153)
14
+
15
+ ### Fixed
16
+ - **Duplicate task lists** - Fixed issue where multiple task lists would appear in threads due to race conditions (#152, #151)
17
+ - **Code block rendering** - Fixed issues with code blocks not rendering correctly, including improved handling of language tags and empty blocks (#154)
18
+ - **Website logo rendering** - Improved SVG logo rendering on the project website (#155)
19
+
20
+ ## [0.50.0] - 2026-01-09
21
+
22
+ ### Added
23
+ - **Bug reporting feature** - Users can now report bugs with `!bug <description>` command. The bot collects recent conversation context, session state, and system info into a markdown report posted as a file attachment (#150)
24
+
10
25
  ## [0.49.0] - 2026-01-09
11
26
 
12
27
  ### Added
package/README.md CHANGED
@@ -6,6 +6,10 @@
6
6
  ✴ ▀█▄ █ ✴
7
7
  ```
8
8
 
9
+ <p align="center">
10
+ <a href="https://claude-threads.run"><strong>claude-threads.run</strong></a>
11
+ </p>
12
+
9
13
  [![npm version](https://img.shields.io/npm/v/claude-threads.svg)](https://www.npmjs.com/package/claude-threads)
10
14
  [![npm downloads](https://img.shields.io/npm/dm/claude-threads.svg)](https://www.npmjs.com/package/claude-threads)
11
15
  [![CI](https://github.com/anneschuth/claude-threads/actions/workflows/ci.yml/badge.svg)](https://github.com/anneschuth/claude-threads/actions/workflows/ci.yml)
package/dist/index.js CHANGED
@@ -42763,9 +42763,11 @@ ${code}
42763
42763
  `);
42764
42764
  }
42765
42765
  formatMarkdown(content) {
42766
- return content.replace(/\n{3,}/g, `
42766
+ let processed = content.replace(/(?<=\n)```(?=\S)(?![a-zA-Z]*\n)/g, "```\n");
42767
+ processed = processed.replace(/\n{3,}/g, `
42767
42768
 
42768
42769
  `);
42770
+ return processed;
42769
42771
  }
42770
42772
  }
42771
42773
 
@@ -43407,6 +43409,7 @@ function convertMarkdownToSlack(content) {
43407
43409
  for (let i = 0;i < codeBlocks.length; i++) {
43408
43410
  preserved = preserved.replace(`${CODE_BLOCK_PLACEHOLDER}${i}\x00`, codeBlocks[i]);
43409
43411
  }
43412
+ preserved = preserved.replace(/(?<=\n)```(?=\S)(?![a-zA-Z]*\n)/g, "```\n");
43410
43413
  return preserved;
43411
43414
  }
43412
43415
  function convertMarkdownTablesToSlack(content) {
@@ -44639,7 +44642,7 @@ class SlackPermissionApi {
44639
44642
  import { EventEmitter as EventEmitter4 } from "events";
44640
44643
  import { existsSync as existsSync9 } from "fs";
44641
44644
  import { readdir, rm } from "fs/promises";
44642
- import { join as join4 } from "path";
44645
+ import { join as join5 } from "path";
44643
44646
 
44644
44647
  // src/persistence/session-store.ts
44645
44648
  import { existsSync as existsSync3, mkdirSync as mkdirSync2, readFileSync as readFileSync3, writeFileSync as writeFileSync2, renameSync } from "fs";
@@ -44894,6 +44897,7 @@ var CANCEL_EMOJIS = ["x", "octagonal_sign", "stop_sign", "stop"];
44894
44897
  var ESCAPE_EMOJIS = ["double_vertical_bar", "pause_button", "pause"];
44895
44898
  var RESUME_EMOJIS = ["arrows_counterclockwise", "arrow_forward", "repeat"];
44896
44899
  var TASK_TOGGLE_EMOJIS = ["arrow_down_small", "small_red_triangle_down"];
44900
+ var BUG_REPORT_EMOJI = "bug";
44897
44901
  function isApprovalEmoji(emoji) {
44898
44902
  return APPROVAL_EMOJIS.includes(emoji);
44899
44903
  }
@@ -44915,6 +44919,9 @@ function isResumeEmoji(emoji) {
44915
44919
  function isTaskToggleEmoji(emoji) {
44916
44920
  return TASK_TOGGLE_EMOJIS.includes(emoji);
44917
44921
  }
44922
+ function isBugReportEmoji(emoji) {
44923
+ return emoji === BUG_REPORT_EMOJI || emoji === "\uD83D\uDC1B";
44924
+ }
44918
44925
  var UNICODE_NUMBER_EMOJIS = {
44919
44926
  "1\uFE0F\u20E3": 0,
44920
44927
  "2\uFE0F\u20E3": 1,
@@ -45284,8 +45291,19 @@ async function postSuccess(session, message) {
45284
45291
  async function postWarning(session, message) {
45285
45292
  return createPostAndTrack(session, `\u26A0\uFE0F ${message}`);
45286
45293
  }
45287
- async function postError(session, message) {
45288
- return createPostAndTrack(session, `\u274C ${message}`);
45294
+ async function postError(session, message, addBugReaction = true) {
45295
+ const post = await createPostAndTrack(session, `\u274C ${message}`);
45296
+ if (addBugReaction) {
45297
+ try {
45298
+ await session.platform.addReaction(post.id, BUG_REPORT_EMOJI);
45299
+ session.lastError = {
45300
+ postId: post.id,
45301
+ message,
45302
+ timestamp: new Date
45303
+ };
45304
+ } catch {}
45305
+ }
45306
+ return post;
45289
45307
  }
45290
45308
  async function postSecure(session, message) {
45291
45309
  return createPostAndTrack(session, `\uD83D\uDD10 ${message}`);
@@ -46388,14 +46406,15 @@ function formatToolUse(toolName, input, formatter, options = {}) {
46388
46406
  for (const line of lines) {
46389
46407
  if (lineCount >= maxLines)
46390
46408
  break;
46409
+ const escapedLine = line.replace(/```/g, "` ``");
46391
46410
  if (change.added) {
46392
- diffLines2.push(`+ ${line}`);
46411
+ diffLines2.push(`+ ${escapedLine}`);
46393
46412
  lineCount++;
46394
46413
  } else if (change.removed) {
46395
- diffLines2.push(`- ${line}`);
46414
+ diffLines2.push(`- ${escapedLine}`);
46396
46415
  lineCount++;
46397
46416
  } else {
46398
- diffLines2.push(` ${line}`);
46417
+ diffLines2.push(` ${escapedLine}`);
46399
46418
  lineCount++;
46400
46419
  }
46401
46420
  }
@@ -46426,7 +46445,7 @@ ${formatter.formatCodeBlock(diffLines2.join(`
46426
46445
  const lineCount = lines.length;
46427
46446
  if (detailed && content && lineCount > 0) {
46428
46447
  const maxLines = 6;
46429
- const previewLines = lines.slice(0, maxLines);
46448
+ const previewLines = lines.slice(0, maxLines).map((line) => line.replace(/```/g, "` ``"));
46430
46449
  let preview = `\uD83D\uDCDD ${formatter.formatBold("Write")} ${formatter.formatCode(filePath)} ${formatter.formatItalic(`(${lineCount} lines)`)}
46431
46450
  `;
46432
46451
  if (lineCount > maxLines) {
@@ -50488,6 +50507,353 @@ function getLogo(version) {
50488
50507
  \`\`\``;
50489
50508
  }
50490
50509
 
50510
+ // src/session/bug-report.ts
50511
+ import { execSync as execSync2 } from "child_process";
50512
+ import { writeFileSync as writeFileSync4, unlinkSync as unlinkSync2 } from "fs";
50513
+ import { tmpdir as tmpdir2 } from "os";
50514
+ import { join as join4 } from "path";
50515
+
50516
+ // src/claude/version-check.ts
50517
+ var import_semver3 = __toESM(require_semver2(), 1);
50518
+ import { execSync } from "child_process";
50519
+ var CLAUDE_CLI_VERSION_RANGE = ">=2.0.74 <2.2.0";
50520
+ function getClaudeCliVersion() {
50521
+ const claudePath = process.env.CLAUDE_PATH || "claude";
50522
+ try {
50523
+ const output = execSync(`${claudePath} --version`, {
50524
+ encoding: "utf8",
50525
+ timeout: 5000,
50526
+ stdio: ["pipe", "pipe", "pipe"]
50527
+ }).trim();
50528
+ const match = output.match(/^([\d.]+)/);
50529
+ return match ? match[1] : null;
50530
+ } catch {
50531
+ return null;
50532
+ }
50533
+ }
50534
+ function isVersionCompatible(version) {
50535
+ const semverVersion = import_semver3.coerce(version);
50536
+ if (!semverVersion)
50537
+ return false;
50538
+ return import_semver3.satisfies(semverVersion, CLAUDE_CLI_VERSION_RANGE);
50539
+ }
50540
+ function validateClaudeCli() {
50541
+ const version = getClaudeCliVersion();
50542
+ if (!version) {
50543
+ return {
50544
+ installed: false,
50545
+ version: null,
50546
+ compatible: false,
50547
+ message: "Claude CLI not found. Install it with: bun install -g @anthropic-ai/claude-code"
50548
+ };
50549
+ }
50550
+ const compatible = isVersionCompatible(version);
50551
+ if (!compatible) {
50552
+ return {
50553
+ installed: true,
50554
+ version,
50555
+ compatible: false,
50556
+ message: `Claude CLI version ${version} is not compatible. Required: ${CLAUDE_CLI_VERSION_RANGE}
50557
+ ` + `Install a compatible version: bun install -g @anthropic-ai/claude-code@2.0.76`
50558
+ };
50559
+ }
50560
+ return {
50561
+ installed: true,
50562
+ version,
50563
+ compatible: true,
50564
+ message: `Claude CLI ${version} \u2713`
50565
+ };
50566
+ }
50567
+
50568
+ // src/session/bug-report.ts
50569
+ var CATBOX_API_URL = "https://catbox.moe/user/api.php";
50570
+ async function uploadImageToCatbox(imageBuffer, filename) {
50571
+ const arrayBuffer = imageBuffer.buffer.slice(imageBuffer.byteOffset, imageBuffer.byteOffset + imageBuffer.byteLength);
50572
+ const formData = new FormData;
50573
+ formData.append("reqtype", "fileupload");
50574
+ formData.append("fileToUpload", new Blob([arrayBuffer]), filename);
50575
+ const response = await fetch(CATBOX_API_URL, {
50576
+ method: "POST",
50577
+ body: formData
50578
+ });
50579
+ if (!response.ok) {
50580
+ throw new Error(`Catbox upload failed: ${response.status} ${response.statusText}`);
50581
+ }
50582
+ const responseText = await response.text();
50583
+ if (responseText.startsWith("https://")) {
50584
+ return responseText.trim();
50585
+ }
50586
+ throw new Error(`Catbox upload failed: ${responseText}`);
50587
+ }
50588
+ async function uploadImages(files, downloadFile) {
50589
+ const results = [];
50590
+ for (const file of files) {
50591
+ if (!file.mimeType.startsWith("image/")) {
50592
+ results.push({
50593
+ success: false,
50594
+ error: "Not an image file",
50595
+ originalFile: file
50596
+ });
50597
+ continue;
50598
+ }
50599
+ try {
50600
+ const buffer = await downloadFile(file.id);
50601
+ const url = await uploadImageToCatbox(buffer, file.name);
50602
+ results.push({
50603
+ success: true,
50604
+ url,
50605
+ originalFile: file
50606
+ });
50607
+ } catch (err) {
50608
+ results.push({
50609
+ success: false,
50610
+ error: err instanceof Error ? err.message : String(err),
50611
+ originalFile: file
50612
+ });
50613
+ }
50614
+ }
50615
+ return results;
50616
+ }
50617
+ var MAX_RECENT_EVENTS = 10;
50618
+ var GITHUB_REPO = "anneschuth/claude-threads";
50619
+ function trackEvent(session, type, summary) {
50620
+ if (!session.recentEvents) {
50621
+ session.recentEvents = [];
50622
+ }
50623
+ session.recentEvents.push({
50624
+ type,
50625
+ timestamp: Date.now(),
50626
+ summary: summary.substring(0, 100)
50627
+ });
50628
+ if (session.recentEvents.length > MAX_RECENT_EVENTS) {
50629
+ session.recentEvents.shift();
50630
+ }
50631
+ }
50632
+ function getRecentEvents(session) {
50633
+ return session.recentEvents || [];
50634
+ }
50635
+ function sanitizePath(path10) {
50636
+ let sanitized = path10.replace(/^\/Users\/[^/]+/, "~").replace(/^\/home\/[^/]+/, "~").replace(/^C:\\Users\\[^\\]+/i, "~");
50637
+ sanitized = sanitized.replace(/\/(tokens?|secrets?|credentials?|\.env)/gi, "/[REDACTED]").replace(/\\(tokens?|secrets?|credentials?|\.env)/gi, "\\[REDACTED]");
50638
+ return sanitized;
50639
+ }
50640
+ function sanitizeText(text) {
50641
+ return text.replace(/xoxb-[\w-]+/gi, "[SLACK_TOKEN]").replace(/xoxp-[\w-]+/gi, "[SLACK_TOKEN]").replace(/xapp-[\w-]+/gi, "[SLACK_TOKEN]").replace(/xoxa-[\w-]+/gi, "[SLACK_TOKEN]").replace(/ghp_[\w]+/gi, "[GITHUB_TOKEN]").replace(/gho_[\w]+/gi, "[GITHUB_TOKEN]").replace(/github_pat_[\w]+/gi, "[GITHUB_TOKEN]").replace(/sk_live_[\w]+/gi, "[STRIPE_KEY]").replace(/sk_test_[\w]+/gi, "[STRIPE_KEY]").replace(/pk_live_[\w]+/gi, "[STRIPE_KEY]").replace(/pk_test_[\w]+/gi, "[STRIPE_KEY]").replace(/AKIA[\w]{16}/g, "[AWS_KEY]").replace(/['"]?[a-zA-Z_]*(?:api[_-]?key|secret|token|password|credential)['":]?\s*['"]?[\w-]{20,}['"]?/gi, "[REDACTED_KEY]").replace(/\/Users\/[\w.-]+/g, "~").replace(/\/home\/[\w.-]+/g, "~").replace(/C:\\Users\\[\w.-]+/gi, "~");
50642
+ }
50643
+ async function getCurrentBranch2(workingDir) {
50644
+ try {
50645
+ const output = execSync2("git rev-parse --abbrev-ref HEAD", {
50646
+ cwd: workingDir,
50647
+ encoding: "utf-8",
50648
+ timeout: 5000,
50649
+ stdio: ["pipe", "pipe", "pipe"]
50650
+ }).trim();
50651
+ return output || null;
50652
+ } catch {
50653
+ return null;
50654
+ }
50655
+ }
50656
+ async function collectBugReportContext(session, errorContext) {
50657
+ const branch = await getCurrentBranch2(session.workingDir);
50658
+ let usageStats;
50659
+ if (session.usageStats) {
50660
+ const stats = session.usageStats;
50661
+ const contextPercent = stats.contextWindowSize > 0 ? Math.round(stats.contextTokens / stats.contextWindowSize * 100) : 0;
50662
+ usageStats = {
50663
+ model: stats.modelDisplayName || stats.primaryModel,
50664
+ contextPercent,
50665
+ cost: stats.totalCostUSD.toFixed(2)
50666
+ };
50667
+ }
50668
+ return {
50669
+ version: VERSION,
50670
+ claudeCliVersion: getClaudeCliVersion(),
50671
+ platform: session.platform.displayName,
50672
+ platformType: session.platform.platformType,
50673
+ nodeVersion: process.version,
50674
+ osVersion: `${process.platform} ${process.arch}`,
50675
+ sessionId: session.sessionId,
50676
+ claudeSessionId: session.claudeSessionId,
50677
+ workingDir: sanitizePath(session.workingDir),
50678
+ branch,
50679
+ worktreeBranch: session.worktreeInfo?.branch,
50680
+ usageStats,
50681
+ recentEvents: getRecentEvents(session),
50682
+ errorContext
50683
+ };
50684
+ }
50685
+ function generateIssueTitle(description) {
50686
+ let title = description.replace(/\s+/g, " ").trim();
50687
+ if (title.length > 80) {
50688
+ title = title.substring(0, 77) + "...";
50689
+ }
50690
+ return title;
50691
+ }
50692
+ function formatRecentEvents(events) {
50693
+ if (events.length === 0) {
50694
+ return "_No recent events_";
50695
+ }
50696
+ return events.map((e) => {
50697
+ const time = new Date(e.timestamp).toISOString().substring(11, 19);
50698
+ return `[${time}] ${e.type}: ${sanitizeText(e.summary)}`;
50699
+ }).join(`
50700
+ `);
50701
+ }
50702
+ function formatIssueBody(context, userDescription, imageUrls = []) {
50703
+ const sections = [];
50704
+ sections.push(`## Description
50705
+
50706
+ ${sanitizeText(userDescription)}`);
50707
+ if (imageUrls.length > 0) {
50708
+ const imageSection = imageUrls.map((url, i) => `![Screenshot ${i + 1}](${url})`).join(`
50709
+
50710
+ `);
50711
+ sections.push(`## Screenshots
50712
+
50713
+ ${imageSection}`);
50714
+ }
50715
+ sections.push(`## Environment
50716
+
50717
+ | Property | Value |
50718
+ |----------|-------|
50719
+ | claude-threads | v${context.version} |
50720
+ | Claude CLI | ${context.claudeCliVersion || "unknown"} |
50721
+ | Platform | ${context.platformType} (${context.platform}) |
50722
+ | Node.js | ${context.nodeVersion} |
50723
+ | OS | ${context.osVersion} |`);
50724
+ sections.push(`## Session Context
50725
+
50726
+ | Property | Value |
50727
+ |----------|-------|
50728
+ | Session ID | \`${context.claudeSessionId.substring(0, 8)}\` |
50729
+ | Working Dir | \`${context.workingDir}\` |
50730
+ | Branch | ${context.branch || "N/A"} |
50731
+ | Worktree | ${context.worktreeBranch || "N/A"} |`);
50732
+ if (context.usageStats) {
50733
+ sections.push(`## Usage Stats
50734
+
50735
+ - **Model:** ${context.usageStats.model}
50736
+ - **Context:** ${context.usageStats.contextPercent}%
50737
+ - **Cost:** $${context.usageStats.cost}`);
50738
+ }
50739
+ sections.push(`## Recent Events
50740
+
50741
+ \`\`\`
50742
+ ${formatRecentEvents(context.recentEvents)}
50743
+ \`\`\``);
50744
+ if (context.errorContext) {
50745
+ sections.push(`## Error Details
50746
+
50747
+ **Error message:**
50748
+ \`\`\`
50749
+ ${sanitizeText(context.errorContext.message)}
50750
+ \`\`\`
50751
+
50752
+ **Occurred at:** ${context.errorContext.timestamp.toISOString()}`);
50753
+ }
50754
+ sections.push(`---
50755
+ _Reported via claude-threads bug report feature_`);
50756
+ return sections.join(`
50757
+
50758
+ `);
50759
+ }
50760
+ function escapeShell(str) {
50761
+ return str.replace(/"/g, "\\\"");
50762
+ }
50763
+ function checkGitHubCli() {
50764
+ try {
50765
+ execSync2("gh --version", {
50766
+ encoding: "utf-8",
50767
+ timeout: 5000,
50768
+ stdio: ["pipe", "pipe", "pipe"]
50769
+ });
50770
+ } catch {
50771
+ return {
50772
+ installed: false,
50773
+ authenticated: false,
50774
+ error: "GitHub CLI not installed. Install it with: `brew install gh` or see https://cli.github.com"
50775
+ };
50776
+ }
50777
+ try {
50778
+ execSync2("gh auth status", {
50779
+ encoding: "utf-8",
50780
+ timeout: 5000,
50781
+ stdio: ["pipe", "pipe", "pipe"]
50782
+ });
50783
+ } catch {
50784
+ return {
50785
+ installed: true,
50786
+ authenticated: false,
50787
+ error: "Not logged into GitHub CLI. Run `gh auth login` to authenticate."
50788
+ };
50789
+ }
50790
+ return { installed: true, authenticated: true };
50791
+ }
50792
+ async function createGitHubIssue(title, body, workingDir) {
50793
+ const ghStatus = checkGitHubCli();
50794
+ if (!ghStatus.installed || !ghStatus.authenticated) {
50795
+ throw new Error(ghStatus.error);
50796
+ }
50797
+ const bodyFile = join4(tmpdir2(), `bug-body-${Date.now()}.md`);
50798
+ try {
50799
+ writeFileSync4(bodyFile, body, "utf-8");
50800
+ const cmd = `gh issue create --repo "${GITHUB_REPO}" --title "${escapeShell(title)}" --body-file "${bodyFile}"`;
50801
+ const result = execSync2(cmd, {
50802
+ cwd: workingDir,
50803
+ encoding: "utf-8",
50804
+ timeout: 30000,
50805
+ stdio: ["pipe", "pipe", "pipe"]
50806
+ });
50807
+ return result.trim();
50808
+ } finally {
50809
+ try {
50810
+ unlinkSync2(bodyFile);
50811
+ } catch {}
50812
+ }
50813
+ }
50814
+ function formatBugPreview(title, description, context, imageUrls, imageErrors, formatter) {
50815
+ const lines = [];
50816
+ lines.push(`${formatter.formatBold("Bug Report Preview")}`);
50817
+ lines.push("");
50818
+ lines.push(`${formatter.formatBold("Title:")} ${title}`);
50819
+ lines.push("");
50820
+ lines.push(formatter.formatBlockquote(description));
50821
+ lines.push("");
50822
+ lines.push(formatter.formatBold("Environment:"));
50823
+ lines.push(formatter.formatListItem(`claude-threads v${context.version}`));
50824
+ lines.push(formatter.formatListItem(`Claude CLI ${context.claudeCliVersion || "unknown"}`));
50825
+ lines.push(formatter.formatListItem(`Platform: ${context.platformType}`));
50826
+ lines.push(formatter.formatListItem(`Branch: ${context.branch || "N/A"}`));
50827
+ if (context.usageStats) {
50828
+ lines.push("");
50829
+ lines.push(formatter.formatBold("Usage:"));
50830
+ lines.push(formatter.formatListItem(`Model: ${context.usageStats.model}`));
50831
+ lines.push(formatter.formatListItem(`Context: ${context.usageStats.contextPercent}%`));
50832
+ }
50833
+ if (context.recentEvents.length > 0) {
50834
+ lines.push("");
50835
+ lines.push(formatter.formatBold(`Recent Events (${context.recentEvents.length}):`));
50836
+ const lastFew = context.recentEvents.slice(-3);
50837
+ for (const event of lastFew) {
50838
+ lines.push(formatter.formatListItem(`${event.type}: ${event.summary.substring(0, 40)}...`));
50839
+ }
50840
+ }
50841
+ if (imageUrls.length > 0 || imageErrors.length > 0) {
50842
+ lines.push("");
50843
+ lines.push(formatter.formatBold("Screenshots:"));
50844
+ if (imageUrls.length > 0) {
50845
+ lines.push(formatter.formatListItem(`\u2705 ${imageUrls.length} image(s) uploaded successfully`));
50846
+ }
50847
+ if (imageErrors.length > 0) {
50848
+ lines.push(formatter.formatListItem(`\u26A0\uFE0F ${imageErrors.length} image(s) failed: ${imageErrors[0]}`));
50849
+ }
50850
+ }
50851
+ lines.push("");
50852
+ lines.push(`React ${formatter.formatCode("\uD83D\uDC4D")} to create GitHub issue or ${formatter.formatCode("\uD83D\uDC4E")} to cancel`);
50853
+ return lines.join(`
50854
+ `);
50855
+ }
50856
+
50491
50857
  // src/utils/battery.ts
50492
50858
  import { exec as exec2 } from "child_process";
50493
50859
  import { promisify as promisify2 } from "util";
@@ -50762,58 +51128,6 @@ class KeepAliveManager {
50762
51128
  }
50763
51129
  var keepAlive = new KeepAliveManager;
50764
51130
 
50765
- // src/claude/version-check.ts
50766
- var import_semver3 = __toESM(require_semver2(), 1);
50767
- import { execSync } from "child_process";
50768
- var CLAUDE_CLI_VERSION_RANGE = ">=2.0.74 <2.2.0";
50769
- function getClaudeCliVersion() {
50770
- const claudePath = process.env.CLAUDE_PATH || "claude";
50771
- try {
50772
- const output = execSync(`${claudePath} --version`, {
50773
- encoding: "utf8",
50774
- timeout: 5000,
50775
- stdio: ["pipe", "pipe", "pipe"]
50776
- }).trim();
50777
- const match = output.match(/^([\d.]+)/);
50778
- return match ? match[1] : null;
50779
- } catch {
50780
- return null;
50781
- }
50782
- }
50783
- function isVersionCompatible(version) {
50784
- const semverVersion = import_semver3.coerce(version);
50785
- if (!semverVersion)
50786
- return false;
50787
- return import_semver3.satisfies(semverVersion, CLAUDE_CLI_VERSION_RANGE);
50788
- }
50789
- function validateClaudeCli() {
50790
- const version = getClaudeCliVersion();
50791
- if (!version) {
50792
- return {
50793
- installed: false,
50794
- version: null,
50795
- compatible: false,
50796
- message: "Claude CLI not found. Install it with: bun install -g @anthropic-ai/claude-code"
50797
- };
50798
- }
50799
- const compatible = isVersionCompatible(version);
50800
- if (!compatible) {
50801
- return {
50802
- installed: true,
50803
- version,
50804
- compatible: false,
50805
- message: `Claude CLI version ${version} is not compatible. Required: ${CLAUDE_CLI_VERSION_RANGE}
50806
- ` + `Install a compatible version: bun install -g @anthropic-ai/claude-code@2.0.76`
50807
- };
50808
- }
50809
- return {
50810
- installed: true,
50811
- version,
50812
- compatible: true,
50813
- message: `Claude CLI ${version} \u2713`
50814
- };
50815
- }
50816
-
50817
51131
  // src/session/commands.ts
50818
51132
  var log11 = createLogger("commands");
50819
51133
  function sessionLog2(session) {
@@ -51215,6 +51529,71 @@ async function deferUpdate(session, username, updateManager) {
51215
51529
  await postSuccess(session, `\u23F8\uFE0F ${formatter.formatBold("Update deferred")} for 1 hour
51216
51530
  ` + formatter.formatItalic("Use !update now to apply earlier"));
51217
51531
  }
51532
+ async function reportBug(session, description, username, ctx, errorContext, attachedFiles) {
51533
+ const formatter = session.platform.getFormatter();
51534
+ if (!description && !errorContext) {
51535
+ await postInfo(session, `Usage: ${formatter.formatCode("!bug <description>")}
51536
+ ` + `Example: ${formatter.formatCode("!bug Session crashed when uploading large image")}
51537
+
51538
+ ` + `You can also attach screenshots to the !bug message.
51539
+ ` + `Or react with \uD83D\uDC1B on any error message to report it.`);
51540
+ return;
51541
+ }
51542
+ const ghStatus = checkGitHubCli();
51543
+ if (!ghStatus.installed || !ghStatus.authenticated) {
51544
+ await postError(session, ghStatus.error || "GitHub CLI not configured");
51545
+ return;
51546
+ }
51547
+ const bugDescription = description || (errorContext ? `Error: ${errorContext.message.substring(0, 200)}` : "Unknown error");
51548
+ const context = await collectBugReportContext(session, errorContext);
51549
+ let imageUrls = [];
51550
+ let imageErrors = [];
51551
+ const downloadFile = session.platform.downloadFile;
51552
+ if (attachedFiles && attachedFiles.length > 0 && downloadFile) {
51553
+ await postInfo(session, `\uD83D\uDCE4 Uploading ${attachedFiles.length} image(s)...`);
51554
+ const uploadResults = await uploadImages(attachedFiles, (fileId) => downloadFile(fileId));
51555
+ imageUrls = uploadResults.filter((r) => r.success && typeof r.url === "string").map((r) => r.url);
51556
+ imageErrors = uploadResults.filter((r) => !r.success).map((r) => `${r.originalFile.name}: ${r.error}`);
51557
+ }
51558
+ const title = generateIssueTitle(bugDescription);
51559
+ const body = formatIssueBody(context, bugDescription, imageUrls);
51560
+ const preview = formatBugPreview(title, bugDescription, context, imageUrls, imageErrors, formatter);
51561
+ const previewMessage = `\uD83D\uDC1B ${preview}`;
51562
+ const post = await session.platform.createInteractivePost(previewMessage, [APPROVAL_EMOJIS[0], DENIAL_EMOJIS[0]], session.threadId);
51563
+ session.pendingBugReport = {
51564
+ postId: post.id,
51565
+ title,
51566
+ body,
51567
+ userDescription: bugDescription,
51568
+ imageUrls,
51569
+ imageErrors,
51570
+ errorContext
51571
+ };
51572
+ ctx.ops.registerPost(post.id, session.threadId);
51573
+ updateLastMessage(session, post);
51574
+ sessionLog2(session).info(`\uD83D\uDC1B Bug report preview created by @${username}: ${title}`);
51575
+ }
51576
+ async function handleBugReportApproval(session, isApproved, username) {
51577
+ const pending = session.pendingBugReport;
51578
+ if (!pending)
51579
+ return;
51580
+ const formatter = session.platform.getFormatter();
51581
+ if (isApproved) {
51582
+ try {
51583
+ const issueUrl = await createGitHubIssue(pending.title, pending.body, session.workingDir);
51584
+ await withErrorHandling(() => session.platform.updatePost(pending.postId, `\u2705 ${formatter.formatBold("Bug report submitted")}: ${issueUrl}`), { action: "Update bug report post", session });
51585
+ sessionLog2(session).info(`\uD83D\uDC1B Bug report created by @${username}: ${issueUrl}`);
51586
+ } catch (err) {
51587
+ const errorMessage = err instanceof Error ? err.message : String(err);
51588
+ await withErrorHandling(() => session.platform.updatePost(pending.postId, `\u274C ${formatter.formatBold("Failed to create bug report")}: ${errorMessage}`), { action: "Update bug report post", session });
51589
+ sessionLog2(session).error(`Failed to create bug report: ${errorMessage}`);
51590
+ }
51591
+ } else {
51592
+ await withErrorHandling(() => session.platform.updatePost(pending.postId, `\uD83D\uDEAB ${formatter.formatBold("Bug report cancelled")} by ${formatter.formatUserMention(username)}`), { action: "Update bug report post", session });
51593
+ sessionLog2(session).info(`\uD83D\uDC1B Bug report cancelled by @${username}`);
51594
+ }
51595
+ session.pendingBugReport = undefined;
51596
+ }
51218
51597
 
51219
51598
  // src/session/worktree.ts
51220
51599
  import { randomUUID as randomUUID3 } from "crypto";
@@ -51635,7 +52014,8 @@ async function cleanupWorktreeCommand(session, username, hasOtherSessionsUsingWo
51635
52014
  // src/commands/parser.ts
51636
52015
  var CLAUDE_ALLOWED_COMMANDS = new Set([
51637
52016
  "cd",
51638
- "worktree list"
52017
+ "worktree list",
52018
+ "bug"
51639
52019
  ]);
51640
52020
  var COMMAND_PATTERNS = [
51641
52021
  ["stop", /^!(?:stop|cancel)\s*$/i],
@@ -51652,7 +52032,8 @@ var COMMAND_PATTERNS = [
51652
52032
  ["context", /^!context\s*$/i],
51653
52033
  ["cost", /^!cost\s*$/i],
51654
52034
  ["compact", /^!compact\s*$/i],
51655
- ["kill", /^!kill\s*$/i]
52035
+ ["kill", /^!kill\s*$/i],
52036
+ ["bug", /^!bug(?:\s+(.+))?$/i]
51656
52037
  ];
51657
52038
  function parseCommand(text) {
51658
52039
  for (const [command, pattern] of COMMAND_PATTERNS) {
@@ -51684,6 +52065,14 @@ function parseClaudeCommand(text) {
51684
52065
  match: worktreeListMatch[0].trimEnd()
51685
52066
  };
51686
52067
  }
52068
+ const bugMatch = text.match(/^!bug\s+(.+)$/m);
52069
+ if (bugMatch && CLAUDE_ALLOWED_COMMANDS.has("bug")) {
52070
+ return {
52071
+ command: "bug",
52072
+ args: bugMatch[1].trim(),
52073
+ match: bugMatch[0].trimEnd()
52074
+ };
52075
+ }
51687
52076
  return null;
51688
52077
  }
51689
52078
  function isClaudeAllowedCommand(command) {
@@ -51783,6 +52172,9 @@ ${plainMessage}
51783
52172
  }
51784
52173
  break;
51785
52174
  }
52175
+ case "bug":
52176
+ await reportBug(session, args, session.startedBy, ctx);
52177
+ break;
51786
52178
  }
51787
52179
  }
51788
52180
  function extractAndUpdatePullRequest(text, session, ctx) {
@@ -51902,6 +52294,7 @@ function formatEvent(session, e, ctx) {
51902
52294
  if (tool.id) {
51903
52295
  session.activeToolStarts.set(tool.id, Date.now());
51904
52296
  }
52297
+ trackEvent(session, "tool_use", tool.name);
51905
52298
  const worktreeInfo = session.worktreeInfo ? { path: session.worktreeInfo.worktreePath, branch: session.worktreeInfo.branch } : undefined;
51906
52299
  return formatToolUse(tool.name, tool.input || {}, session.platform.getFormatter(), { detailed: true, worktreeInfo }) || null;
51907
52300
  }
@@ -51918,8 +52311,10 @@ function formatEvent(session, e, ctx) {
51918
52311
  session.activeToolStarts.delete(result.tool_use_id);
51919
52312
  }
51920
52313
  }
51921
- if (result.is_error)
52314
+ if (result.is_error) {
52315
+ trackEvent(session, "tool_error", "Tool execution failed");
51922
52316
  return ` \u21B3 \u274C Error${elapsed}`;
52317
+ }
51923
52318
  if (elapsed)
51924
52319
  return ` \u21B3 \u2713${elapsed}`;
51925
52320
  return null;
@@ -51935,8 +52330,10 @@ function formatEvent(session, e, ctx) {
51935
52330
  return null;
51936
52331
  }
51937
52332
  case "system": {
51938
- if (e.subtype === "error")
52333
+ if (e.subtype === "error") {
52334
+ trackEvent(session, "system_error", String(e.error).substring(0, 80));
51939
52335
  return `\u274C ${e.error}`;
52336
+ }
51940
52337
  return null;
51941
52338
  }
51942
52339
  case "user": {
@@ -51978,6 +52375,28 @@ async function handleExitPlanMode(session, toolUseId, ctx) {
51978
52375
  session.pendingApproval = { postId: post.id, type: "plan", toolUseId };
51979
52376
  ctx.ops.stopTyping(session);
51980
52377
  }
52378
+ async function cleanupOrphanedTaskPosts(session, currentTaskPostId) {
52379
+ try {
52380
+ const history = await session.platform.getThreadHistory(session.threadId, { limit: 50 });
52381
+ const taskPostPattern = /^(?:(?:---|___|\*\*\*|\u2014+)\s*\n)?\uD83D\uDCCB/;
52382
+ let cleanedCount = 0;
52383
+ for (const msg of history) {
52384
+ if (msg.id === currentTaskPostId)
52385
+ continue;
52386
+ if (!taskPostPattern.test(msg.message))
52387
+ continue;
52388
+ sessionLog4(session).info(`Cleaning up orphaned task post ${msg.id.substring(0, 8)}`);
52389
+ await session.platform.unpinPost(msg.id).catch(() => {});
52390
+ await session.platform.deletePost(msg.id).catch(() => {});
52391
+ cleanedCount++;
52392
+ }
52393
+ if (cleanedCount > 0) {
52394
+ sessionLog4(session).info(`Cleaned up ${cleanedCount} orphaned task post(s)`);
52395
+ }
52396
+ } catch (err) {
52397
+ sessionLog4(session).debug(`Task cleanup failed: ${err}`);
52398
+ }
52399
+ }
51981
52400
  async function handleTodoWrite(session, input, ctx) {
51982
52401
  const releaseLock = await acquireTaskListLock(session);
51983
52402
  try {
@@ -52066,14 +52485,20 @@ async function handleTodoWriteWithLock(session, input, ctx) {
52066
52485
  const displayMessage = session.tasksMinimized ? minimizedMessage : fullMessage;
52067
52486
  const existingTasksPostId = session.tasksPostId;
52068
52487
  if (existingTasksPostId) {
52069
- await withErrorHandling(() => session.platform.updatePost(existingTasksPostId, displayMessage), { action: "Update tasks", session });
52070
- } else {
52488
+ const updated = await withErrorHandling(() => session.platform.updatePost(existingTasksPostId, displayMessage), { action: "Update tasks", session });
52489
+ if (updated === undefined) {
52490
+ sessionLog4(session).warn(`Task post ${existingTasksPostId.substring(0, 8)} update failed, will create new one`);
52491
+ session.tasksPostId = null;
52492
+ }
52493
+ }
52494
+ if (!session.tasksPostId) {
52071
52495
  const post = await withErrorHandling(() => session.platform.createInteractivePost(displayMessage, [TASK_TOGGLE_EMOJIS[0]], session.threadId), { action: "Create tasks post", session });
52072
52496
  if (post) {
52073
52497
  session.tasksPostId = post.id;
52074
52498
  ctx.ops.registerPost(post.id, session.threadId);
52075
52499
  updateLastMessage(session, post);
52076
52500
  await session.platform.pinPost(post.id).catch(() => {});
52501
+ await cleanupOrphanedTaskPosts(session, post.id);
52077
52502
  }
52078
52503
  }
52079
52504
  ctx.ops.updateStickyMessage().catch(() => {});
@@ -52472,6 +52897,36 @@ async function handleUpdateReaction(session, postId, emojiName, username, ctx, u
52472
52897
  }
52473
52898
  return true;
52474
52899
  }
52900
+ async function handleBugReportReaction(session, postId, emojiName, username, ctx) {
52901
+ if (!isBugReportEmoji(emojiName)) {
52902
+ return false;
52903
+ }
52904
+ if (!session.lastError || session.lastError.postId !== postId) {
52905
+ return false;
52906
+ }
52907
+ if (session.startedBy !== username && !session.platform.isUserAllowed(username) && !session.sessionAllowedUsers.has(username)) {
52908
+ return false;
52909
+ }
52910
+ sessionLog5(session).info(`\uD83D\uDC1B @${username} triggered bug report from error reaction`);
52911
+ await reportBug(session, undefined, username, ctx, session.lastError);
52912
+ return true;
52913
+ }
52914
+ async function handleBugApprovalReaction(session, postId, emojiName, username, _ctx) {
52915
+ const pending = session.pendingBugReport;
52916
+ if (!pending || pending.postId !== postId) {
52917
+ return false;
52918
+ }
52919
+ if (session.startedBy !== username && !session.platform.isUserAllowed(username) && !session.sessionAllowedUsers.has(username)) {
52920
+ return false;
52921
+ }
52922
+ const isApprove = isApprovalEmoji(emojiName);
52923
+ const isDeny = isDenialEmoji(emojiName);
52924
+ if (!isApprove && !isDeny) {
52925
+ return false;
52926
+ }
52927
+ await handleBugReportApproval(session, isApprove, username);
52928
+ return true;
52929
+ }
52475
52930
 
52476
52931
  // src/session/lifecycle.ts
52477
52932
  import { randomUUID as randomUUID4 } from "crypto";
@@ -52686,7 +53141,8 @@ ${CHAT_PLATFORM_PROMPT}`;
52686
53141
  firstPrompt: options.prompt,
52687
53142
  messageCount: 0,
52688
53143
  isProcessing: true,
52689
- statusBarTimer: null
53144
+ statusBarTimer: null,
53145
+ recentEvents: []
52690
53146
  };
52691
53147
  mutableSessions(ctx).set(sessionId, session);
52692
53148
  ctx.ops.registerPost(post.id, actualThreadId);
@@ -52843,7 +53299,8 @@ ${CHAT_PLATFORM_PROMPT}`;
52843
53299
  messageCount: state.messageCount ?? 0,
52844
53300
  isProcessing: false,
52845
53301
  lifecyclePostId: state.lifecyclePostId,
52846
- statusBarTimer: null
53302
+ statusBarTimer: null,
53303
+ recentEvents: []
52847
53304
  };
52848
53305
  mutableSessions(ctx).set(sessionId, session);
52849
53306
  if (session.worktreeInfo) {
@@ -54066,6 +54523,16 @@ class SessionManager extends EventEmitter4 {
54066
54523
  await handleTaskToggleReaction(session, action, this.getContext());
54067
54524
  return;
54068
54525
  }
54526
+ if (action === "added" && session.lastError?.postId === postId) {
54527
+ const handled = await handleBugReportReaction(session, postId, emojiName, username, this.getContext());
54528
+ if (handled)
54529
+ return;
54530
+ }
54531
+ if (action === "added" && session.pendingBugReport?.postId === postId) {
54532
+ const handled = await handleBugApprovalReaction(session, postId, emojiName, username, this.getContext());
54533
+ if (handled)
54534
+ return;
54535
+ }
54069
54536
  }
54070
54537
  getContextPromptHandler() {
54071
54538
  return {
@@ -54315,7 +54782,7 @@ class SessionManager extends EventEmitter4 {
54315
54782
  for (const entry of entries) {
54316
54783
  if (!entry.isDirectory())
54317
54784
  continue;
54318
- const worktreePath = join4(worktreesDir, entry.name);
54785
+ const worktreePath = join5(worktreesDir, entry.name);
54319
54786
  if (activeWorktrees.has(worktreePath)) {
54320
54787
  log18.debug(`Worktree in use by persisted session, skipping: ${entry.name}`);
54321
54788
  continue;
@@ -54493,6 +54960,12 @@ class SessionManager extends EventEmitter4 {
54493
54960
  return;
54494
54961
  await enableInteractivePermissions(session, username, this.getContext());
54495
54962
  }
54963
+ async reportBug(threadId, description, username, files) {
54964
+ const session = this.findSessionByThreadId(threadId);
54965
+ if (!session)
54966
+ return;
54967
+ await reportBug(session, description, username, this.getContext(), undefined, files);
54968
+ }
54496
54969
  async showUpdateStatus(threadId, _username) {
54497
54970
  const session = this.findSessionByThreadId(threadId);
54498
54971
  if (!session)
@@ -65233,7 +65706,8 @@ async function handleMessage(client, session, post, user, options) {
65233
65706
  [code("!update defer"), "Defer pending update for 1 hour"],
65234
65707
  [code("!escape"), "Interrupt current task (session stays active)"],
65235
65708
  [code("!stop"), "Stop this session"],
65236
- [code("!kill"), "Emergency shutdown (kills ALL sessions, exits bot)"]
65709
+ [code("!kill"), "Emergency shutdown (kills ALL sessions, exits bot)"],
65710
+ [code("!bug <description>"), "Report a bug (creates GitHub issue)"]
65237
65711
  ]);
65238
65712
  await client.createPost(`${formatter.formatBold("Commands:")}
65239
65713
 
@@ -65328,6 +65802,12 @@ Release notes not available. See ${formatter.formatLink("GitHub releases", "http
65328
65802
  await session.sendFollowUp(threadRoot, claudeCommand);
65329
65803
  }
65330
65804
  return;
65805
+ case "bug":
65806
+ if (isAllowed) {
65807
+ const files3 = post.metadata?.files;
65808
+ await session.reportBug(threadRoot, parsed.args, username, files3);
65809
+ }
65810
+ return;
65331
65811
  case "kill":
65332
65812
  return;
65333
65813
  }
@@ -65799,7 +66279,7 @@ class UpdateScheduler extends EventEmitter8 {
65799
66279
 
65800
66280
  // src/auto-update/installer.ts
65801
66281
  import { spawn as spawn5 } from "child_process";
65802
- import { existsSync as existsSync11, readFileSync as readFileSync8, writeFileSync as writeFileSync4, mkdirSync as mkdirSync3 } from "fs";
66282
+ import { existsSync as existsSync11, readFileSync as readFileSync8, writeFileSync as writeFileSync5, mkdirSync as mkdirSync3 } from "fs";
65803
66283
  import { dirname as dirname6, resolve as resolve6 } from "path";
65804
66284
  import { homedir as homedir4 } from "os";
65805
66285
  var log21 = createLogger("installer");
@@ -65822,7 +66302,7 @@ function saveUpdateState(state) {
65822
66302
  if (!existsSync11(dir)) {
65823
66303
  mkdirSync3(dir, { recursive: true });
65824
66304
  }
65825
- writeFileSync4(STATE_PATH, JSON.stringify(state, null, 2), "utf-8");
66305
+ writeFileSync5(STATE_PATH, JSON.stringify(state, null, 2), "utf-8");
65826
66306
  log21.debug("Update state saved");
65827
66307
  } catch (err) {
65828
66308
  log21.warn(`Failed to save update state: ${err}`);
@@ -65831,7 +66311,7 @@ function saveUpdateState(state) {
65831
66311
  function clearUpdateState() {
65832
66312
  try {
65833
66313
  if (existsSync11(STATE_PATH)) {
65834
- writeFileSync4(STATE_PATH, "{}", "utf-8");
66314
+ writeFileSync5(STATE_PATH, "{}", "utf-8");
65835
66315
  }
65836
66316
  } catch (err) {
65837
66317
  log21.warn(`Failed to clear update state: ${err}`);
@@ -33831,9 +33831,11 @@ ${code}
33831
33831
  `);
33832
33832
  }
33833
33833
  formatMarkdown(content) {
33834
- return content.replace(/\n{3,}/g, `
33834
+ let processed = content.replace(/(?<=\n)```(?=\S)(?![a-zA-Z]*\n)/g, "```\n");
33835
+ processed = processed.replace(/\n{3,}/g, `
33835
33836
 
33836
33837
  `);
33838
+ return processed;
33837
33839
  }
33838
33840
  }
33839
33841
 
@@ -34104,6 +34106,7 @@ function convertMarkdownToSlack(content) {
34104
34106
  for (let i = 0;i < codeBlocks.length; i++) {
34105
34107
  preserved = preserved.replace(`${CODE_BLOCK_PLACEHOLDER}${i}\x00`, codeBlocks[i]);
34106
34108
  }
34109
+ preserved = preserved.replace(/(?<=\n)```(?=\S)(?![a-zA-Z]*\n)/g, "```\n");
34107
34110
  return preserved;
34108
34111
  }
34109
34112
  function convertMarkdownTablesToSlack(content) {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "claude-threads",
3
- "version": "0.49.0",
3
+ "version": "0.51.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",