claude-threads 1.13.0 → 1.14.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,16 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
7
7
 
8
8
  ## [Unreleased]
9
9
 
10
+ ## [1.14.0] - 2026-05-05
11
+
12
+ ### Changed
13
+ - **`read_post` allows cross-channel reads on public Mattermost channels.** The cross-channel guard kept rejecting permalinks that pointed outside the bot's session channel — appropriate for private/DM/group channels (real privacy concern: a thread participant without access to the source channel sees content they shouldn't), but overzealous for public channels where anyone with an account can already navigate to the post. `McpPost` now carries a `channelType?: 'public' | 'private'` field, populated for Mattermost via `/channels/{id}` (type `O` = public; `P`/`D`/`G` = private) with a per-process cache so chatty threads don't trigger N redundant lookups. The resolver skips the wrong-channel check when `channelType === 'public'`; missing `channelType` is treated as private (fail-safe). Slack is unchanged: its MCP-side `readPost` is hard-scoped to the bot's configured channel via `conversations.history`, so cross-channel reads aren't possible there regardless of channel visibility. (#369)
14
+
15
+ ## [1.13.1] - 2026-05-05
16
+
17
+ ### Fixed
18
+ - **`read_post` works on Mattermost subpath installs.** `parseMattermostPermalink` matched only on origin and ignored the configured baseUrl path, so on a Mattermost install at `/chat` (e.g. `digilab.overheid.nl/chat`) every permalink had a leading `/chat` segment that pushed the path to four segments and got rejected with "not a Mattermost permalink for ..." even though the link was on the bot's own instance. The parser now strips the configured subpath as a path-segment prefix before validating the `{team}/pl/{id}` or `_redirect/pl/{id}` shape; segment-level comparison so `/chat` doesn't accidentally match `/chatter`. Regression from #366. (#368)
19
+
10
20
  ## [1.13.0] - 2026-05-05
11
21
 
12
22
  ### Added
package/dist/index.js CHANGED
@@ -51783,6 +51783,14 @@ async function getPostRaw(config, postId) {
51783
51783
  return null;
51784
51784
  }
51785
51785
  }
51786
+ async function getChannelRaw(config, channelId) {
51787
+ try {
51788
+ return await mattermostApi(config, "GET", `/channels/${channelId}`);
51789
+ } catch (err) {
51790
+ apiLog.debug(`Failed to get channel ${channelId}: ${err}`);
51791
+ return null;
51792
+ }
51793
+ }
51786
51794
  async function getThreadRaw(config, threadRootId) {
51787
51795
  try {
51788
51796
  return await mattermostApi(config, "GET", `/posts/${threadRootId}/thread`);
@@ -51820,6 +51828,7 @@ class MattermostMcpPlatformApi {
51820
51828
  config;
51821
51829
  formatter = new MattermostFormatter;
51822
51830
  botUserIdCache = null;
51831
+ channelTypeCache = new Map;
51823
51832
  constructor(config) {
51824
51833
  this.config = config;
51825
51834
  this.apiConfig = {
@@ -51960,7 +51969,19 @@ class MattermostMcpPlatformApi {
51960
51969
  if (!post)
51961
51970
  return null;
51962
51971
  const username = post.user_id ? await this.getUsername(post.user_id) : null;
51963
- return toMcpPost(post, username);
51972
+ const channelType = await this.getChannelType(post.channel_id);
51973
+ return toMcpPost(post, username, channelType);
51974
+ }
51975
+ async getChannelType(channelId) {
51976
+ const cached = this.channelTypeCache.get(channelId);
51977
+ if (cached)
51978
+ return cached;
51979
+ const channel = await getChannelRaw(this.apiConfig, channelId);
51980
+ if (!channel)
51981
+ return;
51982
+ const visibility = channel.type === "O" ? "public" : "private";
51983
+ this.channelTypeCache.set(channelId, visibility);
51984
+ return visibility;
51964
51985
  }
51965
51986
  async readThread(threadRootId, options) {
51966
51987
  mcpLogger.debug(`readThread: ${formatShortId(threadRootId)}`);
@@ -51975,10 +51996,11 @@ class MattermostMcpPlatformApi {
51975
51996
  usernameByUserId.set(p.user_id, await this.getUsername(p.user_id));
51976
51997
  }
51977
51998
  }
51978
- return limited.map((p) => toMcpPost(p, p.user_id ? usernameByUserId.get(p.user_id) ?? null : null));
51999
+ const channelType = limited[0] ? await this.getChannelType(limited[0].channel_id) : undefined;
52000
+ return limited.map((p) => toMcpPost(p, p.user_id ? usernameByUserId.get(p.user_id) ?? null : null, channelType));
51979
52001
  }
51980
52002
  }
51981
- function toMcpPost(post, username) {
52003
+ function toMcpPost(post, username, channelType) {
51982
52004
  return {
51983
52005
  id: post.id,
51984
52006
  channelId: post.channel_id,
@@ -51986,7 +52008,8 @@ function toMcpPost(post, username) {
51986
52008
  username,
51987
52009
  message: post.message,
51988
52010
  createAt: post.create_at ?? 0,
51989
- threadRootId: post.root_id || undefined
52011
+ threadRootId: post.root_id || undefined,
52012
+ channelType
51990
52013
  };
51991
52014
  }
51992
52015
 
@@ -56086,6 +56086,14 @@ async function getPostRaw(config3, postId) {
56086
56086
  return null;
56087
56087
  }
56088
56088
  }
56089
+ async function getChannelRaw(config3, channelId) {
56090
+ try {
56091
+ return await mattermostApi(config3, "GET", `/channels/${channelId}`);
56092
+ } catch (err) {
56093
+ apiLog.debug(`Failed to get channel ${channelId}: ${err}`);
56094
+ return null;
56095
+ }
56096
+ }
56089
56097
  async function getThreadRaw(config3, threadRootId) {
56090
56098
  try {
56091
56099
  return await mattermostApi(config3, "GET", `/posts/${threadRootId}/thread`);
@@ -56123,6 +56131,7 @@ class MattermostMcpPlatformApi {
56123
56131
  config;
56124
56132
  formatter = new MattermostFormatter;
56125
56133
  botUserIdCache = null;
56134
+ channelTypeCache = new Map;
56126
56135
  constructor(config3) {
56127
56136
  this.config = config3;
56128
56137
  this.apiConfig = {
@@ -56263,7 +56272,19 @@ class MattermostMcpPlatformApi {
56263
56272
  if (!post2)
56264
56273
  return null;
56265
56274
  const username = post2.user_id ? await this.getUsername(post2.user_id) : null;
56266
- return toMcpPost(post2, username);
56275
+ const channelType = await this.getChannelType(post2.channel_id);
56276
+ return toMcpPost(post2, username, channelType);
56277
+ }
56278
+ async getChannelType(channelId) {
56279
+ const cached3 = this.channelTypeCache.get(channelId);
56280
+ if (cached3)
56281
+ return cached3;
56282
+ const channel = await getChannelRaw(this.apiConfig, channelId);
56283
+ if (!channel)
56284
+ return;
56285
+ const visibility = channel.type === "O" ? "public" : "private";
56286
+ this.channelTypeCache.set(channelId, visibility);
56287
+ return visibility;
56267
56288
  }
56268
56289
  async readThread(threadRootId, options) {
56269
56290
  mcpLogger.debug(`readThread: ${formatShortId(threadRootId)}`);
@@ -56278,10 +56299,11 @@ class MattermostMcpPlatformApi {
56278
56299
  usernameByUserId.set(p.user_id, await this.getUsername(p.user_id));
56279
56300
  }
56280
56301
  }
56281
- return limited.map((p) => toMcpPost(p, p.user_id ? usernameByUserId.get(p.user_id) ?? null : null));
56302
+ const channelType = limited[0] ? await this.getChannelType(limited[0].channel_id) : undefined;
56303
+ return limited.map((p) => toMcpPost(p, p.user_id ? usernameByUserId.get(p.user_id) ?? null : null, channelType));
56282
56304
  }
56283
56305
  }
56284
- function toMcpPost(post2, username) {
56306
+ function toMcpPost(post2, username, channelType) {
56285
56307
  return {
56286
56308
  id: post2.id,
56287
56309
  channelId: post2.channel_id,
@@ -56289,7 +56311,8 @@ function toMcpPost(post2, username) {
56289
56311
  username,
56290
56312
  message: post2.message,
56291
56313
  createAt: post2.create_at ?? 0,
56292
- threadRootId: post2.root_id || undefined
56314
+ threadRootId: post2.root_id || undefined,
56315
+ channelType
56293
56316
  };
56294
56317
  }
56295
56318
  function createMattermostMcpPlatformApi(config3) {
@@ -56868,7 +56891,13 @@ function parseMattermostPermalink(url2, baseUrl) {
56868
56891
  if (parsed.origin !== base.origin) {
56869
56892
  return null;
56870
56893
  }
56871
- const segments = parsed.pathname.replace(/^\/+|\/+$/g, "").split("/");
56894
+ const baseSegments = base.pathname.replace(/^\/+|\/+$/g, "").split("/").filter(Boolean);
56895
+ const allSegments = parsed.pathname.replace(/^\/+|\/+$/g, "").split("/");
56896
+ for (let i2 = 0;i2 < baseSegments.length; i2++) {
56897
+ if (allSegments[i2] !== baseSegments[i2])
56898
+ return null;
56899
+ }
56900
+ const segments = allSegments.slice(baseSegments.length);
56872
56901
  if (segments.length !== 3)
56873
56902
  return null;
56874
56903
  if (segments[1] !== "pl")
@@ -56890,7 +56919,7 @@ async function resolvePermalink(api3, postId, botChannelId, opts = {}) {
56890
56919
  if (!post2) {
56891
56920
  return { ok: false, error: { kind: "not-found" } };
56892
56921
  }
56893
- if (botChannelId !== undefined && post2.channelId !== botChannelId) {
56922
+ if (botChannelId !== undefined && post2.channelId !== botChannelId && post2.channelType !== "public") {
56894
56923
  return { ok: false, error: { kind: "wrong-channel" } };
56895
56924
  }
56896
56925
  if (!opts.includeThread) {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "claude-threads",
3
- "version": "1.13.0",
3
+ "version": "1.14.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",