claude-threads 1.40.0 → 1.40.2

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,26 @@ 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.2] - 2026-10-30
9
+
10
+ ### Changed
11
+ - `hono` 4.13.14, plus dev tooling (#655, #656).
12
+
13
+ ### Fixed
14
+ - **Auto-update respects `engines`.** The updater now skips a release this host's Node or Bun cannot run, logs why, and `!update` says so instead of reporting "Up to date". npm only warns about `engines`, so an update used to install anyway. A host that is held back gets no further releases until its runtime is upgraded (#659).
15
+
16
+ ## [1.40.1] - 2026-10-30
17
+
18
+ ### Added
19
+ - **`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.
20
+
21
+ ### Changed
22
+ - **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.
23
+ - **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.
24
+
25
+ ### Security
26
+ - **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.
27
+
8
28
  ## [1.40.0] - 2026-10-30
9
29
 
10
30
  ### 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("");
@@ -67363,6 +67430,11 @@ async function showUpdateStatus(session, updateManager, ctx) {
67363
67430
  }
67364
67431
  const updateInfo = await updateManager.checkNow();
67365
67432
  if (!updateInfo || !updateInfo.available) {
67433
+ const blocked = updateManager.getBlockedUpdate();
67434
+ if (blocked) {
67435
+ await post(session, "warning", `${formatter.formatBold(`v${blocked.latestVersion} is available but not installable`)}: ${blocked.reason}`);
67436
+ return;
67437
+ }
67366
67438
  await post(session, "success", `${formatter.formatBold("Up to date")} - no updates available`);
67367
67439
  return;
67368
67440
  }
@@ -67396,7 +67468,8 @@ async function forceUpdateNow(session, username, updateManager) {
67396
67468
  }
67397
67469
  const updateInfo = await updateManager.checkNow();
67398
67470
  if (!updateInfo || !updateInfo.available) {
67399
- await post(session, "info", `No update available to install`);
67471
+ const blocked = updateManager.getBlockedUpdate();
67472
+ await post(session, "info", blocked ? `Not installing v${blocked.latestVersion}: ${blocked.reason}` : `No update available to install`);
67400
67473
  return;
67401
67474
  }
67402
67475
  await post(session, "info", `\uD83D\uDD04 ${formatter.formatBold("Forcing update")} to v${updateInfo.latestVersion} - restarting shortly...
@@ -67420,7 +67493,7 @@ async function deferUpdate(session, username, updateManager) {
67420
67493
  await post(session, "success", `⏸️ ${formatter.formatBold("Update deferred")} for 1 hour
67421
67494
  ` + formatter.formatItalic("Use !update now to apply earlier"));
67422
67495
  }
67423
- async function reportBug(session, description, username, ctx, errorContext, attachedFiles) {
67496
+ async function reportBug(session, description, username, ctx, errorContext, attachedFiles, exec) {
67424
67497
  const formatter = session.platform.getFormatter();
67425
67498
  if (!ctx.config.bugReportsEnabled) {
67426
67499
  await post(session, "info", `${formatter.formatBold("Bug reporting is disabled")} by this server's configuration ` + `(${formatter.formatCode("bugReports: false")}).
@@ -67435,25 +67508,18 @@ async function reportBug(session, description, username, ctx, errorContext, atta
67435
67508
  ` + `Or react with \uD83D\uDC1B on any error message to report it.`);
67436
67509
  return;
67437
67510
  }
67438
- const ghStatus = checkGitHubCli();
67511
+ const ghStatus = checkGitHubCli(exec);
67439
67512
  if (!ghStatus.installed || !ghStatus.authenticated) {
67440
67513
  await postError(session, ghStatus.error || "GitHub CLI not configured");
67441
67514
  return;
67442
67515
  }
67443
67516
  const bugDescription = description || (errorContext ? `Error: ${errorContext.message.substring(0, 200)}` : "Unknown error");
67444
67517
  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
- }
67518
+ const target = inspectBugReportTarget(ctx.config.bugReportRepo, exec);
67519
+ const { screenshots, skipped } = selectScreenshots(attachedFiles ?? [], target);
67454
67520
  const title = generateIssueTitle(bugDescription);
67455
- const body = formatIssueBody(context, bugDescription, imageUrls);
67456
- const preview = formatBugPreview(title, bugDescription, context, imageUrls, imageErrors, formatter);
67521
+ const body = formatIssueBody(context, bugDescription, skipped.length);
67522
+ const preview = formatBugPreview(title, bugDescription, context, target, screenshots.length, skipped, formatter);
67457
67523
  const previewMessage = `\uD83D\uDC1B ${preview}`;
67458
67524
  const bugReportPost = await postInteractiveAndRegister(session, previewMessage, [APPROVAL_EMOJIS[0], DENIAL_EMOJIS[0]], ctx.ops.registerPost);
67459
67525
  session.messageManager?.setPendingBugReport({
@@ -67461,8 +67527,7 @@ async function reportBug(session, description, username, ctx, errorContext, atta
67461
67527
  title,
67462
67528
  body,
67463
67529
  userDescription: bugDescription,
67464
- imageUrls,
67465
- imageErrors,
67530
+ screenshots,
67466
67531
  errorContext
67467
67532
  });
67468
67533
  sessionLog8(session).info(`\uD83D\uDC1B Bug report preview created by @${username}: ${title}`);
@@ -67476,14 +67541,19 @@ async function handleBugReportApproval(session, isApproved, username, ctx, exec)
67476
67541
  }
67477
67542
  const formatter = session.platform.getFormatter();
67478
67543
  if (isApproved) {
67544
+ const saved = await saveScreenshots(pending.screenshots ?? [], session.platform.downloadFile?.bind(session.platform));
67479
67545
  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}`);
67546
+ const issue = await createGitHubIssue(pending.title, withOmittedScreenshots(pending.body, saved.failed), session.workingDir, { repo: ctx.config.bugReportRepo, attachments: saved.paths, exec });
67547
+ const incomplete = issue.attachmentsIncomplete || saved.failed > 0;
67548
+ await updatePostSuccess(session, pending.postId, `${formatter.formatBold("Bug report submitted")}: ${issue.url}` + (incomplete ? `
67549
+ ⚠️ Not every screenshot could be attached. Add the missing ones to the issue by hand.` : ""));
67550
+ sessionLog8(session).info(`\uD83D\uDC1B Bug report created by @${username}: ${issue.url}`);
67483
67551
  } catch (err) {
67484
67552
  const errorMessage = err instanceof Error ? err.message : String(err);
67485
67553
  await updatePostError(session, pending.postId, `${formatter.formatBold("Failed to create bug report")}: ${errorMessage}`);
67486
67554
  sessionLog8(session).error(`Failed to create bug report: ${errorMessage}`);
67555
+ } finally {
67556
+ saved.cleanup();
67487
67557
  }
67488
67558
  } else {
67489
67559
  await updatePostCancelled(session, pending.postId, `${formatter.formatBold("Bug report cancelled")} by ${formatter.formatUserMention(username)}`);
@@ -73915,6 +73985,7 @@ class SessionManager extends EventEmitter4 {
73915
73985
  userAttribution;
73916
73986
  threadLogsEnabled;
73917
73987
  bugReportsEnabled;
73988
+ bugReportRepo;
73918
73989
  usageShowEmails;
73919
73990
  threadLogsRetentionDays;
73920
73991
  limits;
@@ -73944,7 +74015,7 @@ class SessionManager extends EventEmitter4 {
73944
74015
  connectorsOff = null;
73945
74016
  usageRefreshInFlight = null;
73946
74017
  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) {
74018
+ 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
74019
  super();
73949
74020
  this.workingDir = workingDir;
73950
74021
  this.permissionMode = typeof permissionModeOrSkipFlag === "boolean" ? permissionModeOrSkipFlag ? "bypass" : "default" : permissionModeOrSkipFlag;
@@ -73953,6 +74024,7 @@ class SessionManager extends EventEmitter4 {
73953
74024
  this.respondOnlyWhenMentioned = respondOnlyWhenMentioned;
73954
74025
  this.userAttribution = userAttribution;
73955
74026
  this.bugReportsEnabled = bugReportsEnabled;
74027
+ this.bugReportRepo = bugReportRepo;
73956
74028
  this.threadLogsEnabled = threadLogsEnabled;
73957
74029
  this.threadLogsRetentionDays = threadLogsRetentionDays;
73958
74030
  this.limits = resolveLimits(limits);
@@ -74097,7 +74169,8 @@ class SessionManager extends EventEmitter4 {
74097
74169
  threadLogsRetentionDays: this.threadLogsRetentionDays,
74098
74170
  permissionTimeoutMs: this.limits.permissionTimeoutSeconds * 1000,
74099
74171
  flushDelayMs: this.limits.flushDelayMs,
74100
- bugReportsEnabled: this.bugReportsEnabled
74172
+ bugReportsEnabled: this.bugReportsEnabled,
74173
+ bugReportRepo: this.bugReportRepo
74101
74174
  };
74102
74175
  const state = {
74103
74176
  sessions: this.registry.getSessions(),
@@ -74762,7 +74835,8 @@ class SessionManager extends EventEmitter4 {
74762
74835
  }
74763
74836
  const updateInfo = await this.autoUpdateManager.checkNow();
74764
74837
  if (!updateInfo || !updateInfo.available) {
74765
- await platform.createPost(`✅ ${formatter.formatBold("Up to date")} - no updates available`, threadId);
74838
+ const blocked = this.autoUpdateManager.getBlockedUpdate();
74839
+ await platform.createPost(blocked ? `⚠️ ${formatter.formatBold(`v${blocked.latestVersion} is available but not installable`)}: ${blocked.reason}` : `✅ ${formatter.formatBold("Up to date")} - no updates available`, threadId);
74766
74840
  return;
74767
74841
  }
74768
74842
  const scheduledAt = this.autoUpdateManager.getScheduledRestartAt();
@@ -77817,12 +77891,12 @@ function wrapAnsi2(string, columns, options) {
77817
77891
 
77818
77892
  // node_modules/terminal-size/index.js
77819
77893
  import process12 from "node:process";
77820
- import { execFileSync } from "node:child_process";
77894
+ import { execFileSync as execFileSync2 } from "node:child_process";
77821
77895
  import fs6 from "node:fs";
77822
77896
  import tty2 from "node:tty";
77823
77897
  var defaultColumns = 80;
77824
77898
  var defaultRows = 24;
77825
- var exec5 = (command, arguments_, { shell, env } = {}) => execFileSync(command, arguments_, {
77899
+ var exec5 = (command, arguments_, { shell, env } = {}) => execFileSync2(command, arguments_, {
77826
77900
  encoding: "utf8",
77827
77901
  stdio: ["ignore", "pipe", "ignore"],
77828
77902
  timeout: 500,
@@ -89734,6 +89808,7 @@ import { EventEmitter as EventEmitter9 } from "events";
89734
89808
 
89735
89809
  // src/auto-update/checker.ts
89736
89810
  init_logger();
89811
+ var import_semver5 = __toESM(require_semver2(), 1);
89737
89812
  import { EventEmitter as EventEmitter7 } from "events";
89738
89813
  var log54 = createLogger("checker");
89739
89814
  var PACKAGE_NAME = "claude-threads";
@@ -89750,7 +89825,16 @@ function compareVersions(a, b) {
89750
89825
  }
89751
89826
  return 0;
89752
89827
  }
89753
- async function fetchLatestVersion() {
89828
+ function unmetEngine(engines, versions = process.versions) {
89829
+ const [runtime, running] = versions.bun ? ["bun", versions.bun] : ["node", versions.node];
89830
+ const required = engines?.[runtime];
89831
+ if (!required || !import_semver5.validRange(required))
89832
+ return null;
89833
+ if (import_semver5.satisfies(running, required, { includePrerelease: true }))
89834
+ return null;
89835
+ return `requires ${runtime} ${required}, this host runs ${running}`;
89836
+ }
89837
+ async function fetchLatestRelease() {
89754
89838
  try {
89755
89839
  const response = await fetch(`https://registry.npmjs.org/${PACKAGE_NAME}/latest`, {
89756
89840
  headers: {
@@ -89762,7 +89846,7 @@ async function fetchLatestVersion() {
89762
89846
  return null;
89763
89847
  }
89764
89848
  const data = await response.json();
89765
- return data.version ?? null;
89849
+ return data.version ? { version: data.version, engines: data.engines } : null;
89766
89850
  } catch (err) {
89767
89851
  log54.warn(`Failed to fetch latest version: ${err}`);
89768
89852
  return null;
@@ -89774,6 +89858,7 @@ class UpdateChecker extends EventEmitter7 {
89774
89858
  checkInterval = null;
89775
89859
  lastCheck = null;
89776
89860
  lastUpdateInfo = null;
89861
+ blockedUpdate = null;
89777
89862
  isChecking = false;
89778
89863
  constructor(config) {
89779
89864
  super();
@@ -89813,14 +89898,25 @@ class UpdateChecker extends EventEmitter7 {
89813
89898
  this.emit("check:start");
89814
89899
  try {
89815
89900
  log54.debug("Checking for updates...");
89816
- const latestVersion = await fetchLatestVersion();
89817
- if (!latestVersion) {
89901
+ const latest = await fetchLatestRelease();
89902
+ if (!latest) {
89818
89903
  this.emit("check:complete", false);
89819
89904
  return null;
89820
89905
  }
89821
89906
  this.lastCheck = new Date;
89907
+ const latestVersion = latest.version;
89822
89908
  const currentVersion = VERSION;
89823
89909
  const hasUpdate = compareVersions(latestVersion, currentVersion) > 0;
89910
+ const blockedReason = hasUpdate ? unmetEngine(latest.engines) : null;
89911
+ if (blockedReason) {
89912
+ if (this.blockedUpdate?.latestVersion !== latestVersion) {
89913
+ log54.warn(`⚠️ Not updating to v${latestVersion}: ${blockedReason}`);
89914
+ }
89915
+ this.blockedUpdate = { latestVersion, reason: blockedReason };
89916
+ this.emit("check:complete", false);
89917
+ return null;
89918
+ }
89919
+ this.blockedUpdate = null;
89824
89920
  if (hasUpdate) {
89825
89921
  const updateInfo = {
89826
89922
  available: true,
@@ -89850,6 +89946,9 @@ class UpdateChecker extends EventEmitter7 {
89850
89946
  getLastUpdateInfo() {
89851
89947
  return this.lastUpdateInfo;
89852
89948
  }
89949
+ getBlockedUpdate() {
89950
+ return this.blockedUpdate;
89951
+ }
89853
89952
  getLastCheckTime() {
89854
89953
  return this.lastCheck;
89855
89954
  }
@@ -90557,6 +90656,9 @@ class AutoUpdateManager extends EventEmitter9 {
90557
90656
  getUpdateInfo() {
90558
90657
  return this.state.updateInfo;
90559
90658
  }
90659
+ getBlockedUpdate() {
90660
+ return this.checker.getBlockedUpdate();
90661
+ }
90560
90662
  getScheduledRestartAt() {
90561
90663
  return this.scheduler.getScheduledRestartAt();
90562
90664
  }
@@ -91137,7 +91239,7 @@ async function startWithoutDaemon() {
91137
91239
  const threadLogsRetentionDays = config.threadLogs?.retentionDays ?? 30;
91138
91240
  const bugReportsEnabled = resolveBugReportsEnabled(config.bugReports);
91139
91241
  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);
91242
+ 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
91243
  if (config.stickyMessage) {
91142
91244
  session.setStickyMessageCustomization(config.stickyMessage.description, config.stickyMessage.footer);
91143
91245
  }
@@ -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.2",
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",
@@ -78,7 +78,7 @@
78
78
  "cross-spawn": "^7.0.6",
79
79
  "diff": "^8.0.3",
80
80
  "express-rate-limit": "^8.7.1",
81
- "hono": "4.13.13",
81
+ "hono": "4.13.14",
82
82
  "ink": "^8.0.0",
83
83
  "js-yaml": "^4.3.1",
84
84
  "prompts": "^2.4.2",
@@ -101,7 +101,7 @@
101
101
  "@types/ws": "^8.18.2",
102
102
  "eslint": "^10.13.0",
103
103
  "husky": "^9.1.7",
104
- "knip": "6.41.0",
104
+ "knip": "6.43.0",
105
105
  "lint-staged": "^17.0.4",
106
106
  "prettier": "^3.9.10",
107
107
  "react-devtools-core": "^8.0.0",