claude-threads 1.15.1 → 1.16.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/CHANGELOG.md CHANGED
@@ -7,6 +7,27 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
7
7
 
8
8
  ## [Unreleased]
9
9
 
10
+ ## [1.16.0] - 2026-05-14
11
+
12
+ ### Added
13
+ - **Per-platform channel-verbosity controls.** New `sessionHeader` and `stickyMessage` settings, both `full` (default) / `minimal` (one-line status bar) / `hidden` (no post). Reachable three ways: setup wizard ("How verbose should the bot be in this channel?"), CLI flags (`--session-header`, `--sticky-message`, applied to every platform), or per-platform YAML for split values. `hidden` for the header means Claude's reply is the first message in the thread; `hidden` for the sticky stops the `channel_post` bump entirely. Update notices still ride along in `minimal`. Session-header mode persists per session — resume preserves the user's choice. Old `sessions.json` defaults to `full`. The pre-existing top-level `stickyMessage: { description, footer }` block is unchanged. (#384, closes #383)
14
+
15
+ ### Security
16
+ - **`ws` 8.20.0 → 8.20.1** picks up a fix for an uninitialized memory disclosure in `websocket.close()`. Triggered when a `TypedArray` (e.g. `Float32Array`) is passed as the `reason` argument instead of a string or `Buffer` — uninitialized memory was leaked to the remote peer. claude-threads doesn't pass typed arrays to `close()` anywhere we control, but the Mattermost client and inbound webhook surface use `ws` as a transitive dependency. (#382)
17
+
18
+ ### Changed
19
+ - **Production deps** bumped: `@hono/node-server` 2.0.1 → 2.0.2 (serve-static fixes), `react` 19.2.5 → 19.2.6 (RSC type hardening), `semver` 7.7.4 → 7.8.0 (new `truncate` function), `ink-scroll-view` 0.3.6 → 0.3.7. Lockfile-only. (#382)
20
+ - **Dev deps** bumped: `@types/node` 25.6.0 → 25.7.0, `lint-staged` 16.4.0 → 17.0.4, `typescript-eslint` 8.59.2 → 8.59.3. lint-staged 17 drops Node 20 support, but it's a dev dep — the bot's runtime floor is unchanged. Lockfile-only. (#381)
21
+
22
+ ## [1.15.2] - 2026-05-06
23
+
24
+ ### Security
25
+ - **hono 4.12.15 → 4.12.17** picks up two upstream security fixes — GHSA-69xw-7hcm-h432 (unvalidated JSX tag names in `hono/jsx` could allow HTML injection) and GHSA-9vqf-7f2p-gf9v (`bodyLimit()` could be bypassed for chunked / unknown-length requests). claude-threads doesn't render JSX from untrusted input today, but the bodyLimit fix is load-bearing for the inbound webhook surface. (#377)
26
+
27
+ ### Changed
28
+ - **Production deps** bumped: `@hono/node-server` 2.0.0 → 2.0.1 (forwards Hono response headers during WebSocket upgrade), `express-rate-limit` 8.4.1 → 8.5.0 (async store init), `zod` 4.3.6 → 4.4.3 (correctness fixes for `preprocess`, `catch`, and discriminated unions on absent object keys). Lockfile-only. (#377)
29
+ - **Dev deps** bumped: `eslint` 10.2.1 → 10.3.0, `typescript-eslint` 8.59.1 → 8.59.2. Lockfile-only. (#376)
30
+
10
31
  ## [1.15.1] - 2026-05-05
11
32
 
12
33
  ### Fixed
package/dist/index.js CHANGED
@@ -48344,6 +48344,18 @@ var jsYaml = {
48344
48344
  };
48345
48345
 
48346
48346
  // src/config/types.ts
48347
+ var OVERHEAD_VISIBILITY_VALUES = ["full", "minimal", "hidden"];
48348
+ var DEFAULT_OVERHEAD_VISIBILITY = "full";
48349
+ function isOverheadVisibility(value) {
48350
+ return typeof value === "string" && OVERHEAD_VISIBILITY_VALUES.includes(value);
48351
+ }
48352
+ function resolveOverheadVisibility(value, fieldPath) {
48353
+ if (value === undefined || value === null)
48354
+ return DEFAULT_OVERHEAD_VISIBILITY;
48355
+ if (isOverheadVisibility(value))
48356
+ return value;
48357
+ throw new Error(`Invalid ${fieldPath}: expected one of ${OVERHEAD_VISIBILITY_VALUES.join(", ")}, got ${JSON.stringify(value)}`);
48358
+ }
48347
48359
  var LIMITS_DEFAULTS = {
48348
48360
  maxSessions: 5,
48349
48361
  sessionTimeoutMinutes: 30,
@@ -48648,6 +48660,26 @@ var PERMISSION_MODE_CHOICES = [
48648
48660
  function permissionModeChoiceIndex(mode) {
48649
48661
  return PERMISSION_MODE_CHOICES.findIndex((c) => c.value === mode);
48650
48662
  }
48663
+ var OVERHEAD_VISIBILITY_CHOICES = [
48664
+ {
48665
+ title: "Full (default)",
48666
+ value: "full",
48667
+ description: "Per-thread session header + channel sticky with active-sessions list"
48668
+ },
48669
+ {
48670
+ title: "Minimal",
48671
+ value: "minimal",
48672
+ description: "One-line status bar only — drops the table and sessions list"
48673
+ },
48674
+ {
48675
+ title: "Hidden",
48676
+ value: "hidden",
48677
+ description: "No header post, no sticky — Claude's reply is the first message in the thread"
48678
+ }
48679
+ ];
48680
+ function overheadVisibilityChoiceIndex(mode) {
48681
+ return OVERHEAD_VISIBILITY_CHOICES.findIndex((c) => c.value === mode);
48682
+ }
48651
48683
  var __dirname2 = dirname2(fileURLToPath(import.meta.url));
48652
48684
  var SLACK_MANIFEST_PATH = join2(__dirname2, "..", "docs", "slack-app-manifest.yaml");
48653
48685
  var onCancel = () => {
@@ -49417,6 +49449,10 @@ async function setupMattermostPlatform(id, existing) {
49417
49449
  permissionMode: existingMattermost.permissionMode,
49418
49450
  skipPermissions: existingMattermost.skipPermissions
49419
49451
  }) : "auto";
49452
+ const existingHeader = existingMattermost?.sessionHeader;
49453
+ const existingSticky = existingMattermost?.stickyMessage;
49454
+ const hasSplitVerbosity = existingHeader !== undefined && existingSticky !== undefined && existingHeader !== existingSticky;
49455
+ let lastChannelVerbosity = existingHeader ?? existingSticky ?? DEFAULT_OVERHEAD_VISIBILITY;
49420
49456
  while (true) {
49421
49457
  console.log("");
49422
49458
  console.log(dim(" Now enter your Mattermost credentials:"));
@@ -49521,6 +49557,20 @@ async function setupMattermostPlatform(id, existing) {
49521
49557
  choices: PERMISSION_MODE_CHOICES,
49522
49558
  initial: permissionModeChoiceIndex(lastPermissionMode)
49523
49559
  }, { onCancel });
49560
+ let channelVerbosity = lastChannelVerbosity;
49561
+ if (hasSplitVerbosity) {
49562
+ console.log("");
49563
+ console.log(dim(` Channel verbosity: keeping split values from current config ` + `(sessionHeader=${existingHeader}, stickyMessage=${existingSticky}). ` + `Edit YAML directly to change.`));
49564
+ } else {
49565
+ const result = await import_prompts.default({
49566
+ type: "select",
49567
+ name: "channelVerbosity",
49568
+ message: "How verbose should the bot be in this channel?",
49569
+ choices: OVERHEAD_VISIBILITY_CHOICES,
49570
+ initial: overheadVisibilityChoiceIndex(lastChannelVerbosity)
49571
+ }, { onCancel });
49572
+ channelVerbosity = result.channelVerbosity;
49573
+ }
49524
49574
  lastUrl = basicSettings.url;
49525
49575
  lastDisplayName = basicSettings.displayName;
49526
49576
  lastToken = finalToken;
@@ -49528,6 +49578,7 @@ async function setupMattermostPlatform(id, existing) {
49528
49578
  lastBotName = basicSettings.botName;
49529
49579
  lastAllowedUsers = allowedUsers.join(",");
49530
49580
  lastPermissionMode = permissionMode;
49581
+ lastChannelVerbosity = channelVerbosity;
49531
49582
  console.log("");
49532
49583
  console.log(dim(" Validating credentials..."));
49533
49584
  const validationResult = await validateMattermostCredentials(basicSettings.url, finalToken, basicSettings.channelId);
@@ -49586,7 +49637,8 @@ async function setupMattermostPlatform(id, existing) {
49586
49637
  channelId: basicSettings.channelId,
49587
49638
  botName: basicSettings.botName,
49588
49639
  allowedUsers,
49589
- permissionMode: lastPermissionMode
49640
+ permissionMode: lastPermissionMode,
49641
+ ...hasSplitVerbosity ? { sessionHeader: existingHeader, stickyMessage: existingSticky } : lastChannelVerbosity !== DEFAULT_OVERHEAD_VISIBILITY ? { sessionHeader: lastChannelVerbosity, stickyMessage: lastChannelVerbosity } : {}
49590
49642
  };
49591
49643
  }
49592
49644
  }
@@ -49703,6 +49755,10 @@ async function setupSlackPlatform(id, existing) {
49703
49755
  permissionMode: existingSlack.permissionMode,
49704
49756
  skipPermissions: existingSlack.skipPermissions
49705
49757
  }) : "auto";
49758
+ const existingHeader = existingSlack?.sessionHeader;
49759
+ const existingSticky = existingSlack?.stickyMessage;
49760
+ const hasSplitVerbosity = existingHeader !== undefined && existingSticky !== undefined && existingHeader !== existingSticky;
49761
+ let lastChannelVerbosity = existingHeader ?? existingSticky ?? DEFAULT_OVERHEAD_VISIBILITY;
49706
49762
  while (true) {
49707
49763
  console.log("");
49708
49764
  console.log(dim(" Now enter your Slack credentials:"));
@@ -49818,6 +49874,20 @@ async function setupSlackPlatform(id, existing) {
49818
49874
  choices: PERMISSION_MODE_CHOICES,
49819
49875
  initial: permissionModeChoiceIndex(lastPermissionMode)
49820
49876
  }, { onCancel });
49877
+ let channelVerbosity = lastChannelVerbosity;
49878
+ if (hasSplitVerbosity) {
49879
+ console.log("");
49880
+ console.log(dim(` Channel verbosity: keeping split values from current config ` + `(sessionHeader=${existingHeader}, stickyMessage=${existingSticky}). ` + `Edit YAML directly to change.`));
49881
+ } else {
49882
+ const result = await import_prompts.default({
49883
+ type: "select",
49884
+ name: "channelVerbosity",
49885
+ message: "How verbose should the bot be in this channel?",
49886
+ choices: OVERHEAD_VISIBILITY_CHOICES,
49887
+ initial: overheadVisibilityChoiceIndex(lastChannelVerbosity)
49888
+ }, { onCancel });
49889
+ channelVerbosity = result.channelVerbosity;
49890
+ }
49821
49891
  lastDisplayName = basicSettings.displayName;
49822
49892
  lastBotToken = finalBotToken;
49823
49893
  lastAppToken = finalAppToken;
@@ -49825,6 +49895,7 @@ async function setupSlackPlatform(id, existing) {
49825
49895
  lastBotName = basicSettings.botName;
49826
49896
  lastAllowedUsers = allowedUsers.join(",");
49827
49897
  lastPermissionMode = permissionMode;
49898
+ lastChannelVerbosity = channelVerbosity;
49828
49899
  console.log("");
49829
49900
  console.log(dim(" Validating credentials..."));
49830
49901
  const validationResult = await validateSlackCredentials(finalBotToken, finalAppToken, basicSettings.channelId);
@@ -49889,7 +49960,8 @@ async function setupSlackPlatform(id, existing) {
49889
49960
  channelId: basicSettings.channelId,
49890
49961
  botName: basicSettings.botName,
49891
49962
  allowedUsers,
49892
- permissionMode: lastPermissionMode
49963
+ permissionMode: lastPermissionMode,
49964
+ ...hasSplitVerbosity ? { sessionHeader: existingHeader, stickyMessage: existingSticky } : lastChannelVerbosity !== DEFAULT_OVERHEAD_VISIBILITY ? { sessionHeader: lastChannelVerbosity, stickyMessage: lastChannelVerbosity } : {}
49893
49965
  };
49894
49966
  }
49895
49967
  }
@@ -63980,6 +64052,13 @@ async function buildStickyMessage(sessions, platformId, config, formatter, getTh
63980
64052
  const otherPlatformSessions = allSessions.filter((s) => s.platformId !== platformId);
63981
64053
  const totalCount = allSessions.length;
63982
64054
  const statusBar = await buildStatusBar(totalCount, config, formatter, platformId);
64055
+ if (config.overhead === "minimal") {
64056
+ return [
64057
+ formatter.formatHorizontalRule(),
64058
+ statusBar
64059
+ ].join(`
64060
+ `);
64061
+ }
63983
64062
  const activeSessionIds = new Set(sessions.keys());
63984
64063
  const historySessions = sessionStore ? sessionStore.getHistory(platformId, activeSessionIds).slice(0, 5) : [];
63985
64064
  if (totalCount === 0) {
@@ -64119,7 +64198,35 @@ async function validateLastMessageIds(platform, sessions) {
64119
64198
  });
64120
64199
  await Promise.all(validationPromises);
64121
64200
  }
64201
+ var hiddenCleanupDone = new Set;
64202
+ function clearHiddenCleanupTracking(platformId) {
64203
+ hiddenCleanupDone.delete(platformId);
64204
+ }
64122
64205
  async function updateStickyMessageImpl(platform, sessions, config) {
64206
+ if (config.overhead === "hidden") {
64207
+ if (!hiddenCleanupDone.has(platform.platformId)) {
64208
+ hiddenCleanupDone.add(platform.platformId);
64209
+ const existing = stickyPostIds.get(platform.platformId);
64210
+ if (existing) {
64211
+ log21.info(`sticky[${platform.platformId}] hidden mode: removing leftover ${formatShortId(existing)}`);
64212
+ try {
64213
+ await platform.unpinPost(existing);
64214
+ } catch {}
64215
+ try {
64216
+ await platform.deletePost(existing);
64217
+ } catch {}
64218
+ stickyPostIds.delete(platform.platformId);
64219
+ if (sessionStore) {
64220
+ sessionStore.removeStickyPostId(platform.platformId);
64221
+ }
64222
+ }
64223
+ try {
64224
+ const botUser = await platform.getBotUser();
64225
+ cleanupOldStickyMessages(platform, botUser.id, false, new Set).catch(() => {});
64226
+ } catch {}
64227
+ }
64228
+ return;
64229
+ }
64123
64230
  const platformSessions = [...sessions.values()].filter((s) => s.platformId === platform.platformId);
64124
64231
  log21.debug(`updateStickyMessage for ${platform.platformId}, ${platformSessions.length} sessions`);
64125
64232
  for (const s of platformSessions) {
@@ -64199,8 +64306,11 @@ async function updateStickyMessageImpl(platform, sessions, config) {
64199
64306
  log21.error(`Failed to update sticky message for ${platform.platformId}`, err instanceof Error ? err : undefined);
64200
64307
  }
64201
64308
  }
64202
- async function updateAllStickyMessages(platforms, sessions, config) {
64203
- const updates = [...platforms.values()].map((platform) => updateStickyMessage(platform, sessions, config));
64309
+ async function updateAllStickyMessages(platforms, sessions, config, overheadByPlatform) {
64310
+ const updates = [...platforms.values()].map((platform) => {
64311
+ const overhead = overheadByPlatform?.get(platform.platformId) ?? config.overhead;
64312
+ return updateStickyMessage(platform, sessions, { ...config, overhead });
64313
+ });
64204
64314
  await Promise.all(updates);
64205
64315
  }
64206
64316
  function markNeedsBump(platformId) {
@@ -65986,49 +66096,62 @@ async function requestMessageApproval(session, username, message, ctx) {
65986
66096
  fromUser: username
65987
66097
  });
65988
66098
  }
65989
- async function updateSessionHeader(session, ctx) {
65990
- if (!session.sessionStartPostId)
65991
- return;
66099
+ async function buildSessionHeaderStatusBar(session, ctx) {
65992
66100
  const formatter = session.platform.getFormatter();
65993
- const worktreeContext = session.worktreeInfo ? { path: session.worktreeInfo.worktreePath, branch: session.worktreeInfo.branch } : undefined;
65994
- const shortDir = shortenPath(session.workingDir, undefined, worktreeContext);
65995
66101
  const effectiveMode = effectivePermissionMode({
65996
66102
  override: session.permissionModeOverride,
65997
66103
  sessionHasInteractiveOverride: session.forceInteractivePermissions,
65998
66104
  botWideMode: ctx.config.permissionMode
65999
66105
  });
66000
66106
  const permMode = permissionModeDisplay(effectiveMode).chip;
66001
- const otherParticipants = [...session.sessionAllowedUsers].filter((u) => u !== session.startedBy).map((u) => formatter.formatUserMention(u)).join(", ");
66002
- const statusItems = [];
66003
- const versionStr = formatVersionString();
66004
- statusItems.push(formatter.formatCode(versionStr));
66107
+ const items = [];
66108
+ items.push(formatter.formatCode(formatVersionString()));
66005
66109
  if (session.usageStats) {
66006
66110
  const stats = session.usageStats;
66007
- statusItems.push(formatter.formatCode(`\uD83E\uDD16 ${stats.modelDisplayName}`));
66111
+ items.push(formatter.formatCode(`\uD83E\uDD16 ${stats.modelDisplayName}`));
66008
66112
  const contextPercent = Math.round(stats.contextTokens / stats.contextWindowSize * 100);
66009
- const contextBar = formatContextBar(contextPercent);
66010
- statusItems.push(formatter.formatCode(`${contextBar} ${contextPercent}%`));
66011
- statusItems.push(formatter.formatCode(`\uD83D\uDCB0 $${stats.totalCostUSD.toFixed(2)}`));
66113
+ items.push(formatter.formatCode(`${formatContextBar(contextPercent)} ${contextPercent}%`));
66114
+ items.push(formatter.formatCode(`\uD83D\uDCB0 $${stats.totalCostUSD.toFixed(2)}`));
66012
66115
  }
66013
- statusItems.push(formatter.formatCode(permMode));
66116
+ items.push(formatter.formatCode(permMode));
66014
66117
  if (session.messageManager?.getPendingApproval()?.type === "plan") {
66015
- statusItems.push(formatter.formatCode("\uD83D\uDCCB Plan pending"));
66118
+ items.push(formatter.formatCode("\uD83D\uDCCB Plan pending"));
66016
66119
  } else if (session.planApproved) {
66017
- statusItems.push(formatter.formatCode("\uD83D\uDD28 Implementing"));
66120
+ items.push(formatter.formatCode("\uD83D\uDD28 Implementing"));
66018
66121
  }
66019
66122
  if (ctx.config.chromeEnabled) {
66020
- statusItems.push(formatter.formatCode("\uD83C\uDF10 Chrome"));
66123
+ items.push(formatter.formatCode("\uD83C\uDF10 Chrome"));
66021
66124
  }
66022
66125
  if (keepAlive.isActive()) {
66023
- statusItems.push(formatter.formatCode("\uD83D\uDC93 Keep-alive"));
66126
+ items.push(formatter.formatCode("\uD83D\uDC93 Keep-alive"));
66024
66127
  }
66025
66128
  const battery = await formatBatteryStatus();
66026
66129
  if (battery) {
66027
- statusItems.push(formatter.formatCode(battery));
66130
+ items.push(formatter.formatCode(battery));
66028
66131
  }
66029
- const uptime = formatUptime(session.startedAt);
66030
- statusItems.push(formatter.formatCode(`⏱️ ${uptime}`));
66031
- const statusBar = statusItems.join(" · ");
66132
+ items.push(formatter.formatCode(`⏱️ ${formatUptime(session.startedAt)}`));
66133
+ return items.join(" · ");
66134
+ }
66135
+ async function updateSessionHeader(session, ctx) {
66136
+ if (session.sessionHeaderMode === "hidden")
66137
+ return;
66138
+ if (!session.sessionStartPostId)
66139
+ return;
66140
+ const formatter = session.platform.getFormatter();
66141
+ const statusBar = await buildSessionHeaderStatusBar(session, ctx);
66142
+ const updateInfo = getUpdateInfo();
66143
+ const updateNotice = updateInfo ? `> ⚠️ ${formatter.formatBold("Update available:")} v${updateInfo.current} → v${updateInfo.latest} - Run ${formatter.formatCode("bun install -g claude-threads")}
66144
+
66145
+ ` : undefined;
66146
+ if (session.sessionHeaderMode === "minimal") {
66147
+ const msg2 = [updateNotice, statusBar].filter(Boolean).join(`
66148
+ `);
66149
+ await updatePost(session, session.sessionStartPostId, msg2);
66150
+ return;
66151
+ }
66152
+ const worktreeContext = session.worktreeInfo ? { path: session.worktreeInfo.worktreePath, branch: session.worktreeInfo.branch } : undefined;
66153
+ const shortDir = shortenPath(session.workingDir, undefined, worktreeContext);
66154
+ const otherParticipants = [...session.sessionAllowedUsers].filter((u) => u !== session.startedBy).map((u) => formatter.formatUserMention(u)).join(", ");
66032
66155
  const items = [];
66033
66156
  if (session.sessionTitle) {
66034
66157
  items.push(["\uD83D\uDCDD", "Topic", session.sessionTitle]);
@@ -66074,10 +66197,6 @@ async function updateSessionHeader(session, ctx) {
66074
66197
  const logPath = getLogFilePath(session.platform.platformId, session.claudeSessionId);
66075
66198
  const shortLogPath = logPath.replace(process.env.HOME || "", "~");
66076
66199
  items.push(["\uD83D\uDCCB", "Log File", formatter.formatCode(shortLogPath)]);
66077
- const updateInfo = getUpdateInfo();
66078
- const updateNotice = updateInfo ? `> ⚠️ ${formatter.formatBold("Update available:")} v${updateInfo.current} → v${updateInfo.latest} - Run ${formatter.formatCode("bun install -g claude-threads")}
66079
-
66080
- ` : undefined;
66081
66200
  const msg = [
66082
66201
  updateNotice,
66083
66202
  statusBar,
@@ -66085,8 +66204,7 @@ async function updateSessionHeader(session, ctx) {
66085
66204
  formatter.formatKeyValueList(items)
66086
66205
  ].filter((item) => item !== null && item !== undefined).join(`
66087
66206
  `);
66088
- const postId = session.sessionStartPostId;
66089
- await updatePost(session, postId, msg);
66207
+ await updatePost(session, session.sessionStartPostId, msg);
66090
66208
  }
66091
66209
  async function showUpdateStatus(session, updateManager, ctx) {
66092
66210
  const formatter = session.platform.getFormatter();
@@ -66586,6 +66704,17 @@ function maybeInjectMetadataReminder(message, session, ctx, fullSession) {
66586
66704
  }
66587
66705
  return message;
66588
66706
  }
66707
+ function resumeSessionHeaderMode(persisted, platformConfigured) {
66708
+ return persisted ?? platformConfigured ?? DEFAULT_OVERHEAD_VISIBILITY;
66709
+ }
66710
+ function resolveSessionHeaderMode(configured, replyToPostId, platformId) {
66711
+ const mode = configured ?? DEFAULT_OVERHEAD_VISIBILITY;
66712
+ if (mode === "hidden" && !replyToPostId) {
66713
+ log30.error(`sessionHeader: hidden requires a replyToPostId for ${platformId}; ` + `downgrading this session to 'minimal' so the header post is still short.`);
66714
+ return "minimal";
66715
+ }
66716
+ return mode;
66717
+ }
66589
66718
  async function startSession(options, username, displayName, replyToPostId, platformId, ctx, triggeringPostId, initialOptions) {
66590
66719
  const threadId = replyToPostId || "";
66591
66720
  const existingSessionId = ctx.ops.getSessionId(platformId, threadId);
@@ -66611,13 +66740,18 @@ async function startSession(options, username, displayName, replyToPostId, platf
66611
66740
  return;
66612
66741
  }
66613
66742
  pendingStartsCount++;
66743
+ const sessionHeaderMode = resolveSessionHeaderMode(ctx.ops.getPlatformOverhead(platformId).sessionHeader, replyToPostId, platformId);
66614
66744
  const startFormatter = platform.getFormatter();
66615
- const startPost = await withErrorHandling(() => platform.createPost(startFormatter.formatItalic("Claude Threads session starting..."), replyToPostId), { action: "Create session post" });
66616
- if (!startPost) {
66617
- releasePendingStart();
66618
- return;
66745
+ const skipHeaderPost = sessionHeaderMode === "hidden";
66746
+ let startPost;
66747
+ if (!skipHeaderPost) {
66748
+ startPost = await withErrorHandling(() => platform.createPost(startFormatter.formatItalic("Claude Threads session starting..."), replyToPostId), { action: "Create session post" });
66749
+ if (!startPost) {
66750
+ releasePendingStart();
66751
+ return;
66752
+ }
66619
66753
  }
66620
- const actualThreadId = replyToPostId || startPost.id;
66754
+ const actualThreadId = replyToPostId || (startPost ? startPost.id : "");
66621
66755
  const sessionId = ctx.ops.getSessionId(platformId, actualThreadId);
66622
66756
  platform.sendTyping(actualThreadId);
66623
66757
  const claudeSessionId = randomUUID4();
@@ -66631,13 +66765,23 @@ async function startSession(options, username, displayName, replyToPostId, platf
66631
66765
  const requestedDir = initialOptions.workingDir.startsWith("~") ? initialOptions.workingDir.replace("~", process.env.HOME || "") : initialOptions.workingDir;
66632
66766
  const resolvedDir = resolve6(requestedDir);
66633
66767
  if (!existsSync12(resolvedDir)) {
66634
- await platform.updatePost(startPost.id, `❌ Directory does not exist: ${formatter.formatCode(initialOptions.workingDir)}`);
66768
+ const msg = `❌ Directory does not exist: ${formatter.formatCode(initialOptions.workingDir)}`;
66769
+ if (startPost) {
66770
+ await platform.updatePost(startPost.id, msg);
66771
+ } else {
66772
+ await platform.createPost(msg, replyToPostId);
66773
+ }
66635
66774
  releasePendingStart();
66636
66775
  return;
66637
66776
  }
66638
66777
  const { statSync: statSync4 } = await import("fs");
66639
66778
  if (!statSync4(resolvedDir).isDirectory()) {
66640
- await platform.updatePost(startPost.id, `❌ Not a directory: ${formatter.formatCode(initialOptions.workingDir)}`);
66779
+ const msg = `❌ Not a directory: ${formatter.formatCode(initialOptions.workingDir)}`;
66780
+ if (startPost) {
66781
+ await platform.updatePost(startPost.id, msg);
66782
+ } else {
66783
+ await platform.createPost(msg, replyToPostId);
66784
+ }
66641
66785
  releasePendingStart();
66642
66786
  return;
66643
66787
  }
@@ -66695,7 +66839,8 @@ async function startSession(options, username, displayName, replyToPostId, platf
66695
66839
  sessionAllowedUsers: new Set([username]),
66696
66840
  forceInteractivePermissions,
66697
66841
  permissionModeOverride: sessionPermissionModeOverride,
66698
- sessionStartPostId: startPost.id,
66842
+ sessionStartPostId: startPost ? startPost.id : null,
66843
+ sessionHeaderMode,
66699
66844
  timers: createSessionTimers(),
66700
66845
  lifecycle: createSessionLifecycle(),
66701
66846
  timeoutWarningPosted: false,
@@ -66714,7 +66859,9 @@ async function startSession(options, username, displayName, replyToPostId, platf
66714
66859
  });
66715
66860
  mutableSessions(ctx).set(sessionId, session);
66716
66861
  releasePendingStart();
66717
- ctx.ops.registerPost(startPost.id, actualThreadId);
66862
+ if (startPost) {
66863
+ ctx.ops.registerPost(startPost.id, actualThreadId);
66864
+ }
66718
66865
  ctx.ops.emitSessionAdd(session);
66719
66866
  sessionLog6(session).info(`▶ Session started by @${username}`);
66720
66867
  fireMetadataSuggestions(session, options.prompt, ctx);
@@ -66847,6 +66994,7 @@ Please start a new session.`), { action: "Post resume failure notification" });
66847
66994
  sessionAllowedUsers: new Set(state.sessionAllowedUsers),
66848
66995
  forceInteractivePermissions: state.forceInteractivePermissions ?? false,
66849
66996
  sessionStartPostId: state.sessionStartPostId ?? null,
66997
+ sessionHeaderMode: resumeSessionHeaderMode(state.sessionHeaderMode, ctx.ops.getPlatformOverhead(platformId).sessionHeader),
66850
66998
  timers: createSessionTimers(),
66851
66999
  lifecycle: createResumedLifecycle(state.resumeFailCount ?? 0),
66852
67000
  timeoutWarningPosted: false,
@@ -67581,6 +67729,7 @@ class SessionManager extends EventEmitter4 {
67581
67729
  isShuttingDown = false;
67582
67730
  customDescription;
67583
67731
  customFooter;
67732
+ platformOverhead = new Map;
67584
67733
  autoUpdateManager = null;
67585
67734
  accountPool;
67586
67735
  constructor(workingDir, permissionModeOrSkipFlag = "default", chromeEnabled = false, worktreeMode = "prompt", sessionsPath, threadLogsEnabled = true, threadLogsRetentionDays = 30, limits, claudeAccounts) {
@@ -67612,8 +67761,12 @@ class SessionManager extends EventEmitter4 {
67612
67761
  cleanupWorktrees: this.limits.cleanupWorktrees
67613
67762
  });
67614
67763
  }
67615
- addPlatform(platformId, client) {
67764
+ addPlatform(platformId, client, overhead) {
67616
67765
  this.platforms.set(platformId, client);
67766
+ this.platformOverhead.set(platformId, {
67767
+ sessionHeader: overhead?.sessionHeader ?? DEFAULT_OVERHEAD_VISIBILITY,
67768
+ stickyMessage: overhead?.stickyMessage ?? DEFAULT_OVERHEAD_VISIBILITY
67769
+ });
67617
67770
  client.on("message", (post2, user) => this.handleMessage(platformId, post2, user));
67618
67771
  client.on("reaction", (reaction, user) => {
67619
67772
  if (user) {
@@ -67626,6 +67779,9 @@ class SessionManager extends EventEmitter4 {
67626
67779
  }
67627
67780
  });
67628
67781
  client.on("channel_post", () => {
67782
+ if (this.platformOverhead.get(platformId)?.stickyMessage === "hidden") {
67783
+ return;
67784
+ }
67629
67785
  markNeedsBump(platformId);
67630
67786
  this.updateStickyMessage();
67631
67787
  });
@@ -67633,6 +67789,8 @@ class SessionManager extends EventEmitter4 {
67633
67789
  }
67634
67790
  removePlatform(platformId) {
67635
67791
  this.platforms.delete(platformId);
67792
+ this.platformOverhead.delete(platformId);
67793
+ clearHiddenCleanupTracking(platformId);
67636
67794
  }
67637
67795
  setAutoUpdateManager(manager) {
67638
67796
  this.autoUpdateManager = manager;
@@ -67718,7 +67876,11 @@ class SessionManager extends EventEmitter4 {
67718
67876
  getClaudeAccount: (id) => this.accountPool.get(id),
67719
67877
  releaseClaudeAccount: (id) => this.accountPool.release(id),
67720
67878
  markClaudeAccountCooling: (id, untilMs) => this.accountPool.markCooling(id, untilMs),
67721
- getClaudeAccountPoolStatus: () => this.accountPool.status()
67879
+ getClaudeAccountPoolStatus: () => this.accountPool.status(),
67880
+ getPlatformOverhead: (pid) => this.platformOverhead.get(pid) ?? {
67881
+ sessionHeader: DEFAULT_OVERHEAD_VISIBILITY,
67882
+ stickyMessage: DEFAULT_OVERHEAD_VISIBILITY
67883
+ }
67722
67884
  };
67723
67885
  return createSessionContext(config, state, ops);
67724
67886
  }
@@ -67878,7 +68040,8 @@ class SessionManager extends EventEmitter4 {
67878
68040
  pullRequestUrl: session.pullRequestUrl,
67879
68041
  messageCount: session.messageCount,
67880
68042
  resumeFailCount: session.lifecycle.resumeFailCount,
67881
- claudeAccountId: session.claudeAccountId
68043
+ claudeAccountId: session.claudeAccountId,
68044
+ sessionHeaderMode: session.sessionHeaderMode
67882
68045
  };
67883
68046
  this.sessionStore.save(session.sessionId, state);
67884
68047
  }
@@ -67895,6 +68058,10 @@ class SessionManager extends EventEmitter4 {
67895
68058
  });
67896
68059
  }
67897
68060
  async updateStickyMessage() {
68061
+ const overheadByPlatform = new Map;
68062
+ for (const [platformId, overhead] of this.platformOverhead) {
68063
+ overheadByPlatform.set(platformId, overhead.stickyMessage);
68064
+ }
67898
68065
  await updateAllStickyMessages(this.platforms, this.registry.getSessions(), {
67899
68066
  maxSessions: this.limits.maxSessions,
67900
68067
  chromeEnabled: this.chromeEnabled,
@@ -67905,7 +68072,7 @@ class SessionManager extends EventEmitter4 {
67905
68072
  description: this.customDescription,
67906
68073
  footer: this.customFooter,
67907
68074
  accountPoolStatus: this.accountPool.isEmpty ? undefined : this.accountPool.status()
67908
- });
68075
+ }, overheadByPlatform);
67909
68076
  }
67910
68077
  async updateAllStickyMessages() {
67911
68078
  await this.updateStickyMessage();
@@ -79149,7 +79316,7 @@ function wirePlatformEvents(platformId, client, session, ui) {
79149
79316
  ui.addLog({ level: "error", component: platformId, message });
79150
79317
  });
79151
79318
  }
79152
- program.name("claude-threads").version(VERSION).description("Share Claude Code sessions in Mattermost").option("--url <url>", "Mattermost server URL").option("--token <token>", "Mattermost bot token").option("--channel <id>", "Mattermost channel ID").option("--bot-name <name>", "Bot mention name (default: claude-code)").option("--allowed-users <users>", "Comma-separated allowed usernames").option("--permission-mode <mode>", "Permission mode: default | auto | bypass (default: from config)").option("--skip-permissions", "[deprecated] Alias for --permission-mode bypass").option("--no-skip-permissions", "[deprecated] Alias for --permission-mode default").option("--chrome", "Enable Claude in Chrome integration").option("--no-chrome", "Disable Claude in Chrome integration").option("--worktree-mode <mode>", "Git worktree mode: off, prompt, require (default: prompt)").option("--keep-alive", "Enable system sleep prevention (default: enabled)").option("--no-keep-alive", "Disable system sleep prevention").option("--setup", "Run interactive setup wizard (reconfigure existing settings)").option("--debug", "Enable debug logging").option("--skip-version-check", "Skip Claude CLI version compatibility check").option("--auto-restart", "Enable auto-restart on updates (default when autoUpdate enabled)").option("--no-auto-restart", "Disable auto-restart on updates").option("--headless", "Run without interactive UI (logs to stdout)").parse();
79319
+ program.name("claude-threads").version(VERSION).description("Share Claude Code sessions in Mattermost").option("--url <url>", "Mattermost server URL").option("--token <token>", "Mattermost bot token").option("--channel <id>", "Mattermost channel ID").option("--bot-name <name>", "Bot mention name (default: claude-code)").option("--allowed-users <users>", "Comma-separated allowed usernames").option("--permission-mode <mode>", "Permission mode: default | auto | bypass (default: from config)").option("--skip-permissions", "[deprecated] Alias for --permission-mode bypass").option("--no-skip-permissions", "[deprecated] Alias for --permission-mode default").option("--chrome", "Enable Claude in Chrome integration").option("--no-chrome", "Disable Claude in Chrome integration").option("--worktree-mode <mode>", "Git worktree mode: off, prompt, require (default: prompt)").option("--session-header <mode>", "Per-thread session header: full | minimal | hidden. Overrides per-platform config.").option("--sticky-message <mode>", "Channel sticky message: full | minimal | hidden. Overrides per-platform config.").option("--keep-alive", "Enable system sleep prevention (default: enabled)").option("--no-keep-alive", "Disable system sleep prevention").option("--setup", "Run interactive setup wizard (reconfigure existing settings)").option("--debug", "Enable debug logging").option("--skip-version-check", "Skip Claude CLI version compatibility check").option("--auto-restart", "Enable auto-restart on updates (default when autoUpdate enabled)").option("--no-auto-restart", "Disable auto-restart on updates").option("--headless", "Run without interactive UI (logs to stdout)").parse();
79153
79320
  var opts = program.opts();
79154
79321
  var forcedInteractive = !!process.env.CLAUDE_THREADS_INTERACTIVE;
79155
79322
  var isHeadless = opts.headless || !forcedInteractive && (!process.stdout.isTTY || !process.stdin.isTTY);
@@ -79237,6 +79404,14 @@ async function startWithoutDaemon() {
79237
79404
  console.error(red(` ❌ Invalid --permission-mode: "${opts.permissionMode}". Must be one of: default, auto, bypass.`));
79238
79405
  process.exit(1);
79239
79406
  }
79407
+ if (opts.sessionHeader !== undefined && !isOverheadVisibility(opts.sessionHeader)) {
79408
+ console.error(red(` ❌ Invalid --session-header: "${opts.sessionHeader}". Must be one of: ${OVERHEAD_VISIBILITY_VALUES.join(", ")}.`));
79409
+ process.exit(1);
79410
+ }
79411
+ if (opts.stickyMessage !== undefined && !isOverheadVisibility(opts.stickyMessage)) {
79412
+ console.error(red(` ❌ Invalid --sticky-message: "${opts.stickyMessage}". Must be one of: ${OVERHEAD_VISIBILITY_VALUES.join(", ")}.`));
79413
+ process.exit(1);
79414
+ }
79240
79415
  const cliArgs = {
79241
79416
  url: opts.url,
79242
79417
  token: opts.token,
@@ -79247,7 +79422,9 @@ async function startWithoutDaemon() {
79247
79422
  permissionMode: opts.permissionMode,
79248
79423
  chrome: opts.chrome,
79249
79424
  worktreeMode: opts.worktreeMode,
79250
- keepAlive: opts.keepAlive
79425
+ keepAlive: opts.keepAlive,
79426
+ sessionHeader: opts.sessionHeader,
79427
+ stickyMessage: opts.stickyMessage
79251
79428
  };
79252
79429
  if (opts.setup) {
79253
79430
  await runOnboarding(true);
@@ -79268,6 +79445,16 @@ async function startWithoutDaemon() {
79268
79445
  if (cliArgs.keepAlive !== undefined) {
79269
79446
  newConfig.keepAlive = cliArgs.keepAlive;
79270
79447
  }
79448
+ if (cliArgs.sessionHeader !== undefined) {
79449
+ for (const p of newConfig.platforms) {
79450
+ p.sessionHeader = cliArgs.sessionHeader;
79451
+ }
79452
+ }
79453
+ if (cliArgs.stickyMessage !== undefined) {
79454
+ for (const p of newConfig.platforms) {
79455
+ p.stickyMessage = cliArgs.stickyMessage;
79456
+ }
79457
+ }
79271
79458
  const keepAliveEnabled = newConfig.keepAlive !== false;
79272
79459
  if (!newConfig.platforms || newConfig.platforms.length === 0) {
79273
79460
  throw new Error("No platforms configured. Run with --setup to configure.");
@@ -79441,7 +79628,10 @@ async function startWithoutDaemon() {
79441
79628
  });
79442
79629
  const client = createPlatformClient(platformConfig);
79443
79630
  platforms.set(platformConfig.id, client);
79444
- session.addPlatform(platformConfig.id, client);
79631
+ session.addPlatform(platformConfig.id, client, {
79632
+ sessionHeader: resolveOverheadVisibility(platformConfig.sessionHeader, `platforms[${platformConfig.id}].sessionHeader`),
79633
+ stickyMessage: resolveOverheadVisibility(platformConfig.stickyMessage, `platforms[${platformConfig.id}].stickyMessage`)
79634
+ });
79445
79635
  wirePlatformEvents(platformConfig.id, client, session, ui);
79446
79636
  }
79447
79637
  const enabledPlatforms = Array.from(platforms.entries()).filter(([id]) => platformEnabledState.get(id) ?? true);
@@ -3035,6 +3035,9 @@ var require_data = __commonJS((exports, module) => {
3035
3035
  var require_utils = __commonJS((exports, module) => {
3036
3036
  var isUUID = RegExp.prototype.test.bind(/^[\da-f]{8}-[\da-f]{4}-[\da-f]{4}-[\da-f]{4}-[\da-f]{12}$/iu);
3037
3037
  var isIPv4 = RegExp.prototype.test.bind(/^(?:(?:25[0-5]|2[0-4]\d|1\d{2}|[1-9]\d|\d)\.){3}(?:25[0-5]|2[0-4]\d|1\d{2}|[1-9]\d|\d)$/u);
3038
+ var isHexPair = RegExp.prototype.test.bind(/^[\da-f]{2}$/iu);
3039
+ var isUnreserved = RegExp.prototype.test.bind(/^[\da-z\-._~]$/iu);
3040
+ var isPathCharacter = RegExp.prototype.test.bind(/^[\da-z\-._~!$&'()*+,;=:@/]$/iu);
3038
3041
  function stringArrayToHexStripped(input) {
3039
3042
  let acc = "";
3040
3043
  let code = 0;
@@ -3228,27 +3231,77 @@ var require_utils = __commonJS((exports, module) => {
3228
3231
  }
3229
3232
  return output.join("");
3230
3233
  }
3231
- function normalizeComponentEncoding(component, esc2) {
3232
- const func = esc2 !== true ? escape : unescape;
3233
- if (component.scheme !== undefined) {
3234
- component.scheme = func(component.scheme);
3235
- }
3236
- if (component.userinfo !== undefined) {
3237
- component.userinfo = func(component.userinfo);
3238
- }
3239
- if (component.host !== undefined) {
3240
- component.host = func(component.host);
3234
+ var HOST_DELIMS = { "@": "%40", "/": "%2F", "?": "%3F", "#": "%23", ":": "%3A" };
3235
+ var HOST_DELIM_RE = /[@/?#:]/g;
3236
+ var HOST_DELIM_NO_COLON_RE = /[@/?#]/g;
3237
+ function reescapeHostDelimiters(host, isIP) {
3238
+ const re = isIP ? HOST_DELIM_NO_COLON_RE : HOST_DELIM_RE;
3239
+ re.lastIndex = 0;
3240
+ return host.replace(re, (ch) => HOST_DELIMS[ch]);
3241
+ }
3242
+ function normalizePercentEncoding(input, decodeUnreserved = false) {
3243
+ if (input.indexOf("%") === -1) {
3244
+ return input;
3241
3245
  }
3242
- if (component.path !== undefined) {
3243
- component.path = func(component.path);
3246
+ let output = "";
3247
+ for (let i = 0;i < input.length; i++) {
3248
+ if (input[i] === "%" && i + 2 < input.length) {
3249
+ const hex = input.slice(i + 1, i + 3);
3250
+ if (isHexPair(hex)) {
3251
+ const normalizedHex = hex.toUpperCase();
3252
+ const decoded = String.fromCharCode(parseInt(normalizedHex, 16));
3253
+ if (decodeUnreserved && isUnreserved(decoded)) {
3254
+ output += decoded;
3255
+ } else {
3256
+ output += "%" + normalizedHex;
3257
+ }
3258
+ i += 2;
3259
+ continue;
3260
+ }
3261
+ }
3262
+ output += input[i];
3244
3263
  }
3245
- if (component.query !== undefined) {
3246
- component.query = func(component.query);
3264
+ return output;
3265
+ }
3266
+ function normalizePathEncoding(input) {
3267
+ let output = "";
3268
+ for (let i = 0;i < input.length; i++) {
3269
+ if (input[i] === "%" && i + 2 < input.length) {
3270
+ const hex = input.slice(i + 1, i + 3);
3271
+ if (isHexPair(hex)) {
3272
+ const normalizedHex = hex.toUpperCase();
3273
+ const decoded = String.fromCharCode(parseInt(normalizedHex, 16));
3274
+ if (decoded !== "." && isUnreserved(decoded)) {
3275
+ output += decoded;
3276
+ } else {
3277
+ output += "%" + normalizedHex;
3278
+ }
3279
+ i += 2;
3280
+ continue;
3281
+ }
3282
+ }
3283
+ if (isPathCharacter(input[i])) {
3284
+ output += input[i];
3285
+ } else {
3286
+ output += escape(input[i]);
3287
+ }
3247
3288
  }
3248
- if (component.fragment !== undefined) {
3249
- component.fragment = func(component.fragment);
3289
+ return output;
3290
+ }
3291
+ function escapePreservingEscapes(input) {
3292
+ let output = "";
3293
+ for (let i = 0;i < input.length; i++) {
3294
+ if (input[i] === "%" && i + 2 < input.length) {
3295
+ const hex = input.slice(i + 1, i + 3);
3296
+ if (isHexPair(hex)) {
3297
+ output += "%" + hex.toUpperCase();
3298
+ i += 2;
3299
+ continue;
3300
+ }
3301
+ }
3302
+ output += escape(input[i]);
3250
3303
  }
3251
- return component;
3304
+ return output;
3252
3305
  }
3253
3306
  function recomposeAuthority(component) {
3254
3307
  const uriTokens = [];
@@ -3263,7 +3316,7 @@ var require_utils = __commonJS((exports, module) => {
3263
3316
  if (ipV6res.isIPV6 === true) {
3264
3317
  host = `[${ipV6res.escapedHost}]`;
3265
3318
  } else {
3266
- host = component.host;
3319
+ host = reescapeHostDelimiters(host, false);
3267
3320
  }
3268
3321
  }
3269
3322
  uriTokens.push(host);
@@ -3277,7 +3330,10 @@ var require_utils = __commonJS((exports, module) => {
3277
3330
  module.exports = {
3278
3331
  nonSimpleDomain,
3279
3332
  recomposeAuthority,
3280
- normalizeComponentEncoding,
3333
+ reescapeHostDelimiters,
3334
+ normalizePercentEncoding,
3335
+ normalizePathEncoding,
3336
+ escapePreservingEscapes,
3281
3337
  removeDotSegments,
3282
3338
  isIPv4,
3283
3339
  isUUID,
@@ -3462,11 +3518,11 @@ var require_schemes = __commonJS((exports, module) => {
3462
3518
 
3463
3519
  // node_modules/fast-uri/index.js
3464
3520
  var require_fast_uri = __commonJS((exports, module) => {
3465
- var { normalizeIPv6, removeDotSegments, recomposeAuthority, normalizeComponentEncoding, isIPv4, nonSimpleDomain } = require_utils();
3521
+ var { normalizeIPv6, removeDotSegments, recomposeAuthority, normalizePercentEncoding, normalizePathEncoding, escapePreservingEscapes, reescapeHostDelimiters, isIPv4, nonSimpleDomain } = require_utils();
3466
3522
  var { SCHEMES, getSchemeHandler } = require_schemes();
3467
3523
  function normalize(uri, options) {
3468
3524
  if (typeof uri === "string") {
3469
- uri = serialize(parse6(uri, options), options);
3525
+ uri = normalizeString(uri, options);
3470
3526
  } else if (typeof uri === "object") {
3471
3527
  uri = parse6(serialize(uri, options), options);
3472
3528
  }
@@ -3532,19 +3588,9 @@ var require_fast_uri = __commonJS((exports, module) => {
3532
3588
  return target;
3533
3589
  }
3534
3590
  function equal(uriA, uriB, options) {
3535
- if (typeof uriA === "string") {
3536
- uriA = unescape(uriA);
3537
- uriA = serialize(normalizeComponentEncoding(parse6(uriA, options), true), { ...options, skipEscape: true });
3538
- } else if (typeof uriA === "object") {
3539
- uriA = serialize(normalizeComponentEncoding(uriA, true), { ...options, skipEscape: true });
3540
- }
3541
- if (typeof uriB === "string") {
3542
- uriB = unescape(uriB);
3543
- uriB = serialize(normalizeComponentEncoding(parse6(uriB, options), true), { ...options, skipEscape: true });
3544
- } else if (typeof uriB === "object") {
3545
- uriB = serialize(normalizeComponentEncoding(uriB, true), { ...options, skipEscape: true });
3546
- }
3547
- return uriA.toLowerCase() === uriB.toLowerCase();
3591
+ const normalizedA = normalizeComparableURI(uriA, options);
3592
+ const normalizedB = normalizeComparableURI(uriB, options);
3593
+ return normalizedA !== undefined && normalizedB !== undefined && normalizedA.toLowerCase() === normalizedB.toLowerCase();
3548
3594
  }
3549
3595
  function serialize(cmpts, opts) {
3550
3596
  const component = {
@@ -3570,12 +3616,12 @@ var require_fast_uri = __commonJS((exports, module) => {
3570
3616
  schemeHandler.serialize(component, options);
3571
3617
  if (component.path !== undefined) {
3572
3618
  if (!options.skipEscape) {
3573
- component.path = escape(component.path);
3619
+ component.path = escapePreservingEscapes(component.path);
3574
3620
  if (component.scheme !== undefined) {
3575
3621
  component.path = component.path.split("%3A").join(":");
3576
3622
  }
3577
3623
  } else {
3578
- component.path = unescape(component.path);
3624
+ component.path = normalizePercentEncoding(component.path);
3579
3625
  }
3580
3626
  }
3581
3627
  if (options.reference !== "suffix" && component.scheme) {
@@ -3610,7 +3656,16 @@ var require_fast_uri = __commonJS((exports, module) => {
3610
3656
  return uriTokens.join("");
3611
3657
  }
3612
3658
  var URI_PARSE = /^(?:([^#/:?]+):)?(?:\/\/((?:([^#/?@]*)@)?(\[[^#/?\]]+\]|[^#/:?]*)(?::(\d*))?))?([^#?]*)(?:\?([^#]*))?(?:#((?:.|[\n\r])*))?/u;
3613
- function parse6(uri, opts) {
3659
+ function getParseError(parsed, matches) {
3660
+ if (matches[2] !== undefined && parsed.path && parsed.path[0] !== "/") {
3661
+ return 'URI path must start with "/" when authority is present.';
3662
+ }
3663
+ if (typeof parsed.port === "number" && (parsed.port < 0 || parsed.port > 65535)) {
3664
+ return "URI port is malformed.";
3665
+ }
3666
+ return;
3667
+ }
3668
+ function parseWithStatus(uri, opts) {
3614
3669
  const options = Object.assign({}, opts);
3615
3670
  const parsed = {
3616
3671
  scheme: undefined,
@@ -3621,6 +3676,7 @@ var require_fast_uri = __commonJS((exports, module) => {
3621
3676
  query: undefined,
3622
3677
  fragment: undefined
3623
3678
  };
3679
+ let malformedAuthorityOrPort = false;
3624
3680
  let isIP = false;
3625
3681
  if (options.reference === "suffix") {
3626
3682
  if (options.scheme) {
@@ -3641,6 +3697,11 @@ var require_fast_uri = __commonJS((exports, module) => {
3641
3697
  if (isNaN(parsed.port)) {
3642
3698
  parsed.port = matches[5];
3643
3699
  }
3700
+ const parseError = getParseError(parsed, matches);
3701
+ if (parseError !== undefined) {
3702
+ parsed.error = parsed.error || parseError;
3703
+ malformedAuthorityOrPort = true;
3704
+ }
3644
3705
  if (parsed.host) {
3645
3706
  const ipv4result = isIPv4(parsed.host);
3646
3707
  if (ipv4result === false) {
@@ -3679,14 +3740,18 @@ var require_fast_uri = __commonJS((exports, module) => {
3679
3740
  parsed.scheme = unescape(parsed.scheme);
3680
3741
  }
3681
3742
  if (parsed.host !== undefined) {
3682
- parsed.host = unescape(parsed.host);
3743
+ parsed.host = reescapeHostDelimiters(unescape(parsed.host), isIP);
3683
3744
  }
3684
3745
  }
3685
3746
  if (parsed.path) {
3686
- parsed.path = escape(unescape(parsed.path));
3747
+ parsed.path = normalizePathEncoding(parsed.path);
3687
3748
  }
3688
3749
  if (parsed.fragment) {
3689
- parsed.fragment = encodeURI(decodeURIComponent(parsed.fragment));
3750
+ try {
3751
+ parsed.fragment = encodeURI(decodeURIComponent(parsed.fragment));
3752
+ } catch {
3753
+ parsed.error = parsed.error || "URI malformed";
3754
+ }
3690
3755
  }
3691
3756
  }
3692
3757
  if (schemeHandler && schemeHandler.parse) {
@@ -3695,7 +3760,29 @@ var require_fast_uri = __commonJS((exports, module) => {
3695
3760
  } else {
3696
3761
  parsed.error = parsed.error || "URI can not be parsed.";
3697
3762
  }
3698
- return parsed;
3763
+ return { parsed, malformedAuthorityOrPort };
3764
+ }
3765
+ function parse6(uri, opts) {
3766
+ return parseWithStatus(uri, opts).parsed;
3767
+ }
3768
+ function normalizeString(uri, opts) {
3769
+ return normalizeStringWithStatus(uri, opts).normalized;
3770
+ }
3771
+ function normalizeStringWithStatus(uri, opts) {
3772
+ const { parsed, malformedAuthorityOrPort } = parseWithStatus(uri, opts);
3773
+ return {
3774
+ normalized: malformedAuthorityOrPort ? uri : serialize(parsed, opts),
3775
+ malformedAuthorityOrPort
3776
+ };
3777
+ }
3778
+ function normalizeComparableURI(uri, opts) {
3779
+ if (typeof uri === "string") {
3780
+ const { normalized, malformedAuthorityOrPort } = normalizeStringWithStatus(uri, opts);
3781
+ return malformedAuthorityOrPort ? undefined : normalized;
3782
+ }
3783
+ if (typeof uri === "object") {
3784
+ return serialize(uri, opts);
3785
+ }
3699
3786
  }
3700
3787
  var fastUri = {
3701
3788
  SCHEMES,
@@ -54449,6 +54536,7 @@ var pausedPlatforms = new Map;
54449
54536
  var lastCleanupTime = new Map;
54450
54537
  var CLEANUP_THROTTLE_MS = 5 * 60 * 1000;
54451
54538
  var CLEANUP_MAX_AGE_MS = 60 * 60 * 1000;
54539
+ var hiddenCleanupDone = new Set;
54452
54540
  // node_modules/@redactpii/node/lib/index.mjs
54453
54541
  class Redactor {
54454
54542
  apiKey;
@@ -57,6 +57,8 @@ platforms:
57
57
  | `botName` | No | Mention name (default: `claude-code`) |
58
58
  | `allowedUsers` | No | List of usernames who can use the bot |
59
59
  | `skipPermissions` | No | Auto-approve actions (default: `false`) |
60
+ | `sessionHeader` | No | Per-thread header visibility: `full` (default) / `minimal` (status bar only) / `hidden` (no header post) |
61
+ | `stickyMessage` | No | Channel sticky visibility: `full` (default) / `minimal` (status bar only) / `hidden` (no sticky, no bumping) |
60
62
 
61
63
  ### Slack
62
64
 
@@ -71,6 +73,23 @@ platforms:
71
73
  | `botName` | No | Mention name (default: `claude`) |
72
74
  | `allowedUsers` | No | List of Slack usernames |
73
75
  | `skipPermissions` | No | Auto-approve actions (default: `false`) |
76
+ | `sessionHeader` | No | Per-thread header visibility: `full` (default) / `minimal` (status bar only) / `hidden` (no header post) |
77
+ | `stickyMessage` | No | Channel sticky visibility: `full` (default) / `minimal` (status bar only) / `hidden` (no sticky, no bumping) |
78
+
79
+ ### Quieting the bot's overhead messages
80
+
81
+ Both the per-thread session header and the channel sticky message default to `full` for backward compatibility. To strip them down on a noisy channel, set the per-platform fields in `config.yaml`:
82
+
83
+ ```yaml
84
+ platforms:
85
+ - id: mattermost-main
86
+ type: mattermost
87
+ # ... credentials ...
88
+ sessionHeader: hidden # no header post — Claude's reply is the first message in the thread
89
+ stickyMessage: minimal # one-line status bar at the channel bottom, no sessions list
90
+ ```
91
+
92
+ Note: the per-platform `stickyMessage: <mode>` field is distinct from the top-level `Config.stickyMessage: { description, footer }` block, which still customizes the full sticky for platforms not in `hidden` mode.
74
93
 
75
94
  ## Claude Accounts (optional, multi-account mode)
76
95
 
@@ -139,6 +158,8 @@ Options:
139
158
  --chrome Enable Chrome integration
140
159
  --no-chrome Disable Chrome integration
141
160
  --worktree-mode <mode> Git worktree mode: off, prompt, require
161
+ --session-header <mode> Per-thread header: full | minimal | hidden (overrides per-platform config)
162
+ --sticky-message <mode> Channel sticky: full | minimal | hidden (overrides per-platform config)
142
163
  --setup Re-run setup wizard
143
164
  --debug Enable debug logging
144
165
  --version Show version
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "claude-threads",
3
- "version": "1.15.1",
3
+ "version": "1.16.0",
4
4
  "description": "Share Claude Code sessions live in a Mattermost channel with interactive features",
5
5
  "main": "dist/index.js",
6
6
  "type": "module",
@@ -71,7 +71,7 @@
71
71
  "commander": "^14.0.2",
72
72
  "diff": "^8.0.3",
73
73
  "express-rate-limit": "^8.3.0",
74
- "hono": "^4.12.5",
74
+ "hono": "^4.12.18",
75
75
  "ink": "^6.6.0",
76
76
  "ink-scroll-view": "^0.3.5",
77
77
  "js-yaml": "^4.1.1",
@@ -94,7 +94,7 @@
94
94
  "@types/ws": "^8.18.0",
95
95
  "eslint": "^10.1.0",
96
96
  "husky": "^9.1.7",
97
- "lint-staged": "^16.2.7",
97
+ "lint-staged": "^17.0.4",
98
98
  "prettier": "^3.4.2",
99
99
  "typescript": "^6.0.2",
100
100
  "typescript-eslint": "^8.57.2"
@@ -115,7 +115,8 @@
115
115
  "express-rate-limit": "$express-rate-limit",
116
116
  "flatted": ">=3.4.0",
117
117
  "picomatch": ">=2.3.2",
118
- "path-to-regexp": ">=8.4.0"
118
+ "path-to-regexp": ">=8.4.0",
119
+ "fast-uri": ">=3.1.2"
119
120
  },
120
121
  "resolutions": {
121
122
  "hono": "$hono",
@@ -123,6 +124,7 @@
123
124
  "express-rate-limit": "$express-rate-limit",
124
125
  "flatted": ">=3.4.0",
125
126
  "picomatch": ">=2.3.2",
126
- "path-to-regexp": ">=8.4.0"
127
+ "path-to-regexp": ">=8.4.0",
128
+ "fast-uri": ">=3.1.2"
127
129
  }
128
130
  }