claude-threads 1.40.0 → 1.40.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 CHANGED
@@ -5,6 +5,18 @@ All notable changes to this project will be documented in this file.
5
5
  The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/),
6
6
  and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
7
7
 
8
+ ## [1.40.1] - 2026-10-30
9
+
10
+ ### Added
11
+ - **`bugReports: { repo: owner/name }` files `!bug` reports in your own repository** (#577). `true` and `false` keep their meaning, and a malformed `repo` disables bug reports rather than falling back to the public default.
12
+
13
+ ### Changed
14
+ - **Screenshots no longer go to catbox.moe** (#577). They are attached to the GitHub issue with `gh issue create --attach`, which needs `gh` 2.99 or newer and write access to the target repository. Without those the report is filed without them and says so. Attachments on a public repository stay online permanently, even if the issue is deleted, and the preview warns about that.
15
+ - **Nothing leaves before approval.** Screenshots used to be uploaded while the preview was being built, so a cancelled report had already sent them. The preview now also names the destination repository and whether it is public.
16
+
17
+ ### Security
18
+ - **The `!bug` title can no longer run commands on the host.** The title went into a shell string with only double quotes escaped, so `$(...)` and backticks in a bug description were executed. `gh` is now called with an argument list.
19
+
8
20
  ## [1.40.0] - 2026-10-30
9
21
 
10
22
  ### Fixed
package/dist/index.js CHANGED
@@ -2345,6 +2345,10 @@ function resolveWatchesEnabled(value, fieldPath) {
2345
2345
  function resolveAuditLogEnabled(value, fieldPath) {
2346
2346
  return resolveBooleanFeature(value, fieldPath ?? "auditLog", { default: false, verb: "audit log stays off" });
2347
2347
  }
2348
+ function isBugReportRepoConfig(value) {
2349
+ const repo = value?.repo;
2350
+ return typeof repo === "string" && GITHUB_REPO_PATTERN.test(repo);
2351
+ }
2348
2352
  function resolveBugReportsEnabled(value, fieldPath) {
2349
2353
  if (value === undefined)
2350
2354
  return true;
@@ -2352,9 +2356,14 @@ function resolveBugReportsEnabled(value, fieldPath) {
2352
2356
  return true;
2353
2357
  if (value === false)
2354
2358
  return false;
2355
- console.warn(`Invalid ${fieldPath ?? "bugReports"} config: expected boolean, got ${JSON.stringify(value)} — ` + `bug reports are DISABLED (this flag fails closed: it controls data leaving your infrastructure)`);
2359
+ if (isBugReportRepoConfig(value))
2360
+ return true;
2361
+ console.warn(`Invalid ${fieldPath ?? "bugReports"} config: expected boolean or { repo: owner/name }, got ${JSON.stringify(value)} — ` + `bug reports are DISABLED (this flag fails closed: it controls data leaving your infrastructure)`);
2356
2362
  return false;
2357
2363
  }
2364
+ function resolveBugReportRepo(value) {
2365
+ return isBugReportRepoConfig(value) ? value.repo : DEFAULT_BUG_REPORT_REPO;
2366
+ }
2358
2367
  function isRemoteMcpServer(server) {
2359
2368
  return server.type === "http" || server.type === "sse";
2360
2369
  }
@@ -2473,7 +2482,7 @@ function effectivePermissionMode(input) {
2473
2482
  return "default";
2474
2483
  return input.botWideMode;
2475
2484
  }
2476
- var OVERHEAD_VISIBILITY_VALUES, DEFAULT_OVERHEAD_VISIBILITY = "full", RECONNECT_POLICY_VALUES, DEFAULT_RECONNECT_POLICY = "retry", PRESENTATION_MODE_VALUES, PRESENTATION_MODE_EXPANSIONS, TURN_MARKER_VALUES, DEFAULT_TURN_MARKER, DEFAULT_TURN_MARKER_EMOJI = "checkered_flag", TURN_COMPLETE_EVENT_TYPE = "claude_threads_turn_complete", TURN_COMPLETE_PAYLOAD_VERSION = 1, TOOL_ACTIVITY_VALUES, TOOL_DETAILS_VALUES, DEFAULT_TOOL_ACTIVITY, DEFAULT_MEMORY_CONFIG, MEMORY_DISABLED, BOT_MCP_SERVER_NAME = "claude-threads-mcp", STDIO_KEYS, REMOTE_KEYS, LIMITS_DEFAULTS, MODE_INFO;
2485
+ var OVERHEAD_VISIBILITY_VALUES, DEFAULT_OVERHEAD_VISIBILITY = "full", RECONNECT_POLICY_VALUES, DEFAULT_RECONNECT_POLICY = "retry", PRESENTATION_MODE_VALUES, PRESENTATION_MODE_EXPANSIONS, TURN_MARKER_VALUES, DEFAULT_TURN_MARKER, DEFAULT_TURN_MARKER_EMOJI = "checkered_flag", TURN_COMPLETE_EVENT_TYPE = "claude_threads_turn_complete", TURN_COMPLETE_PAYLOAD_VERSION = 1, TOOL_ACTIVITY_VALUES, TOOL_DETAILS_VALUES, DEFAULT_TOOL_ACTIVITY, DEFAULT_MEMORY_CONFIG, MEMORY_DISABLED, DEFAULT_BUG_REPORT_REPO = "anneschuth/claude-threads", GITHUB_REPO_PATTERN, BOT_MCP_SERVER_NAME = "claude-threads-mcp", STDIO_KEYS, REMOTE_KEYS, LIMITS_DEFAULTS, MODE_INFO;
2477
2486
  var init_types = __esm(() => {
2478
2487
  OVERHEAD_VISIBILITY_VALUES = ["full", "minimal", "hidden"];
2479
2488
  RECONNECT_POLICY_VALUES = ["retry", "exit"];
@@ -2503,6 +2512,7 @@ var init_types = __esm(() => {
2503
2512
  channelLayer: false,
2504
2513
  distillation: false
2505
2514
  };
2515
+ GITHUB_REPO_PATTERN = /^[A-Za-z0-9][\w.-]*\/[A-Za-z0-9][\w.-]*$/;
2506
2516
  STDIO_KEYS = new Set(["type", "command", "args", "env"]);
2507
2517
  REMOTE_KEYS = new Set(["type", "url", "headers"]);
2508
2518
  LIMITS_DEFAULTS = {
@@ -57530,8 +57540,8 @@ function getUpdateInfo() {
57530
57540
  init_emoji();
57531
57541
 
57532
57542
  // src/operations/bug-report/handler.ts
57533
- import { execSync as execSync2 } from "child_process";
57534
- import { writeFileSync as writeFileSync5, unlinkSync as unlinkSync3 } from "fs";
57543
+ import { execSync as execSync2, execFileSync } from "child_process";
57544
+ import { writeFileSync as writeFileSync5, unlinkSync as unlinkSync3, mkdtempSync as mkdtempSync2, rmSync } from "fs";
57535
57545
  import { tmpdir as tmpdir4 } from "os";
57536
57546
  import { join as join9 } from "path";
57537
57547
 
@@ -57791,58 +57801,67 @@ class Redactor {
57791
57801
  }
57792
57802
 
57793
57803
  // src/operations/bug-report/handler.ts
57804
+ var import_semver4 = __toESM(require_semver2(), 1);
57794
57805
  init_version_check();
57795
57806
  var piiRedactor = new Redactor({ aggressive: true });
57796
- var CATBOX_API_URL = "https://catbox.moe/user/api.php";
57797
- async function uploadImageToCatbox(imageBuffer, filename) {
57798
- const arrayBuffer = imageBuffer.buffer.slice(imageBuffer.byteOffset, imageBuffer.byteOffset + imageBuffer.byteLength);
57799
- const formData = new FormData;
57800
- formData.append("reqtype", "fileupload");
57801
- formData.append("fileToUpload", new Blob([arrayBuffer]), filename);
57802
- const response = await fetch(CATBOX_API_URL, {
57803
- method: "POST",
57804
- body: formData
57805
- });
57806
- if (!response.ok) {
57807
- throw new Error(`Catbox upload failed: ${response.status} ${response.statusText}`);
57808
- }
57809
- const responseText = await response.text();
57810
- if (responseText.startsWith("https://")) {
57811
- return responseText.trim();
57812
- }
57813
- throw new Error(`Catbox upload failed: ${responseText}`);
57814
- }
57815
- async function uploadImages(files, downloadFile) {
57816
- const results = [];
57807
+ var GH_MIN_ATTACH_VERSION = "2.99.0";
57808
+ var MAX_SCREENSHOT_BYTES = 10 * 1024 * 1024;
57809
+ var ATTACH_PERMISSIONS = ["ADMIN", "MAINTAIN", "WRITE"];
57810
+ function selectScreenshots(files, target) {
57811
+ const screenshots = [];
57812
+ const skipped = [];
57817
57813
  for (const file of files) {
57818
57814
  if (!file.mimeType.startsWith("image/")) {
57819
- results.push({
57820
- success: false,
57821
- error: "Not an image file",
57822
- originalFile: file
57823
- });
57824
- continue;
57815
+ skipped.push(`${file.name}: not an image`);
57816
+ } else if (file.size > MAX_SCREENSHOT_BYTES) {
57817
+ skipped.push(`${file.name}: larger than 10 MB`);
57818
+ } else if (!target.canAttach) {
57819
+ skipped.push(`${file.name}: ${target.attachBlocker}`);
57820
+ } else {
57821
+ screenshots.push(file);
57825
57822
  }
57823
+ }
57824
+ return { screenshots, skipped };
57825
+ }
57826
+ async function saveScreenshots(files, downloadFile) {
57827
+ if (files.length === 0) {
57828
+ return { paths: [], failed: 0, cleanup: () => {} };
57829
+ }
57830
+ const dir = mkdtempSync2(join9(tmpdir4(), "bug-screenshots-"));
57831
+ const paths = [];
57832
+ let failed = 0;
57833
+ for (const [index, file] of files.entries()) {
57826
57834
  try {
57835
+ if (!downloadFile)
57836
+ throw new Error("Platform cannot download files");
57827
57837
  const buffer = await downloadFile(file.id);
57828
- const url = await uploadImageToCatbox(buffer, file.name);
57829
- results.push({
57830
- success: true,
57831
- url,
57832
- originalFile: file
57833
- });
57834
- } catch (err) {
57835
- results.push({
57836
- success: false,
57837
- error: err instanceof Error ? err.message : String(err),
57838
- originalFile: file
57839
- });
57838
+ const extension = (file.extension ?? file.mimeType.split("/")[1] ?? "").replace(/^\./, "").toLowerCase();
57839
+ const path = join9(dir, `screenshot-${index + 1}.${/^[a-z0-9]+$/.test(extension) ? extension : "png"}`);
57840
+ writeFileSync5(path, buffer, { mode: 384 });
57841
+ paths.push(path);
57842
+ } catch {
57843
+ failed++;
57840
57844
  }
57841
57845
  }
57842
- return results;
57846
+ return {
57847
+ paths,
57848
+ failed,
57849
+ cleanup: () => {
57850
+ try {
57851
+ rmSync(dir, { recursive: true, force: true });
57852
+ } catch {}
57853
+ }
57854
+ };
57855
+ }
57856
+ function omittedScreenshotsNote(count) {
57857
+ return `${count} screenshot(s) were shared in the chat thread but are not attached to this issue.`;
57858
+ }
57859
+ function withOmittedScreenshots(body, count) {
57860
+ return count > 0 ? `${body}
57861
+
57862
+ ${omittedScreenshotsNote(count)}` : body;
57843
57863
  }
57844
57864
  var MAX_RECENT_EVENTS = 10;
57845
- var GITHUB_REPO = "anneschuth/claude-threads";
57846
57865
  function trackEvent(session, type, summary) {
57847
57866
  if (!session.recentEvents) {
57848
57867
  session.recentEvents = [];
@@ -58033,18 +58052,15 @@ function formatLogEntries(entries) {
58033
58052
  }).join(`
58034
58053
  `);
58035
58054
  }
58036
- function formatIssueBody(context, userDescription, imageUrls = []) {
58055
+ function formatIssueBody(context, userDescription, omittedScreenshots = 0) {
58037
58056
  const sections = [];
58038
58057
  sections.push(`## Description
58039
58058
 
58040
58059
  ${sanitizeText(userDescription)}`);
58041
- if (imageUrls.length > 0) {
58042
- const imageSection = imageUrls.map((url, i) => `![Screenshot ${i + 1}](${url})`).join(`
58043
-
58044
- `);
58060
+ if (omittedScreenshots > 0) {
58045
58061
  sections.push(`## Screenshots
58046
58062
 
58047
- ${imageSection}`);
58063
+ ${omittedScreenshotsNote(omittedScreenshots)}`);
58048
58064
  }
58049
58065
  sections.push(`## Environment
58050
58066
 
@@ -58104,16 +58120,15 @@ _Reported via claude-threads bug report feature_`);
58104
58120
 
58105
58121
  `);
58106
58122
  }
58107
- function escapeShell(str) {
58108
- return str.replace(/"/g, "\\\"");
58109
- }
58110
- function checkGitHubCli(exec = execSync2) {
58123
+ var runGh = (args, options) => execFileSync("gh", args, options);
58124
+ var GH_CHECK_OPTIONS = {
58125
+ encoding: "utf-8",
58126
+ timeout: 5000,
58127
+ stdio: ["pipe", "pipe", "pipe"]
58128
+ };
58129
+ function checkGitHubCli(exec = runGh) {
58111
58130
  try {
58112
- exec("gh --version", {
58113
- encoding: "utf-8",
58114
- timeout: 5000,
58115
- stdio: ["pipe", "pipe", "pipe"]
58116
- });
58131
+ exec(["--version"], GH_CHECK_OPTIONS);
58117
58132
  } catch {
58118
58133
  return {
58119
58134
  installed: false,
@@ -58122,11 +58137,7 @@ function checkGitHubCli(exec = execSync2) {
58122
58137
  };
58123
58138
  }
58124
58139
  try {
58125
- exec("gh auth status", {
58126
- encoding: "utf-8",
58127
- timeout: 5000,
58128
- stdio: ["pipe", "pipe", "pipe"]
58129
- });
58140
+ exec(["auth", "status"], GH_CHECK_OPTIONS);
58130
58141
  } catch {
58131
58142
  return {
58132
58143
  installed: true,
@@ -58136,29 +58147,79 @@ function checkGitHubCli(exec = execSync2) {
58136
58147
  }
58137
58148
  return { installed: true, authenticated: true };
58138
58149
  }
58139
- async function createGitHubIssue(title, body, workingDir, exec = execSync2) {
58150
+ function inspectBugReportTarget(repo, exec = runGh) {
58151
+ let visibility;
58152
+ let viewerPermission;
58153
+ try {
58154
+ const info = JSON.parse(exec(["repo", "view", repo, "--json", "viewerPermission,visibility"], GH_CHECK_OPTIONS));
58155
+ visibility = info.visibility;
58156
+ viewerPermission = info.viewerPermission;
58157
+ } catch {
58158
+ return { repo, isPublic: true, canAttach: false, attachBlocker: `could not read ${repo}` };
58159
+ }
58160
+ const isPublic = visibility !== "PRIVATE" && visibility !== "INTERNAL";
58161
+ if (viewerPermission && !ATTACH_PERMISSIONS.includes(viewerPermission)) {
58162
+ return { repo, isPublic, canAttach: false, attachBlocker: `attaching needs write access to ${repo}` };
58163
+ }
58164
+ let ghVersion = null;
58165
+ try {
58166
+ ghVersion = import_semver4.coerce(exec(["--version"], GH_CHECK_OPTIONS));
58167
+ } catch {}
58168
+ if (ghVersion && import_semver4.lt(ghVersion, GH_MIN_ATTACH_VERSION)) {
58169
+ return { repo, isPublic, canAttach: false, attachBlocker: `attaching needs gh ${GH_MIN_ATTACH_VERSION} or newer` };
58170
+ }
58171
+ return { repo, isPublic, canAttach: true };
58172
+ }
58173
+ async function createGitHubIssue(title, body, workingDir, options) {
58174
+ const { repo, attachments = [], exec = runGh } = options;
58140
58175
  const ghStatus = checkGitHubCli(exec);
58141
58176
  if (!ghStatus.installed || !ghStatus.authenticated) {
58142
58177
  throw new Error(ghStatus.error);
58143
58178
  }
58144
58179
  const bodyFile = join9(tmpdir4(), `bug-body-${Date.now()}.md`);
58145
- try {
58146
- writeFileSync5(bodyFile, body, "utf-8");
58147
- const cmd = `gh issue create --repo "${GITHUB_REPO}" --title "${escapeShell(title)}" --body-file "${bodyFile}"`;
58148
- const result = exec(cmd, {
58180
+ const create = (issueBody, paths) => {
58181
+ writeFileSync5(bodyFile, issueBody, "utf-8");
58182
+ return exec([
58183
+ "issue",
58184
+ "create",
58185
+ "--repo",
58186
+ repo,
58187
+ "--title",
58188
+ title,
58189
+ "--body-file",
58190
+ bodyFile,
58191
+ ...paths.flatMap((path, index) => ["--attach", `${path}#Screenshot ${index + 1}`])
58192
+ ], {
58149
58193
  cwd: workingDir,
58150
58194
  encoding: "utf-8",
58151
- timeout: 30000,
58195
+ timeout: paths.length > 0 ? 120000 : 30000,
58152
58196
  stdio: ["pipe", "pipe", "pipe"]
58153
- });
58154
- return result.trim();
58197
+ }).trim();
58198
+ };
58199
+ try {
58200
+ try {
58201
+ return { url: create(body, attachments), attachmentsIncomplete: false };
58202
+ } catch (err) {
58203
+ if (attachments.length === 0)
58204
+ throw err;
58205
+ const failure = err;
58206
+ const url = String(failure.stdout ?? "").match(/https:\/\/\S+\/issues\/\d+/)?.[0];
58207
+ if (url)
58208
+ return { url, attachmentsIncomplete: true };
58209
+ if (failure.code === "ETIMEDOUT")
58210
+ throw err;
58211
+ return {
58212
+ url: create(withOmittedScreenshots(body, attachments.length), []),
58213
+ attachmentsIncomplete: true
58214
+ };
58215
+ }
58155
58216
  } finally {
58156
58217
  try {
58157
58218
  unlinkSync3(bodyFile);
58158
58219
  } catch {}
58159
58220
  }
58160
58221
  }
58161
- function formatBugPreview(title, description, context, imageUrls, imageErrors, formatter) {
58222
+ function formatBugPreview(title, description, context, target, screenshotCount, skippedScreenshots, formatter) {
58162
58223
  const lines = [];
58163
58224
  lines.push(`${formatter.formatBold("Bug Report Preview")}`);
58164
58225
  lines.push("");
@@ -58185,14 +58246,20 @@ function formatBugPreview(title, description, context, imageUrls, imageErrors, f
58185
58246
  lines.push(formatter.formatListItem(`${event.type}: ${event.summary.substring(0, 40)}...`));
58186
58247
  }
58187
58248
  }
58188
- if (imageUrls.length > 0 || imageErrors.length > 0) {
58249
+ lines.push("");
58250
+ lines.push(formatter.formatBold("Destination:"));
58251
+ lines.push(formatter.formatListItem(`Issue on ${formatter.formatCode(target.repo)}${target.isPublic ? " (public: anyone can read it)" : ""}`));
58252
+ if (screenshotCount > 0 || skippedScreenshots.length > 0) {
58189
58253
  lines.push("");
58190
58254
  lines.push(formatter.formatBold("Screenshots:"));
58191
- if (imageUrls.length > 0) {
58192
- lines.push(formatter.formatListItem(`✅ ${imageUrls.length} image(s) uploaded successfully`));
58255
+ if (screenshotCount > 0) {
58256
+ lines.push(formatter.formatListItem(`\uD83D\uDCCE ${screenshotCount} image(s) will be attached when you approve`));
58257
+ if (target.isPublic) {
58258
+ lines.push(formatter.formatListItem("⚠️ Attachments on a public repository stay online permanently, even if the issue is deleted. Check them for personal data first."));
58259
+ }
58193
58260
  }
58194
- if (imageErrors.length > 0) {
58195
- lines.push(formatter.formatListItem(`⚠️ ${imageErrors.length} image(s) failed: ${imageErrors[0]}`));
58261
+ if (skippedScreenshots.length > 0) {
58262
+ lines.push(formatter.formatListItem(`⚠️ ${skippedScreenshots.length} image(s) not attached (${skippedScreenshots[0]}). Add them to the issue by hand if needed.`));
58196
58263
  }
58197
58264
  }
58198
58265
  lines.push("");
@@ -67420,7 +67487,7 @@ async function deferUpdate(session, username, updateManager) {
67420
67487
  await post(session, "success", `⏸️ ${formatter.formatBold("Update deferred")} for 1 hour
67421
67488
  ` + formatter.formatItalic("Use !update now to apply earlier"));
67422
67489
  }
67423
- async function reportBug(session, description, username, ctx, errorContext, attachedFiles) {
67490
+ async function reportBug(session, description, username, ctx, errorContext, attachedFiles, exec) {
67424
67491
  const formatter = session.platform.getFormatter();
67425
67492
  if (!ctx.config.bugReportsEnabled) {
67426
67493
  await post(session, "info", `${formatter.formatBold("Bug reporting is disabled")} by this server's configuration ` + `(${formatter.formatCode("bugReports: false")}).
@@ -67435,25 +67502,18 @@ async function reportBug(session, description, username, ctx, errorContext, atta
67435
67502
  ` + `Or react with \uD83D\uDC1B on any error message to report it.`);
67436
67503
  return;
67437
67504
  }
67438
- const ghStatus = checkGitHubCli();
67505
+ const ghStatus = checkGitHubCli(exec);
67439
67506
  if (!ghStatus.installed || !ghStatus.authenticated) {
67440
67507
  await postError(session, ghStatus.error || "GitHub CLI not configured");
67441
67508
  return;
67442
67509
  }
67443
67510
  const bugDescription = description || (errorContext ? `Error: ${errorContext.message.substring(0, 200)}` : "Unknown error");
67444
67511
  const context = await collectBugReportContext(session, errorContext);
67445
- let imageUrls = [];
67446
- let imageErrors = [];
67447
- const downloadFile = session.platform.downloadFile?.bind(session.platform);
67448
- if (attachedFiles && attachedFiles.length > 0 && downloadFile) {
67449
- await post(session, "info", `\uD83D\uDCE4 Uploading ${attachedFiles.length} image(s)...`);
67450
- const uploadResults = await uploadImages(attachedFiles, downloadFile);
67451
- imageUrls = uploadResults.filter((r) => r.success && typeof r.url === "string").map((r) => r.url);
67452
- imageErrors = uploadResults.filter((r) => !r.success).map((r) => `${r.originalFile.name}: ${r.error}`);
67453
- }
67512
+ const target = inspectBugReportTarget(ctx.config.bugReportRepo, exec);
67513
+ const { screenshots, skipped } = selectScreenshots(attachedFiles ?? [], target);
67454
67514
  const title = generateIssueTitle(bugDescription);
67455
- const body = formatIssueBody(context, bugDescription, imageUrls);
67456
- const preview = formatBugPreview(title, bugDescription, context, imageUrls, imageErrors, formatter);
67515
+ const body = formatIssueBody(context, bugDescription, skipped.length);
67516
+ const preview = formatBugPreview(title, bugDescription, context, target, screenshots.length, skipped, formatter);
67457
67517
  const previewMessage = `\uD83D\uDC1B ${preview}`;
67458
67518
  const bugReportPost = await postInteractiveAndRegister(session, previewMessage, [APPROVAL_EMOJIS[0], DENIAL_EMOJIS[0]], ctx.ops.registerPost);
67459
67519
  session.messageManager?.setPendingBugReport({
@@ -67461,8 +67521,7 @@ async function reportBug(session, description, username, ctx, errorContext, atta
67461
67521
  title,
67462
67522
  body,
67463
67523
  userDescription: bugDescription,
67464
- imageUrls,
67465
- imageErrors,
67524
+ screenshots,
67466
67525
  errorContext
67467
67526
  });
67468
67527
  sessionLog8(session).info(`\uD83D\uDC1B Bug report preview created by @${username}: ${title}`);
@@ -67476,14 +67535,19 @@ async function handleBugReportApproval(session, isApproved, username, ctx, exec)
67476
67535
  }
67477
67536
  const formatter = session.platform.getFormatter();
67478
67537
  if (isApproved) {
67538
+ const saved = await saveScreenshots(pending.screenshots ?? [], session.platform.downloadFile?.bind(session.platform));
67479
67539
  try {
67480
- const issueUrl = await createGitHubIssue(pending.title, pending.body, session.workingDir, exec);
67481
- await updatePostSuccess(session, pending.postId, `${formatter.formatBold("Bug report submitted")}: ${issueUrl}`);
67482
- sessionLog8(session).info(`\uD83D\uDC1B Bug report created by @${username}: ${issueUrl}`);
67540
+ const issue = await createGitHubIssue(pending.title, withOmittedScreenshots(pending.body, saved.failed), session.workingDir, { repo: ctx.config.bugReportRepo, attachments: saved.paths, exec });
67541
+ const incomplete = issue.attachmentsIncomplete || saved.failed > 0;
67542
+ await updatePostSuccess(session, pending.postId, `${formatter.formatBold("Bug report submitted")}: ${issue.url}` + (incomplete ? `
67543
+ ⚠️ Not every screenshot could be attached. Add the missing ones to the issue by hand.` : ""));
67544
+ sessionLog8(session).info(`\uD83D\uDC1B Bug report created by @${username}: ${issue.url}`);
67483
67545
  } catch (err) {
67484
67546
  const errorMessage = err instanceof Error ? err.message : String(err);
67485
67547
  await updatePostError(session, pending.postId, `${formatter.formatBold("Failed to create bug report")}: ${errorMessage}`);
67486
67548
  sessionLog8(session).error(`Failed to create bug report: ${errorMessage}`);
67549
+ } finally {
67550
+ saved.cleanup();
67487
67551
  }
67488
67552
  } else {
67489
67553
  await updatePostCancelled(session, pending.postId, `${formatter.formatBold("Bug report cancelled")} by ${formatter.formatUserMention(username)}`);
@@ -73915,6 +73979,7 @@ class SessionManager extends EventEmitter4 {
73915
73979
  userAttribution;
73916
73980
  threadLogsEnabled;
73917
73981
  bugReportsEnabled;
73982
+ bugReportRepo;
73918
73983
  usageShowEmails;
73919
73984
  threadLogsRetentionDays;
73920
73985
  limits;
@@ -73944,7 +74009,7 @@ class SessionManager extends EventEmitter4 {
73944
74009
  connectorsOff = null;
73945
74010
  usageRefreshInFlight = null;
73946
74011
  usageRefreshedAt = 0;
73947
- constructor(workingDir, permissionModeOrSkipFlag = "default", chromeEnabled = false, worktreeMode = "prompt", sessionsPath, threadLogsEnabled = true, threadLogsRetentionDays = 30, limits, claudeAccounts, respondOnlyWhenMentioned = false, userAttribution = true, bugReportsEnabled = true, usage) {
74012
+ constructor(workingDir, permissionModeOrSkipFlag = "default", chromeEnabled = false, worktreeMode = "prompt", sessionsPath, threadLogsEnabled = true, threadLogsRetentionDays = 30, limits, claudeAccounts, respondOnlyWhenMentioned = false, userAttribution = true, bugReportsEnabled = true, usage, bugReportRepo = DEFAULT_BUG_REPORT_REPO) {
73948
74013
  super();
73949
74014
  this.workingDir = workingDir;
73950
74015
  this.permissionMode = typeof permissionModeOrSkipFlag === "boolean" ? permissionModeOrSkipFlag ? "bypass" : "default" : permissionModeOrSkipFlag;
@@ -73953,6 +74018,7 @@ class SessionManager extends EventEmitter4 {
73953
74018
  this.respondOnlyWhenMentioned = respondOnlyWhenMentioned;
73954
74019
  this.userAttribution = userAttribution;
73955
74020
  this.bugReportsEnabled = bugReportsEnabled;
74021
+ this.bugReportRepo = bugReportRepo;
73956
74022
  this.threadLogsEnabled = threadLogsEnabled;
73957
74023
  this.threadLogsRetentionDays = threadLogsRetentionDays;
73958
74024
  this.limits = resolveLimits(limits);
@@ -74097,7 +74163,8 @@ class SessionManager extends EventEmitter4 {
74097
74163
  threadLogsRetentionDays: this.threadLogsRetentionDays,
74098
74164
  permissionTimeoutMs: this.limits.permissionTimeoutSeconds * 1000,
74099
74165
  flushDelayMs: this.limits.flushDelayMs,
74100
- bugReportsEnabled: this.bugReportsEnabled
74166
+ bugReportsEnabled: this.bugReportsEnabled,
74167
+ bugReportRepo: this.bugReportRepo
74101
74168
  };
74102
74169
  const state = {
74103
74170
  sessions: this.registry.getSessions(),
@@ -77817,12 +77884,12 @@ function wrapAnsi2(string, columns, options) {
77817
77884
 
77818
77885
  // node_modules/terminal-size/index.js
77819
77886
  import process12 from "node:process";
77820
- import { execFileSync } from "node:child_process";
77887
+ import { execFileSync as execFileSync2 } from "node:child_process";
77821
77888
  import fs6 from "node:fs";
77822
77889
  import tty2 from "node:tty";
77823
77890
  var defaultColumns = 80;
77824
77891
  var defaultRows = 24;
77825
- var exec5 = (command, arguments_, { shell, env } = {}) => execFileSync(command, arguments_, {
77892
+ var exec5 = (command, arguments_, { shell, env } = {}) => execFileSync2(command, arguments_, {
77826
77893
  encoding: "utf8",
77827
77894
  stdio: ["ignore", "pipe", "ignore"],
77828
77895
  timeout: 500,
@@ -91137,7 +91204,7 @@ async function startWithoutDaemon() {
91137
91204
  const threadLogsRetentionDays = config.threadLogs?.retentionDays ?? 30;
91138
91205
  const bugReportsEnabled = resolveBugReportsEnabled(config.bugReports);
91139
91206
  configureBugReports(bugReportsEnabled);
91140
- const session = new SessionManager(workingDir, initialPermissionMode, config.chrome, config.worktreeMode, undefined, threadLogsEnabled, threadLogsRetentionDays, config.limits, config.claudeAccounts, config.respondOnlyWhenMentioned, config.userAttribution, bugReportsEnabled, config.usage);
91207
+ const session = new SessionManager(workingDir, initialPermissionMode, config.chrome, config.worktreeMode, undefined, threadLogsEnabled, threadLogsRetentionDays, config.limits, config.claudeAccounts, config.respondOnlyWhenMentioned, config.userAttribution, bugReportsEnabled, config.usage, resolveBugReportRepo(config.bugReports));
91141
91208
  if (config.stickyMessage) {
91142
91209
  session.setStickyMessageCustomization(config.stickyMessage.description, config.stickyMessage.footer);
91143
91210
  }
@@ -38835,6 +38835,7 @@ class Redactor {
38835
38835
  }
38836
38836
 
38837
38837
  // src/operations/bug-report/handler.ts
38838
+ var import_semver2 = __toESM(require_semver2(), 1);
38838
38839
  init_version_check();
38839
38840
 
38840
38841
  // src/persistence/thread-logger.ts
@@ -38845,6 +38846,7 @@ var LOGS_BASE_DIR = join4(stateHome(), ".claude-threads", "logs");
38845
38846
 
38846
38847
  // src/operations/bug-report/handler.ts
38847
38848
  var piiRedactor = new Redactor({ aggressive: true });
38849
+ var MAX_SCREENSHOT_BYTES = 10 * 1024 * 1024;
38848
38850
  // src/mcp/decision-bridge.ts
38849
38851
  import { createServer, createConnection } from "node:net";
38850
38852
  var MAX_BRIDGE_REQUEST_BYTES = 1024 * 1024;
@@ -39812,7 +39814,7 @@ var CHAT_PLATFORM_PROMPT = generateChatPlatformPrompt();
39812
39814
  init_cli();
39813
39815
 
39814
39816
  // src/update-notifier.ts
39815
- var import_semver2 = __toESM(require_semver2(), 1);
39817
+ var import_semver3 = __toESM(require_semver2(), 1);
39816
39818
 
39817
39819
  // src/operations/commands/handler.ts
39818
39820
  init_emoji();
@@ -54,7 +54,7 @@ platforms:
54
54
  | `respondOnlyWhenMentioned` | Start new threads in quiet mode, where the bot only replies to messages that @mention it. Users can still toggle per-thread with `!mentions`. | `false` |
55
55
  | `userAttribution` | Prefix each user turn sent to Claude with the sender's `[@username]:` so Claude can tell who is speaking in multi-user threads. Only applied once a thread has more than one participant (after `!invite`); solo threads are left untouched. Set `false` to disable. Applies to new sessions. | `true` |
56
56
  | `keepAlive` | Prevent system sleep while sessions are active | `true` |
57
- | `bugReports` | Whether `!bug` may file a report. ⚠️ A bug report leaves your infrastructure: attached screenshots are uploaded to a public anonymous file host, and the report body — session context plus recent daemon log lines — is filed as an issue on the project's public GitHub repository, behind best-effort redaction. Claude can also invoke `!bug` itself. Set `false` and the whole path is removed: the command is refused, Claude cannot invoke it either, the 🐛 error reaction does nothing, the approval that would file the issue is refused, and `!bug` disappears from `!help`. **Fails closed** — a malformed value disables it rather than defaulting to on. | `true` |
57
+ | `bugReports` | Whether `!bug` may file a report, and where. ⚠️ A bug report leaves your infrastructure: the report body — session context plus recent daemon log lines — is filed as an issue on the project's public GitHub repository, behind best-effort redaction, unless you set `bugReports: { repo: owner/name }` to file in your own repository. Attached screenshots go along with the issue once someone approves the preview; this needs `gh` 2.99 or newer and write access to the target repository, and without those the report is filed without them. An attachment on a public repository stays online permanently, even if the issue is deleted. Claude can also invoke `!bug` itself, but a person still has to approve the preview. Set `false` and the whole path is removed: the command is refused, Claude cannot invoke it either, the 🐛 error reaction does nothing, the approval that would file the issue is refused, and `!bug` disappears from `!help`. **Fails closed** — a malformed value disables it rather than defaulting to on. | `true` |
58
58
  | `limits` | Resource limits and timeouts (see below) | see below |
59
59
  | `threadLogs` | Thread logging (see below) | enabled |
60
60
  | `stickyMessage` | Sticky message text customization (see below) | none |
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "claude-threads",
3
- "version": "1.40.0",
3
+ "version": "1.40.1",
4
4
  "description": "Run Claude Code from Slack or Mattermost. Sessions stream live into threads where your whole team can watch and steer.",
5
5
  "main": "dist/index.js",
6
6
  "type": "module",