claude-threads 1.6.2 → 1.6.3

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,12 @@ 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.6.3] - 2026-04-21
9
+
10
+ ### Fixed
11
+ - **Skipped file attachments are now surfaced on session start** - When a user starts a session with an unsupported file (e.g. `.xlsx`), the bot posts the `⚠️ Some files could not be processed` warning instead of silently dropping it. The same warning now fires on the mid-thread context-prompt and worktree paths. (#325, thanks @shaders)
12
+ - **Recognize `.har` and `.log` as text** - Both extensions come through as `application/octet-stream` via Mattermost/Slack and were previously dropped as unsupported. (#325)
13
+
8
14
  ## [1.6.2] - 2026-04-20
9
15
 
10
16
  ### Fixed
package/dist/index.js CHANGED
@@ -59420,6 +59420,8 @@ var TEXT_FILE_EXTENSIONS = [
59420
59420
  ".xml",
59421
59421
  ".yaml",
59422
59422
  ".yml",
59423
+ ".har",
59424
+ ".log",
59423
59425
  ".js",
59424
59426
  ".ts",
59425
59427
  ".jsx",
@@ -59990,7 +59992,7 @@ function getUnsupportedFileSuggestion(file) {
59990
59992
  async function buildMessageContent(text, platform, files, debug = false) {
59991
59993
  const result = await processFiles(platform, files, debug);
59992
59994
  if (result.blocks.length === 0) {
59993
- return text;
59995
+ return { content: text, skipped: result.skipped };
59994
59996
  }
59995
59997
  if (text) {
59996
59998
  result.blocks.push({
@@ -59998,7 +60000,12 @@ async function buildMessageContent(text, platform, files, debug = false) {
59998
60000
  text
59999
60001
  });
60000
60002
  }
60001
- return result.blocks;
60003
+ return { content: result.blocks, skipped: result.skipped };
60004
+ }
60005
+ async function postSkippedFilesFeedback(platform, threadId, skipped) {
60006
+ if (skipped.length === 0)
60007
+ return;
60008
+ await platform.createPost(formatSkippedFilesFeedback(skipped), threadId);
60002
60009
  }
60003
60010
  async function processFiles(platform, files, debug = false) {
60004
60011
  const blocks = [];
@@ -60052,6 +60059,18 @@ async function processFiles(platform, files, debug = false) {
60052
60059
  }
60053
60060
  return { blocks, skipped };
60054
60061
  }
60062
+ function formatSkippedFilesFeedback(skippedFiles) {
60063
+ const lines = ["⚠️ **Some files could not be processed:**"];
60064
+ for (const file of skippedFiles) {
60065
+ let line = `- **${file.name}**: ${file.reason}`;
60066
+ if (file.suggestion) {
60067
+ line += ` _(${file.suggestion})_`;
60068
+ }
60069
+ lines.push(line);
60070
+ }
60071
+ return lines.join(`
60072
+ `);
60073
+ }
60055
60074
  function startTyping(session) {
60056
60075
  if (session.timers.typingTimer)
60057
60076
  return;
@@ -60498,20 +60517,15 @@ class MessageManager {
60498
60517
  }
60499
60518
  this.session.threadLogger?.logUserMessage(username || this.session.startedBy, message, displayName, files && files.length > 0);
60500
60519
  await this.prepareForUserMessage();
60501
- let skippedFiles = [];
60502
- if (files && files.length > 0) {
60503
- const fileResult = await processFiles(this.platform, files);
60504
- skippedFiles = fileResult.skipped;
60505
- }
60506
60520
  let content = message;
60521
+ let skippedFiles = [];
60507
60522
  if (this.buildMessageContentCallback) {
60508
- content = await this.buildMessageContentCallback(message, this.platform, files);
60523
+ const built = await this.buildMessageContentCallback(message, this.platform, files);
60524
+ content = built.content;
60525
+ skippedFiles = built.skipped;
60509
60526
  }
60510
60527
  this.session.claude.sendMessage(content);
60511
- if (skippedFiles.length > 0) {
60512
- const feedback = this.formatSkippedFilesFeedback(skippedFiles);
60513
- await this.platform.createPost(feedback, this.threadId);
60514
- }
60528
+ await postSkippedFilesFeedback(this.platform, this.threadId, skippedFiles);
60515
60529
  this.session.lastActivityAt = new Date;
60516
60530
  this.session.isProcessing = true;
60517
60531
  this.emitSessionUpdateCallback?.({ status: "active", isTyping: true });
@@ -60519,18 +60533,6 @@ class MessageManager {
60519
60533
  logger.debug("User message sent to Claude");
60520
60534
  return true;
60521
60535
  }
60522
- formatSkippedFilesFeedback(skippedFiles) {
60523
- const lines = ["⚠️ **Some files could not be processed:**"];
60524
- for (const file of skippedFiles) {
60525
- let line = `- **${file.name}**: ${file.reason}`;
60526
- if (file.suggestion) {
60527
- line += ` _(${file.suggestion})_`;
60528
- }
60529
- lines.push(line);
60530
- }
60531
- return lines.join(`
60532
- `);
60533
- }
60534
60536
  getSession() {
60535
60537
  return this.session;
60536
60538
  }
@@ -66294,9 +66296,10 @@ ${fmt.formatItalic("Claude Code restarted in the new worktree")}`);
66294
66296
  const contextPrefix = options2.formatContextForClaude(threadMessages, workSummary);
66295
66297
  const messageToSend = contextPrefix + session.firstPrompt;
66296
66298
  session.messageCount++;
66297
- const content = await options2.buildMessageContent(messageToSend, session, undefined);
66299
+ const { content, skipped } = await options2.buildMessageContent(messageToSend, session, undefined);
66298
66300
  session.claude.sendMessage(content);
66299
66301
  options2.startTyping(session);
66302
+ await postSkippedFilesFeedback(session.platform, session.threadId, skipped);
66300
66303
  sessionLog3(session).debug(`\uD83C\uDF3F Auto-included ${threadMessages.length} messages + work summary for mid-session worktree`);
66301
66304
  }
66302
66305
  session.worktreeResponsePostId = undefined;
@@ -66899,11 +66902,12 @@ async function handleContextPromptTimeout(session, ctx) {
66899
66902
  }
66900
66903
  session.messageCount++;
66901
66904
  const messageToSend = ctx.injectMetadataReminder(queuedPrompt, session);
66902
- const content = await ctx.buildMessageContent(messageToSend, session, queuedFiles);
66905
+ const { content, skipped } = await ctx.buildMessageContent(messageToSend, session, queuedFiles);
66903
66906
  if (session.claude.isRunning()) {
66904
66907
  session.claude.sendMessage(content);
66905
66908
  ctx.startTyping(session);
66906
66909
  }
66910
+ await postSkippedFilesFeedback(session.platform, session.threadId, skipped);
66907
66911
  ctx.persistSession(session);
66908
66912
  sessionLog5(session).debug(`\uD83E\uDDF5 Context prompt timed out, continuing without thread context`);
66909
66913
  }
@@ -66920,11 +66924,12 @@ async function offerContextPrompt(session, queuedPrompt, queuedFiles, ctx, exclu
66920
66924
  sessionLog5(session).debug(`\uD83E\uDDF5 Including work summary (no thread messages)`);
66921
66925
  }
66922
66926
  messageToSend = ctx.injectMetadataReminder(messageToSend, session);
66923
- const content = await ctx.buildMessageContent(messageToSend, session, queuedFiles);
66927
+ const { content, skipped } = await ctx.buildMessageContent(messageToSend, session, queuedFiles);
66924
66928
  if (session.claude.isRunning()) {
66925
66929
  session.claude.sendMessage(content);
66926
66930
  ctx.startTyping(session);
66927
66931
  }
66932
+ await postSkippedFilesFeedback(session.platform, session.threadId, skipped);
66928
66933
  return false;
66929
66934
  }
66930
66935
  if (messageCount === 1) {
@@ -66938,11 +66943,12 @@ async function offerContextPrompt(session, queuedPrompt, queuedFiles, ctx, exclu
66938
66943
  }
66939
66944
  session.messageCount++;
66940
66945
  messageToSend = ctx.injectMetadataReminder(messageToSend, session);
66941
- const content = await ctx.buildMessageContent(messageToSend, session, queuedFiles);
66946
+ const { content, skipped } = await ctx.buildMessageContent(messageToSend, session, queuedFiles);
66942
66947
  if (session.claude.isRunning()) {
66943
66948
  session.claude.sendMessage(content);
66944
66949
  ctx.startTyping(session);
66945
66950
  }
66951
+ await postSkippedFilesFeedback(session.platform, session.threadId, skipped);
66946
66952
  sessionLog5(session).debug(`\uD83E\uDDF5 Auto-included 1 message as context (thread starter)${previousWorkSummary ? " + work summary" : ""}`);
66947
66953
  return false;
66948
66954
  }
@@ -67119,7 +67125,7 @@ function createMessageManager(session, ctx) {
67119
67125
  }
67120
67126
  session.messageCount++;
67121
67127
  messageToSend = maybeInjectMetadataReminder(messageToSend, session, ctx, session);
67122
- const content = await ctx.ops.buildMessageContent(messageToSend, session.platform, undefined);
67128
+ const { content } = await ctx.ops.buildMessageContent(messageToSend, session.platform, undefined);
67123
67129
  if (session.claude.isRunning()) {
67124
67130
  session.claude.sendMessage(content);
67125
67131
  ctx.ops.startTyping(session);
@@ -67425,17 +67431,19 @@ ${CHAT_PLATFORM_PROMPT}`;
67425
67431
  await ctx.ops.updateStickyMessage();
67426
67432
  return;
67427
67433
  }
67428
- const content = await ctx.ops.buildMessageContent(options2.prompt, session.platform, options2.files);
67434
+ const { content, skipped } = await ctx.ops.buildMessageContent(options2.prompt, session.platform, options2.files);
67429
67435
  const messageText = typeof content === "string" ? content : options2.prompt;
67430
67436
  if (replyToPostId) {
67431
67437
  const excludePostId = triggeringPostId || replyToPostId;
67432
67438
  const contextOffered = await ctx.ops.offerContextPrompt(session, messageText, options2.files, excludePostId);
67433
67439
  if (contextOffered) {
67440
+ await postSkippedFilesFeedback(session.platform, actualThreadId, skipped);
67434
67441
  return;
67435
67442
  }
67436
67443
  }
67437
67444
  session.messageCount++;
67438
67445
  claude.sendMessage(content);
67446
+ await postSkippedFilesFeedback(session.platform, actualThreadId, skipped);
67439
67447
  }
67440
67448
  async function resumeSession(state, ctx) {
67441
67449
  if (!state.threadId || !state.platformId || !state.claudeSessionId || !state.workingDir) {
@@ -67620,9 +67628,7 @@ async function sendFollowUp(session, message, files, ctx, username, displayName)
67620
67628
  if (session.needsContextPromptOnNextMessage) {
67621
67629
  session.needsContextPromptOnNextMessage = false;
67622
67630
  await session.messageManager?.prepareForUserMessage();
67623
- const content = await ctx.ops.buildMessageContent(message, session.platform, files);
67624
- const messageText = typeof content === "string" ? content : message;
67625
- const contextOffered = await ctx.ops.offerContextPrompt(session, messageText, files);
67631
+ const contextOffered = await ctx.ops.offerContextPrompt(session, message, files);
67626
67632
  if (contextOffered) {
67627
67633
  session.lastActivityAt = new Date;
67628
67634
  return;
@@ -15849,7 +15849,7 @@ var require_validation3 = __commonJS((exports, module) => {
15849
15849
 
15850
15850
  // node_modules/ws/lib/receiver.js
15851
15851
  var require_receiver = __commonJS((exports, module) => {
15852
- var { Writable: Writable2 } = __require("stream");
15852
+ var { Writable } = __require("stream");
15853
15853
  var PerMessageDeflate = require_permessage_deflate();
15854
15854
  var {
15855
15855
  BINARY_TYPES,
@@ -15868,7 +15868,7 @@ var require_receiver = __commonJS((exports, module) => {
15868
15868
  var INFLATING = 5;
15869
15869
  var DEFER_EVENT = 6;
15870
15870
 
15871
- class Receiver extends Writable2 {
15871
+ class Receiver extends Writable {
15872
15872
  constructor(options2 = {}) {
15873
15873
  super();
15874
15874
  this._allowSynchronousEvents = options2.allowSynchronousEvents !== undefined ? options2.allowSynchronousEvents : true;
@@ -16906,7 +16906,7 @@ var require_websocket = __commonJS((exports, module) => {
16906
16906
  var net = __require("net");
16907
16907
  var tls = __require("tls");
16908
16908
  var { randomBytes, createHash } = __require("crypto");
16909
- var { Duplex, Readable: Readable2 } = __require("stream");
16909
+ var { Duplex, Readable } = __require("stream");
16910
16910
  var { URL: URL2 } = __require("url");
16911
16911
  var PerMessageDeflate = require_permessage_deflate();
16912
16912
  var Receiver = require_receiver();
@@ -52407,661 +52407,28 @@ function createMessageManagerEvents() {
52407
52407
 
52408
52408
  // src/operations/streaming/handler.ts
52409
52409
  var import_yauzl = __toESM(require_yauzl(), 1);
52410
- import { createGunzip } from "zlib";
52411
- import { pipeline as pipeline2 } from "stream/promises";
52412
- import { Readable, Writable } from "stream";
52413
52410
  var log2 = createLogger("streaming");
52414
52411
  var MAX_PDF_SIZE = 32 * 1024 * 1024;
52415
52412
  var MAX_TEXT_FILE_SIZE = 1 * 1024 * 1024;
52416
52413
  var MAX_DECOMPRESSED_SIZE = 10 * 1024 * 1024;
52417
52414
  var MAX_ZIP_SIZE = 50 * 1024 * 1024;
52418
52415
  var MAX_GZIP_SIZE = 50 * 1024 * 1024;
52419
- var MAX_ZIP_FILES = 20;
52420
- var SUPPORTED_IMAGE_TYPES = [
52421
- "image/jpeg",
52422
- "image/png",
52423
- "image/gif",
52424
- "image/webp"
52425
- ];
52426
- var SUPPORTED_TEXT_TYPES = [
52427
- "text/plain",
52428
- "text/markdown",
52429
- "text/csv",
52430
- "text/xml",
52431
- "text/yaml",
52432
- "text/x-yaml",
52433
- "application/json",
52434
- "application/xml",
52435
- "application/x-yaml",
52436
- "application/yaml"
52437
- ];
52438
- var TEXT_FILE_EXTENSIONS = [
52439
- ".txt",
52440
- ".md",
52441
- ".markdown",
52442
- ".json",
52443
- ".csv",
52444
- ".xml",
52445
- ".yaml",
52446
- ".yml",
52447
- ".js",
52448
- ".ts",
52449
- ".jsx",
52450
- ".tsx",
52451
- ".py",
52452
- ".rb",
52453
- ".go",
52454
- ".rs",
52455
- ".java",
52456
- ".c",
52457
- ".cpp",
52458
- ".h",
52459
- ".hpp",
52460
- ".cs",
52461
- ".php",
52462
- ".swift",
52463
- ".kt",
52464
- ".scala",
52465
- ".sh",
52466
- ".bash",
52467
- ".zsh",
52468
- ".fish",
52469
- ".ps1",
52470
- ".bat",
52471
- ".cmd",
52472
- ".html",
52473
- ".htm",
52474
- ".css",
52475
- ".scss",
52476
- ".sass",
52477
- ".less",
52478
- ".sql",
52479
- ".graphql",
52480
- ".gql",
52481
- ".toml",
52482
- ".ini",
52483
- ".cfg",
52484
- ".conf",
52485
- ".env",
52486
- ".properties",
52487
- ".dockerfile",
52488
- ".gitignore",
52489
- ".gitattributes",
52490
- ".editorconfig"
52491
- ];
52492
- function isImageFile(file2) {
52493
- return file2.mimeType.startsWith("image/") && SUPPORTED_IMAGE_TYPES.includes(file2.mimeType);
52494
- }
52495
- function isPdfFile(file2) {
52496
- return file2.mimeType === "application/pdf" || file2.name.toLowerCase().endsWith(".pdf");
52497
- }
52498
- function isTextFile(file2) {
52499
- if (SUPPORTED_TEXT_TYPES.includes(file2.mimeType)) {
52500
- return true;
52501
- }
52502
- const lowerName = file2.name.toLowerCase();
52503
- return TEXT_FILE_EXTENSIONS.some((ext) => lowerName.endsWith(ext));
52504
- }
52505
- function isGzipFile(file2) {
52506
- return file2.mimeType === "application/gzip" || file2.mimeType === "application/x-gzip" || file2.name.toLowerCase().endsWith(".gz");
52507
- }
52508
- function isZipFile(file2) {
52509
- return file2.mimeType === "application/zip" || file2.mimeType === "application/x-zip-compressed" || file2.name.toLowerCase().endsWith(".zip");
52510
- }
52511
- function categorizeFile(file2) {
52512
- if (isImageFile(file2))
52513
- return "image";
52514
- if (isPdfFile(file2))
52515
- return "pdf";
52516
- if (isZipFile(file2))
52517
- return "zip";
52518
- if (isGzipFile(file2))
52519
- return "gzip";
52520
- if (isTextFile(file2))
52521
- return "text";
52522
- return "unsupported";
52523
- }
52524
- async function processImageFile(file2, platform, debug = false) {
52525
- try {
52526
- if (!platform.downloadFile) {
52527
- return {
52528
- skipped: {
52529
- name: file2.name,
52530
- reason: "Platform does not support file downloads"
52531
- }
52532
- };
52533
- }
52534
- const buffer = await platform.downloadFile(file2.id);
52535
- const base644 = buffer.toString("base64");
52536
- if (debug) {
52537
- log2.debug(`Attached image: ${file2.name} (${file2.mimeType}, ${Math.round(buffer.length / 1024)}KB)`);
52538
- }
52539
- return {
52540
- block: {
52541
- type: "image",
52542
- source: {
52543
- type: "base64",
52544
- media_type: file2.mimeType,
52545
- data: base644
52546
- }
52547
- }
52548
- };
52549
- } catch (err) {
52550
- log2.error(`Failed to download image ${file2.name}: ${err}`);
52551
- return {
52552
- skipped: {
52553
- name: file2.name,
52554
- reason: `Download failed: ${err instanceof Error ? err.message : String(err)}`
52555
- }
52556
- };
52557
- }
52558
- }
52559
- async function processPdfFile(file2, platform, debug = false) {
52560
- try {
52561
- if (!platform.downloadFile) {
52562
- return {
52563
- skipped: {
52564
- name: file2.name,
52565
- reason: "Platform does not support file downloads"
52566
- }
52567
- };
52568
- }
52569
- const buffer = await platform.downloadFile(file2.id);
52570
- if (buffer.length > MAX_PDF_SIZE) {
52571
- return {
52572
- skipped: {
52573
- name: file2.name,
52574
- reason: `PDF exceeds ${Math.round(MAX_PDF_SIZE / 1024 / 1024)}MB limit (${Math.round(buffer.length / 1024 / 1024)}MB)`,
52575
- suggestion: "Try splitting the PDF into smaller parts"
52576
- }
52577
- };
52578
- }
52579
- const base644 = buffer.toString("base64");
52580
- if (debug) {
52581
- log2.debug(`Attached PDF: ${file2.name} (${Math.round(buffer.length / 1024)}KB)`);
52582
- }
52583
- return {
52584
- block: {
52585
- type: "document",
52586
- source: {
52587
- type: "base64",
52588
- media_type: "application/pdf",
52589
- data: base644
52590
- },
52591
- title: file2.name
52592
- }
52593
- };
52594
- } catch (err) {
52595
- log2.error(`Failed to process PDF ${file2.name}: ${err}`);
52596
- return {
52597
- skipped: {
52598
- name: file2.name,
52599
- reason: `Processing failed: ${err instanceof Error ? err.message : String(err)}`
52600
- }
52601
- };
52602
- }
52603
- }
52604
- async function processTextFile(file2, platform, debug = false) {
52605
- try {
52606
- if (!platform.downloadFile) {
52607
- return {
52608
- skipped: {
52609
- name: file2.name,
52610
- reason: "Platform does not support file downloads"
52611
- }
52612
- };
52613
- }
52614
- const buffer = await platform.downloadFile(file2.id);
52615
- if (buffer.length > MAX_TEXT_FILE_SIZE) {
52616
- return {
52617
- skipped: {
52618
- name: file2.name,
52619
- reason: `File exceeds ${Math.round(MAX_TEXT_FILE_SIZE / 1024)}KB limit (${Math.round(buffer.length / 1024)}KB)`,
52620
- suggestion: "Try splitting the file or extracting relevant portions"
52621
- }
52622
- };
52623
- }
52624
- const content = buffer.toString("utf-8");
52625
- if (debug) {
52626
- log2.debug(`Attached text file: ${file2.name} (${Math.round(buffer.length / 1024)}KB)`);
52627
- }
52628
- const wrappedContent = formatTextFileContent(file2.name, content);
52629
- return {
52630
- block: {
52631
- type: "text",
52632
- text: wrappedContent
52633
- }
52634
- };
52635
- } catch (err) {
52636
- log2.error(`Failed to process text file ${file2.name}: ${err}`);
52637
- return {
52638
- skipped: {
52639
- name: file2.name,
52640
- reason: `Processing failed: ${err instanceof Error ? err.message : String(err)}`
52641
- }
52642
- };
52643
- }
52644
- }
52645
- function formatTextFileContent(filename, content) {
52646
- return `\uD83D\uDCC4 **${filename}**:
52647
- \`\`\`
52648
- ${content}
52649
- \`\`\``;
52650
- }
52651
- async function decompressGzipStream(compressedBuffer) {
52652
- const chunks = [];
52653
- let totalSize = 0;
52654
- const gunzip = createGunzip();
52655
- const source = Readable.from(compressedBuffer);
52656
- const collector = new Writable({
52657
- write(chunk, _encoding, callback) {
52658
- totalSize += chunk.length;
52659
- if (totalSize > MAX_DECOMPRESSED_SIZE) {
52660
- callback(new Error(`Decompressed size exceeds ${Math.round(MAX_DECOMPRESSED_SIZE / 1024 / 1024)}MB limit`));
52661
- return;
52662
- }
52663
- chunks.push(chunk);
52664
- callback();
52665
- }
52666
- });
52667
- await pipeline2(source, gunzip, collector);
52668
- return Buffer.concat(chunks);
52669
- }
52670
- async function processGzipFile(file2, platform, debug = false) {
52671
- try {
52672
- if (!platform.downloadFile) {
52673
- return {
52674
- skipped: {
52675
- name: file2.name,
52676
- reason: "Platform does not support file downloads"
52677
- }
52678
- };
52679
- }
52680
- if (file2.size && file2.size > MAX_GZIP_SIZE) {
52681
- return {
52682
- skipped: {
52683
- name: file2.name,
52684
- reason: `Gzip file exceeds ${Math.round(MAX_GZIP_SIZE / 1024 / 1024)}MB limit (${Math.round(file2.size / 1024 / 1024)}MB)`,
52685
- suggestion: "Try compressing a smaller file or splitting the content"
52686
- }
52687
- };
52688
- }
52689
- let compressedBuffer;
52690
- try {
52691
- compressedBuffer = await platform.downloadFile(file2.id);
52692
- } catch (err) {
52693
- const errorMessage = err instanceof Error ? err.message : String(err);
52694
- log2.error(`Failed to download gzip file ${file2.name}: ${errorMessage}`);
52695
- return {
52696
- skipped: {
52697
- name: file2.name,
52698
- reason: `Download failed: ${errorMessage}`,
52699
- suggestion: "Check if the file is still available and try again"
52700
- }
52701
- };
52702
- }
52703
- if (file2.size && compressedBuffer.length !== file2.size) {
52704
- log2.warn(`Downloaded size mismatch for ${file2.name}: expected ${file2.size}, got ${compressedBuffer.length}`);
52705
- }
52706
- let decompressedBuffer;
52707
- try {
52708
- decompressedBuffer = await decompressGzipStream(compressedBuffer);
52709
- } catch (err) {
52710
- const errorMessage = err instanceof Error ? err.message : String(err);
52711
- let reason;
52712
- let suggestion;
52713
- if (errorMessage.includes("incorrect header check")) {
52714
- reason = "Invalid gzip file: the file header is corrupted or this is not a gzip file";
52715
- suggestion = "Verify the file is a valid gzip archive";
52716
- } else if (errorMessage.includes("unexpected end of file")) {
52717
- reason = "Incomplete gzip file: the file appears to be truncated";
52718
- suggestion = "Re-download the file or check if the upload completed";
52719
- } else if (errorMessage.includes("invalid stored block lengths")) {
52720
- reason = "Corrupted gzip file: the compressed data is damaged";
52721
- suggestion = "Try re-compressing the original file";
52722
- } else if (errorMessage.includes("exceeds") && errorMessage.includes("limit")) {
52723
- reason = errorMessage;
52724
- suggestion = "Try extracting only the relevant portions of the file";
52725
- } else {
52726
- reason = `Decompression failed: ${errorMessage}`;
52727
- suggestion = "Verify the file is a valid gzip archive";
52728
- }
52729
- return {
52730
- skipped: {
52731
- name: file2.name,
52732
- reason,
52733
- suggestion
52734
- }
52735
- };
52736
- }
52737
- const innerFilename = file2.name.toLowerCase().endsWith(".gz") ? file2.name.slice(0, -3) : file2.name;
52738
- const contentType = detectDecompressedContentType(decompressedBuffer, innerFilename);
52739
- if (debug) {
52740
- log2.debug(`Decompressed ${file2.name}: ${Math.round(decompressedBuffer.length / 1024)}KB, detected type: ${contentType}`);
52741
- }
52742
- if (contentType === "pdf") {
52743
- const base644 = decompressedBuffer.toString("base64");
52744
- return {
52745
- block: {
52746
- type: "document",
52747
- source: {
52748
- type: "base64",
52749
- media_type: "application/pdf",
52750
- data: base644
52751
- },
52752
- title: innerFilename
52753
- }
52754
- };
52755
- } else if (contentType === "text") {
52756
- const content = decompressedBuffer.toString("utf-8");
52757
- const wrappedContent = formatTextFileContent(innerFilename, content);
52758
- return {
52759
- block: {
52760
- type: "text",
52761
- text: wrappedContent
52762
- }
52763
- };
52764
- } else {
52765
- return {
52766
- skipped: {
52767
- name: file2.name,
52768
- reason: "Decompressed content type not supported",
52769
- suggestion: "Only text-based files and PDFs are supported after decompression"
52770
- }
52771
- };
52772
- }
52773
- } catch (err) {
52774
- const errorMessage = err instanceof Error ? err.message : String(err);
52775
- log2.error(`Failed to process gzip file ${file2.name}: ${errorMessage}`);
52776
- return {
52777
- skipped: {
52778
- name: file2.name,
52779
- reason: `Processing failed: ${errorMessage}`,
52780
- suggestion: "An unexpected error occurred. Please try again or contact support if the issue persists"
52781
- }
52782
- };
52783
- }
52784
- }
52785
- async function extractZipEntry(zipfile, entry) {
52786
- return new Promise((resolve2, reject) => {
52787
- zipfile.openReadStream(entry, (err, readStream) => {
52788
- if (err) {
52789
- reject(err);
52790
- return;
52791
- }
52792
- if (!readStream) {
52793
- reject(new Error("No read stream"));
52794
- return;
52795
- }
52796
- const chunks = [];
52797
- readStream.on("data", (chunk) => chunks.push(chunk));
52798
- readStream.on("end", () => resolve2(Buffer.concat(chunks)));
52799
- readStream.on("error", reject);
52800
- });
52801
- });
52802
- }
52803
- async function processZipFile(file2, platform, debug = false) {
52804
- const blocks = [];
52805
- const skipped = [];
52806
- try {
52807
- if (!platform.downloadFile) {
52808
- return {
52809
- blocks: [],
52810
- skipped: [{
52811
- name: file2.name,
52812
- reason: "Platform does not support file downloads"
52813
- }]
52814
- };
52815
- }
52816
- if (file2.size && file2.size > MAX_ZIP_SIZE) {
52817
- return {
52818
- blocks: [],
52819
- skipped: [{
52820
- name: file2.name,
52821
- reason: `Zip file exceeds ${Math.round(MAX_ZIP_SIZE / 1024 / 1024)}MB limit (${Math.round(file2.size / 1024 / 1024)}MB)`
52822
- }]
52823
- };
52824
- }
52825
- const zipBuffer = await platform.downloadFile(file2.id);
52826
- if (debug) {
52827
- log2.debug(`Processing zip file ${file2.name}: ${Math.round(zipBuffer.length / 1024)}KB`);
52828
- }
52829
- const zipfile = await new Promise((resolve2, reject) => {
52830
- import_yauzl.default.fromBuffer(zipBuffer, { lazyEntries: true }, (err, zf) => {
52831
- if (err)
52832
- reject(err);
52833
- else if (!zf)
52834
- reject(new Error("Failed to open zip file"));
52835
- else
52836
- resolve2(zf);
52837
- });
52838
- });
52839
- const entries = [];
52840
- await new Promise((resolve2, reject) => {
52841
- zipfile.on("entry", (entry) => {
52842
- if (!entry.fileName.endsWith("/")) {
52843
- entries.push(entry);
52844
- }
52845
- zipfile.readEntry();
52846
- });
52847
- zipfile.on("end", resolve2);
52848
- zipfile.on("error", reject);
52849
- zipfile.readEntry();
52850
- });
52851
- if (entries.length > MAX_ZIP_FILES) {
52852
- zipfile.close();
52853
- return {
52854
- blocks: [],
52855
- skipped: [{
52856
- name: file2.name,
52857
- reason: `Zip contains too many files (${entries.length}). Maximum is ${MAX_ZIP_FILES} files.`,
52858
- suggestion: "Extract and upload the most relevant files individually"
52859
- }]
52860
- };
52861
- }
52862
- if (entries.length === 0) {
52863
- zipfile.close();
52864
- return {
52865
- blocks: [],
52866
- skipped: [{
52867
- name: file2.name,
52868
- reason: "Zip archive is empty"
52869
- }]
52870
- };
52871
- }
52872
- const zipfile2 = await new Promise((resolve2, reject) => {
52873
- import_yauzl.default.fromBuffer(zipBuffer, { lazyEntries: true }, (err, zf) => {
52874
- if (err)
52875
- reject(err);
52876
- else if (!zf)
52877
- reject(new Error("Failed to open zip file"));
52878
- else
52879
- resolve2(zf);
52880
- });
52881
- });
52882
- let processedCount = 0;
52883
- await new Promise((resolve2, reject) => {
52884
- zipfile2.on("entry", async (entry) => {
52885
- try {
52886
- if (entry.fileName.endsWith("/")) {
52887
- zipfile2.readEntry();
52888
- return;
52889
- }
52890
- if (entry.uncompressedSize > MAX_DECOMPRESSED_SIZE) {
52891
- skipped.push({
52892
- name: entry.fileName,
52893
- reason: `File exceeds ${Math.round(MAX_DECOMPRESSED_SIZE / 1024 / 1024)}MB decompressed size limit`
52894
- });
52895
- zipfile2.readEntry();
52896
- return;
52897
- }
52898
- const buffer = await extractZipEntry(zipfile2, entry);
52899
- const contentType = detectDecompressedContentType(buffer, entry.fileName);
52900
- if (debug) {
52901
- log2.debug(`Extracted ${entry.fileName}: ${Math.round(buffer.length / 1024)}KB, type: ${contentType}`);
52902
- }
52903
- if (contentType === "pdf") {
52904
- const base644 = buffer.toString("base64");
52905
- blocks.push({
52906
- type: "document",
52907
- source: {
52908
- type: "base64",
52909
- media_type: "application/pdf",
52910
- data: base644
52911
- }
52912
- });
52913
- processedCount++;
52914
- } else if (contentType === "text") {
52915
- const content = buffer.toString("utf-8");
52916
- const wrappedContent = formatTextFileContent(entry.fileName, content);
52917
- blocks.push({
52918
- type: "text",
52919
- text: wrappedContent
52920
- });
52921
- processedCount++;
52922
- } else {
52923
- skipped.push({
52924
- name: entry.fileName,
52925
- reason: "Unsupported file type inside zip",
52926
- suggestion: "Only text-based files and PDFs are supported"
52927
- });
52928
- }
52929
- zipfile2.readEntry();
52930
- } catch (err) {
52931
- skipped.push({
52932
- name: entry.fileName,
52933
- reason: `Failed to extract: ${err instanceof Error ? err.message : String(err)}`
52934
- });
52935
- zipfile2.readEntry();
52936
- }
52937
- });
52938
- zipfile2.on("end", resolve2);
52939
- zipfile2.on("error", reject);
52940
- zipfile2.readEntry();
52941
- });
52942
- zipfile2.close();
52943
- if (debug) {
52944
- log2.debug(`Zip ${file2.name}: processed ${processedCount} files, skipped ${skipped.length}`);
52945
- }
52946
- return { blocks, skipped };
52947
- } catch (err) {
52948
- log2.error(`Failed to process zip file ${file2.name}: ${err}`);
52949
- return {
52950
- blocks: [],
52951
- skipped: [{
52952
- name: file2.name,
52953
- reason: `Failed to process zip: ${err instanceof Error ? err.message : String(err)}`
52954
- }]
52955
- };
52956
- }
52957
- }
52958
- function detectDecompressedContentType(buffer, filename) {
52959
- if (buffer.length >= 5 && buffer.toString("ascii", 0, 5) === "%PDF-") {
52960
- return "pdf";
52961
- }
52962
- const lowerFilename = filename.toLowerCase();
52963
- if (lowerFilename.endsWith(".pdf")) {
52964
- return "pdf";
52965
- }
52966
- if (TEXT_FILE_EXTENSIONS.some((ext) => lowerFilename.endsWith(ext))) {
52967
- return "text";
52968
- }
52969
- if (buffer.length > 0) {
52970
- const firstChar = String.fromCharCode(buffer[0]);
52971
- if (firstChar === "{" || firstChar === "[") {
52972
- return "text";
52973
- }
52974
- }
52975
- try {
52976
- const text = buffer.toString("utf-8");
52977
- const printableRatio = countPrintableChars(text) / text.length;
52978
- if (printableRatio > 0.9) {
52979
- return "text";
52980
- }
52981
- } catch {}
52982
- return "unknown";
52983
- }
52984
- function countPrintableChars(text) {
52985
- let count = 0;
52986
- for (let i = 0;i < text.length; i++) {
52987
- const code = text.charCodeAt(i);
52988
- if (code >= 32 && code <= 126 || code === 9 || code === 10 || code === 13) {
52989
- count++;
52990
- }
52991
- }
52992
- return count;
52993
- }
52994
- function getUnsupportedFileSuggestion(file2) {
52995
- const ext = file2.name.toLowerCase().split(".").pop();
52996
- const mime = file2.mimeType.toLowerCase();
52997
- if (ext === "doc" || ext === "docx" || mime.includes("msword") || mime.includes("wordprocessingml")) {
52998
- return "Convert to PDF for best results";
52999
- }
53000
- if (ext === "xls" || ext === "xlsx" || mime.includes("spreadsheet")) {
53001
- return "Export as CSV for text-based analysis";
53002
- }
53003
- if (ext === "ppt" || ext === "pptx" || mime.includes("presentation")) {
53004
- return "Convert to PDF for best results";
53005
- }
53006
- if (ext === "tar" || ext === "rar" || ext === "7z") {
53007
- return "Extract files and upload them individually, or use .zip format";
53008
- }
53009
- if (ext === "exe" || ext === "dll" || ext === "so" || ext === "dylib") {
53010
- return "Binary files are not supported";
53011
- }
53012
- return;
52416
+ async function postSkippedFilesFeedback(platform, threadId, skipped) {
52417
+ if (skipped.length === 0)
52418
+ return;
52419
+ await platform.createPost(formatSkippedFilesFeedback(skipped), threadId);
53013
52420
  }
53014
- async function processFiles(platform, files, debug = false) {
53015
- const blocks = [];
53016
- const skipped = [];
53017
- if (!files || files.length === 0) {
53018
- return { blocks, skipped };
53019
- }
53020
- for (const file2 of files) {
53021
- const category = categorizeFile(file2);
53022
- if (category === "zip") {
53023
- const zipResult = await processZipFile(file2, platform, debug);
53024
- blocks.push(...zipResult.blocks);
53025
- for (const s of zipResult.skipped) {
53026
- skipped.push(s);
53027
- log2.warn(`Skipped file ${s.name}: ${s.reason}`);
53028
- }
53029
- continue;
53030
- }
53031
- let result;
53032
- switch (category) {
53033
- case "image":
53034
- result = await processImageFile(file2, platform, debug);
53035
- break;
53036
- case "pdf":
53037
- result = await processPdfFile(file2, platform, debug);
53038
- break;
53039
- case "text":
53040
- result = await processTextFile(file2, platform, debug);
53041
- break;
53042
- case "gzip":
53043
- result = await processGzipFile(file2, platform, debug);
53044
- break;
53045
- case "unsupported":
53046
- default:
53047
- result = {
53048
- skipped: {
53049
- name: file2.name,
53050
- reason: `Unsupported file type: ${file2.mimeType}`,
53051
- suggestion: getUnsupportedFileSuggestion(file2)
53052
- }
53053
- };
53054
- break;
53055
- }
53056
- if (result.block) {
53057
- blocks.push(result.block);
53058
- }
53059
- if (result.skipped) {
53060
- skipped.push(result.skipped);
53061
- log2.warn(`Skipped file ${result.skipped.name}: ${result.skipped.reason}`);
52421
+ function formatSkippedFilesFeedback(skippedFiles) {
52422
+ const lines = ["⚠️ **Some files could not be processed:**"];
52423
+ for (const file2 of skippedFiles) {
52424
+ let line = `- **${file2.name}**: ${file2.reason}`;
52425
+ if (file2.suggestion) {
52426
+ line += ` _(${file2.suggestion})_`;
53062
52427
  }
52428
+ lines.push(line);
53063
52429
  }
53064
- return { blocks, skipped };
52430
+ return lines.join(`
52431
+ `);
53065
52432
  }
53066
52433
 
53067
52434
  // src/operations/message-manager.ts
@@ -53495,20 +52862,15 @@ class MessageManager {
53495
52862
  }
53496
52863
  this.session.threadLogger?.logUserMessage(username || this.session.startedBy, message, displayName, files && files.length > 0);
53497
52864
  await this.prepareForUserMessage();
53498
- let skippedFiles = [];
53499
- if (files && files.length > 0) {
53500
- const fileResult = await processFiles(this.platform, files);
53501
- skippedFiles = fileResult.skipped;
53502
- }
53503
52865
  let content = message;
52866
+ let skippedFiles = [];
53504
52867
  if (this.buildMessageContentCallback) {
53505
- content = await this.buildMessageContentCallback(message, this.platform, files);
52868
+ const built = await this.buildMessageContentCallback(message, this.platform, files);
52869
+ content = built.content;
52870
+ skippedFiles = built.skipped;
53506
52871
  }
53507
52872
  this.session.claude.sendMessage(content);
53508
- if (skippedFiles.length > 0) {
53509
- const feedback = this.formatSkippedFilesFeedback(skippedFiles);
53510
- await this.platform.createPost(feedback, this.threadId);
53511
- }
52873
+ await postSkippedFilesFeedback(this.platform, this.threadId, skippedFiles);
53512
52874
  this.session.lastActivityAt = new Date;
53513
52875
  this.session.isProcessing = true;
53514
52876
  this.emitSessionUpdateCallback?.({ status: "active", isTyping: true });
@@ -53516,18 +52878,6 @@ class MessageManager {
53516
52878
  logger.debug("User message sent to Claude");
53517
52879
  return true;
53518
52880
  }
53519
- formatSkippedFilesFeedback(skippedFiles) {
53520
- const lines = ["⚠️ **Some files could not be processed:**"];
53521
- for (const file2 of skippedFiles) {
53522
- let line = `- **${file2.name}**: ${file2.reason}`;
53523
- if (file2.suggestion) {
53524
- line += ` _(${file2.suggestion})_`;
53525
- }
53526
- lines.push(line);
53527
- }
53528
- return lines.join(`
53529
- `);
53530
- }
53531
52881
  getSession() {
53532
52882
  return this.session;
53533
52883
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "claude-threads",
3
- "version": "1.6.2",
3
+ "version": "1.6.3",
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",