claude-threads 1.11.0 → 1.12.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.
@@ -50934,6 +50934,26 @@ function createMessageManagerEvents() {
50934
50934
  return new TypedEventEmitter;
50935
50935
  }
50936
50936
 
50937
+ // src/utils/safe-filename.ts
50938
+ import { basename } from "path";
50939
+ function sanitizeFilename(name) {
50940
+ const flat = basename(name.replace(/\\/g, "/"));
50941
+ const cleaned = flat.replace(/[\x00-\x1F\x7F]/g, "_").trim();
50942
+ if (!cleaned || cleaned === "." || cleaned === "..") {
50943
+ return "attachment";
50944
+ }
50945
+ return cleaned;
50946
+ }
50947
+ function formatBytes(bytes) {
50948
+ if (bytes < 1024)
50949
+ return `${bytes} B`;
50950
+ if (bytes < 1024 * 1024)
50951
+ return `${(bytes / 1024).toFixed(1)} KB`;
50952
+ if (bytes < 1024 * 1024 * 1024)
50953
+ return `${(bytes / 1024 / 1024).toFixed(1)} MB`;
50954
+ return `${(bytes / 1024 / 1024 / 1024).toFixed(2)} GB`;
50955
+ }
50956
+
50937
50957
  // src/operations/streaming/handler.ts
50938
50958
  var log2 = createLogger("streaming");
50939
50959
  var MAX_UPLOAD_SIZE = 100 * 1024 * 1024;
@@ -54702,6 +54722,14 @@ import { existsSync as existsSync4, readFileSync as readFileSync3, watchFile, un
54702
54722
  import { tmpdir } from "os";
54703
54723
  import { join as join3 } from "path";
54704
54724
 
54725
+ // src/mcp/outbound-env.ts
54726
+ var OUTBOUND_ENV = {
54727
+ SESSION_WORKING_DIR: "SESSION_WORKING_DIR",
54728
+ SESSION_UPLOAD_DIR: "SESSION_UPLOAD_DIR",
54729
+ OUTBOUND_FILES_ENABLED: "OUTBOUND_FILES_ENABLED",
54730
+ OUTBOUND_FILES_MAX_BYTES: "OUTBOUND_FILES_MAX_BYTES"
54731
+ };
54732
+
54705
54733
  // src/claude/rate-limit-detector.ts
54706
54734
  var RATE_LIMIT_PHRASES = [
54707
54735
  /usage limit reached/i,
@@ -54823,7 +54851,7 @@ function materializeMcpConfig(config3, sessionId, opts = {}) {
54823
54851
  }
54824
54852
  function buildPermissionArgs(opts) {
54825
54853
  const args = [];
54826
- if (opts.permissionMode === "bypass") {
54854
+ if (opts.permissionMode === "bypass" && !opts.platformConfig) {
54827
54855
  args.push("--dangerously-skip-permissions");
54828
54856
  return { args, tempFile: null };
54829
54857
  }
@@ -54843,6 +54871,18 @@ function buildPermissionArgs(opts) {
54843
54871
  if (opts.platformConfig.appToken) {
54844
54872
  mcpEnv.PLATFORM_APP_TOKEN = opts.platformConfig.appToken;
54845
54873
  }
54874
+ if (opts.workingDir) {
54875
+ mcpEnv[OUTBOUND_ENV.SESSION_WORKING_DIR] = opts.workingDir;
54876
+ }
54877
+ if (opts.uploadDir) {
54878
+ mcpEnv[OUTBOUND_ENV.SESSION_UPLOAD_DIR] = opts.uploadDir;
54879
+ }
54880
+ if (opts.outboundFiles?.enabled === false) {
54881
+ mcpEnv[OUTBOUND_ENV.OUTBOUND_FILES_ENABLED] = "0";
54882
+ }
54883
+ if (typeof opts.outboundFiles?.maxBytes === "number" && Number.isFinite(opts.outboundFiles.maxBytes) && opts.outboundFiles.maxBytes > 0) {
54884
+ mcpEnv[OUTBOUND_ENV.OUTBOUND_FILES_MAX_BYTES] = String(opts.outboundFiles.maxBytes);
54885
+ }
54846
54886
  const mcpConfig = {
54847
54887
  mcpServers: {
54848
54888
  "claude-threads-permissions": {
@@ -54861,9 +54901,13 @@ function buildPermissionArgs(opts) {
54861
54901
  } else {
54862
54902
  args.push("--mcp-config", materialized.value);
54863
54903
  }
54864
- args.push("--permission-prompt-tool", "mcp__claude-threads-permissions__permission_prompt");
54865
- if (opts.permissionMode === "auto") {
54866
- args.push("--permission-mode", "auto");
54904
+ if (opts.permissionMode === "bypass") {
54905
+ args.push("--dangerously-skip-permissions");
54906
+ } else {
54907
+ args.push("--permission-prompt-tool", "mcp__claude-threads-permissions__permission_prompt");
54908
+ if (opts.permissionMode === "auto") {
54909
+ args.push("--permission-mode", "auto");
54910
+ }
54867
54911
  }
54868
54912
  return { args, tempFile };
54869
54913
  }
@@ -54958,7 +55002,10 @@ class ClaudeCli extends EventEmitter2 {
54958
55002
  threadId: this.options.threadId,
54959
55003
  sessionId: this.options.sessionId,
54960
55004
  permissionTimeoutMs: this.options.permissionTimeoutMs ?? 120000,
54961
- debug: this.debug
55005
+ debug: this.debug,
55006
+ workingDir: this.options.workingDir,
55007
+ uploadDir: this.options.uploadDir,
55008
+ outboundFiles: this.options.outboundFiles
54962
55009
  });
54963
55010
  args.push(...permResult.args);
54964
55011
  this.mcpConfigTempFile = permResult.tempFile;
@@ -55730,6 +55777,15 @@ You are running inside a chat platform (like Mattermost or Slack). Users interac
55730
55777
  - Keep responses concise - very long responses are split across multiple messages
55731
55778
  - Multiple users may participate in a session (the owner can invite others)
55732
55779
 
55780
+ ## Sending files into THIS thread
55781
+ You are RIGHT NOW running inside a chat thread (Mattermost or Slack). The \`send_file\` MCP tool — exposed as \`mcp__claude-threads-permissions__send_file\` in your tool list — uploads a file from your working directory and posts it directly into THIS thread, where the user is talking to you. It is NOT a hypothetical capability that requires extra setup; it works for the session you are in right now.
55782
+
55783
+ Use it whenever the user asks to "send", "share", "show", or "post" a file, OR whenever you produce an artifact (screenshot, generated audio, plot, document, PDF) that the user would benefit from seeing inline rather than as a path to read.
55784
+
55785
+ Arguments: \`{ path: <absolute path inside the working directory>, caption?: <optional one-line message> }\`. Returns a JSON envelope: \`{ ok: true, postId }\` on success or \`{ ok: false, reason }\` on failure — when it fails, surface \`reason\` to the user verbatim so they understand what went wrong (e.g. "outside the working directory", "file too large").
55786
+
55787
+ Do NOT tell the user the tool isn't available, doesn't apply, or requires Mattermost — it's wired up and pointed at this very thread. Just call it.
55788
+
55733
55789
  ## Permissions & Interactions
55734
55790
  - Permission requests (file writes, commands, etc.) appear as messages with emoji options
55735
55791
  - Users approve with \uD83D\uDC4D or deny with \uD83D\uDC4E by reacting to the message
@@ -55800,7 +55856,6 @@ var contextPromptFiles = new Map;
55800
55856
  var log16 = createLogger("lifecycle");
55801
55857
  var sessionLog3 = createSessionLog(log16);
55802
55858
  var CHAT_PLATFORM_PROMPT = generateChatPlatformPrompt();
55803
-
55804
55859
  // src/update-notifier.ts
55805
55860
  var import_semver2 = __toESM(require_semver2(), 1);
55806
55861
 
@@ -55901,6 +55956,57 @@ ${code}
55901
55956
  }
55902
55957
  }
55903
55958
 
55959
+ // src/platform/mattermost/upload.ts
55960
+ import { readFile } from "fs/promises";
55961
+ var log22 = createLogger("mm-upload");
55962
+ async function uploadFileMattermost(args) {
55963
+ const { url: url2, token, channelId, threadId, filePath, filename, caption } = args;
55964
+ const buffer = await readFile(filePath);
55965
+ const uploadUrl = `${url2}/api/v4/files?channel_id=${encodeURIComponent(channelId)}`;
55966
+ const arrayBuffer = buffer.buffer.slice(buffer.byteOffset, buffer.byteOffset + buffer.byteLength);
55967
+ const formData = new FormData;
55968
+ formData.append("files", new Blob([arrayBuffer]), filename);
55969
+ log22.debug(`POST /files (${buffer.length} bytes, ${filename})`);
55970
+ const uploadResponse = await fetch(uploadUrl, {
55971
+ method: "POST",
55972
+ headers: {
55973
+ Authorization: `Bearer ${token}`
55974
+ },
55975
+ body: formData
55976
+ });
55977
+ if (!uploadResponse.ok) {
55978
+ const text = await uploadResponse.text();
55979
+ throw new Error(`Mattermost file upload failed: ${uploadResponse.status} ${text}`);
55980
+ }
55981
+ const uploadJson = await uploadResponse.json();
55982
+ const fileInfo = uploadJson.file_infos?.[0];
55983
+ if (!fileInfo?.id) {
55984
+ throw new Error("Mattermost file upload response missing file_infos[0].id");
55985
+ }
55986
+ const postUrl = `${url2}/api/v4/posts`;
55987
+ const postBody = {
55988
+ channel_id: channelId,
55989
+ message: caption ?? "",
55990
+ root_id: threadId,
55991
+ file_ids: [fileInfo.id]
55992
+ };
55993
+ log22.debug(`POST /posts (file_ids=[${fileInfo.id}])`);
55994
+ const postResponse = await fetch(postUrl, {
55995
+ method: "POST",
55996
+ headers: {
55997
+ Authorization: `Bearer ${token}`,
55998
+ "Content-Type": "application/json"
55999
+ },
56000
+ body: JSON.stringify(postBody)
56001
+ });
56002
+ if (!postResponse.ok) {
56003
+ const text = await postResponse.text();
56004
+ throw new Error(`Mattermost post-with-file failed: ${postResponse.status} ${text}`);
56005
+ }
56006
+ const post2 = await postResponse.json();
56007
+ return { postId: post2.id, fileId: fileInfo.id, post: post2 };
56008
+ }
56009
+
55904
56010
  // src/platform/mattermost/permission-api.ts
55905
56011
  var apiLog = createLogger("mm-api");
55906
56012
  async function mattermostApi(config3, method, path2, body) {
@@ -56095,6 +56201,20 @@ class MattermostPermissionApi {
56095
56201
  };
56096
56202
  });
56097
56203
  }
56204
+ async uploadFile(filePath, threadId, options) {
56205
+ const filename = sanitizeFilename(options?.filename ?? filePath);
56206
+ mcpLogger.debug(`uploadFile: ${filename} → thread ${formatShortId(threadId)}`);
56207
+ const result = await uploadFileMattermost({
56208
+ url: this.config.url,
56209
+ token: this.config.token,
56210
+ channelId: this.config.channelId,
56211
+ threadId,
56212
+ filePath,
56213
+ filename,
56214
+ caption: options?.caption
56215
+ });
56216
+ return { postId: result.postId };
56217
+ }
56098
56218
  }
56099
56219
  function createMattermostPermissionApi(config3) {
56100
56220
  return new MattermostPermissionApi(config3);
@@ -56169,6 +56289,77 @@ ${code}
56169
56289
  }
56170
56290
  }
56171
56291
 
56292
+ // src/platform/slack/upload.ts
56293
+ import { readFile as readFile2 } from "fs/promises";
56294
+ var log23 = createLogger("slack-upload");
56295
+ var DEFAULT_API_URL = "https://slack.com/api";
56296
+ async function uploadFileSlack(args) {
56297
+ const { botToken, channelId, threadTs, filePath, filename, caption } = args;
56298
+ const apiUrl = args.apiUrl ?? DEFAULT_API_URL;
56299
+ const buffer = await readFile2(filePath);
56300
+ const params = new URLSearchParams({ filename, length: String(buffer.length) });
56301
+ const step1Url = `${apiUrl}/files.getUploadURLExternal?${params.toString()}`;
56302
+ log23.debug(`GET files.getUploadURLExternal (${buffer.length} bytes, ${filename})`);
56303
+ const step1Response = await fetch(step1Url, {
56304
+ method: "GET",
56305
+ headers: {
56306
+ Authorization: `Bearer ${botToken}`
56307
+ }
56308
+ });
56309
+ if (!step1Response.ok) {
56310
+ const text = await step1Response.text();
56311
+ throw new Error(`Slack getUploadURLExternal failed: ${step1Response.status} ${text}`);
56312
+ }
56313
+ const step1Data = await step1Response.json();
56314
+ if (!step1Data.ok || !step1Data.upload_url || !step1Data.file_id) {
56315
+ throw new Error(`Slack getUploadURLExternal error: ${step1Data.error || "missing upload_url/file_id"}`);
56316
+ }
56317
+ const uploadUrl = step1Data.upload_url;
56318
+ const fileId = step1Data.file_id;
56319
+ const arrayBuffer = buffer.buffer.slice(buffer.byteOffset, buffer.byteOffset + buffer.byteLength);
56320
+ log23.debug(`POST <upload_url>`);
56321
+ const step2Response = await fetch(uploadUrl, {
56322
+ method: "POST",
56323
+ headers: {
56324
+ "Content-Type": "application/octet-stream"
56325
+ },
56326
+ body: arrayBuffer
56327
+ });
56328
+ if (!step2Response.ok) {
56329
+ const text = await step2Response.text();
56330
+ throw new Error(`Slack file bytes upload failed: ${step2Response.status} ${text}`);
56331
+ }
56332
+ const step3Body = {
56333
+ files: [{ id: fileId, title: caption ?? filename }],
56334
+ channel_id: channelId,
56335
+ thread_ts: threadTs
56336
+ };
56337
+ if (caption !== undefined) {
56338
+ step3Body.initial_comment = caption;
56339
+ }
56340
+ log23.debug(`POST files.completeUploadExternal (file_id=${fileId}, thread_ts=${threadTs})`);
56341
+ const step3Response = await fetch(`${apiUrl}/files.completeUploadExternal`, {
56342
+ method: "POST",
56343
+ headers: {
56344
+ Authorization: `Bearer ${botToken}`,
56345
+ "Content-Type": "application/json; charset=utf-8"
56346
+ },
56347
+ body: JSON.stringify(step3Body)
56348
+ });
56349
+ if (!step3Response.ok) {
56350
+ const text = await step3Response.text();
56351
+ throw new Error(`Slack completeUploadExternal failed: ${step3Response.status} ${text}`);
56352
+ }
56353
+ const step3Data = await step3Response.json();
56354
+ if (!step3Data.ok) {
56355
+ throw new Error(`Slack completeUploadExternal error: ${step3Data.error || "unknown"}`);
56356
+ }
56357
+ if (!step3Data.ts) {
56358
+ log23.warn(`Slack completeUploadExternal returned no ts; using fileId ${fileId} as postId. ` + `Do not use this id for updatePost/addReaction.`);
56359
+ }
56360
+ return { fileId, postId: step3Data.ts ?? fileId };
56361
+ }
56362
+
56172
56363
  // src/platform/slack/permission-api.ts
56173
56364
  var SLACK_API_BASE = "https://slack.com/api";
56174
56365
  async function slackApi(method, token, body) {
@@ -56377,6 +56568,19 @@ class SlackPermissionApi {
56377
56568
  mcpLogger.debug("Got Socket Mode URL");
56378
56569
  return response.url;
56379
56570
  }
56571
+ async uploadFile(filePath, threadId, options) {
56572
+ const filename = sanitizeFilename(options?.filename ?? filePath);
56573
+ mcpLogger.debug(`uploadFile: ${filename} → thread_ts ${threadId}`);
56574
+ const result = await uploadFileSlack({
56575
+ botToken: this.config.botToken,
56576
+ channelId: this.config.channelId,
56577
+ threadTs: threadId,
56578
+ filePath,
56579
+ filename,
56580
+ caption: options?.caption
56581
+ });
56582
+ return { postId: result.postId };
56583
+ }
56380
56584
  }
56381
56585
  function createSlackPermissionApi(config3) {
56382
56586
  return new SlackPermissionApi(config3);
@@ -56394,6 +56598,109 @@ function createPermissionApi(platformType, config3) {
56394
56598
  }
56395
56599
  }
56396
56600
 
56601
+ // src/mcp/path-validator.ts
56602
+ import { lstat, realpath, stat } from "fs/promises";
56603
+ import { sep as sep2, isAbsolute } from "path";
56604
+ function isUnderRoot(needle, root) {
56605
+ if (needle === root)
56606
+ return true;
56607
+ const withSep = root.endsWith(sep2) ? root : root + sep2;
56608
+ return needle.startsWith(withSep);
56609
+ }
56610
+ var DANGEROUSLY_WIDE_ROOTS = new Set([
56611
+ "/",
56612
+ "/home",
56613
+ "/Users",
56614
+ "/root",
56615
+ "/etc",
56616
+ "/var",
56617
+ "/tmp",
56618
+ "/usr",
56619
+ "/opt"
56620
+ ]);
56621
+ function isDangerouslyWide(root) {
56622
+ if (DANGEROUSLY_WIDE_ROOTS.has(root))
56623
+ return true;
56624
+ if (root === "/private/tmp" || root === "/private/var" || root === "/private/etc")
56625
+ return true;
56626
+ return false;
56627
+ }
56628
+ async function validateOutboundPath(inputPath, opts) {
56629
+ if (typeof inputPath !== "string" || inputPath.length === 0) {
56630
+ return { ok: false, reason: "path is required" };
56631
+ }
56632
+ if (!isAbsolute(inputPath)) {
56633
+ return { ok: false, reason: "path must be absolute" };
56634
+ }
56635
+ if (!Number.isFinite(opts.maxBytes) || opts.maxBytes <= 0) {
56636
+ return {
56637
+ ok: false,
56638
+ reason: `invalid maxBytes ${opts.maxBytes} — check outboundFiles.maxBytes configuration`
56639
+ };
56640
+ }
56641
+ for (const root of opts.allowedRoots) {
56642
+ if (isDangerouslyWide(root)) {
56643
+ return {
56644
+ ok: false,
56645
+ reason: `refusing to validate against dangerously wide allowed root '${root}' — check SESSION_WORKING_DIR configuration`
56646
+ };
56647
+ }
56648
+ }
56649
+ let resolvedPath;
56650
+ try {
56651
+ resolvedPath = await realpath(inputPath);
56652
+ } catch (err) {
56653
+ const msg = err instanceof Error ? err.message : String(err);
56654
+ return { ok: false, reason: `cannot resolve path: ${msg}` };
56655
+ }
56656
+ const inAllowedRoot = opts.allowedRoots.some((root) => isUnderRoot(resolvedPath, root));
56657
+ if (!inAllowedRoot) {
56658
+ return {
56659
+ ok: false,
56660
+ reason: "path is outside the session working directory. Move the file into the working directory and retry."
56661
+ };
56662
+ }
56663
+ let lstatPre, statResolved;
56664
+ try {
56665
+ lstatPre = await lstat(inputPath);
56666
+ } catch (err) {
56667
+ const msg = err instanceof Error ? err.message : String(err);
56668
+ return { ok: false, reason: `cannot stat path: ${msg}` };
56669
+ }
56670
+ try {
56671
+ statResolved = await stat(resolvedPath);
56672
+ } catch (err) {
56673
+ const msg = err instanceof Error ? err.message : String(err);
56674
+ return { ok: false, reason: `cannot stat resolved path: ${msg}` };
56675
+ }
56676
+ if (!statResolved.isFile()) {
56677
+ return { ok: false, reason: "not a regular file" };
56678
+ }
56679
+ if (!lstatPre.isFile() && !lstatPre.isSymbolicLink()) {
56680
+ return { ok: false, reason: "not a regular file" };
56681
+ }
56682
+ const SUID = 2048;
56683
+ const SGID = 1024;
56684
+ if (statResolved.mode & SUID || statResolved.mode & SGID) {
56685
+ return { ok: false, reason: "refusing to upload SUID/SGID file" };
56686
+ }
56687
+ if (statResolved.size === 0) {
56688
+ return { ok: false, reason: "file is empty" };
56689
+ }
56690
+ if (statResolved.size > opts.maxBytes) {
56691
+ return {
56692
+ ok: false,
56693
+ reason: `file too large (${formatBytes(statResolved.size)} > ${formatBytes(opts.maxBytes)} limit)`
56694
+ };
56695
+ }
56696
+ return {
56697
+ ok: true,
56698
+ resolvedPath,
56699
+ size: statResolved.size,
56700
+ basename: sanitizeFilename(resolvedPath)
56701
+ };
56702
+ }
56703
+
56397
56704
  // src/mcp/permission-server.ts
56398
56705
  var PLATFORM_TYPE = process.env.PLATFORM_TYPE || "";
56399
56706
  var PLATFORM_URL = process.env.PLATFORM_URL || "";
@@ -56402,6 +56709,11 @@ var PLATFORM_CHANNEL_ID = process.env.PLATFORM_CHANNEL_ID || "";
56402
56709
  var PLATFORM_THREAD_ID = process.env.PLATFORM_THREAD_ID || "";
56403
56710
  var ALLOWED_USERS = (process.env.ALLOWED_USERS || "").split(",").map((u) => u.trim()).filter((u) => u.length > 0);
56404
56711
  var PERMISSION_TIMEOUT_MS = parseInt(process.env.PERMISSION_TIMEOUT_MS || "120000", 10);
56712
+ var SESSION_WORKING_DIR = process.env[OUTBOUND_ENV.SESSION_WORKING_DIR] || "";
56713
+ var SESSION_UPLOAD_DIR = process.env[OUTBOUND_ENV.SESSION_UPLOAD_DIR] || "";
56714
+ var OUTBOUND_FILES_ENABLED = (process.env[OUTBOUND_ENV.OUTBOUND_FILES_ENABLED] ?? "1") !== "0";
56715
+ var OUTBOUND_FILES_MAX_BYTES = parseInt(process.env[OUTBOUND_ENV.OUTBOUND_FILES_MAX_BYTES] || String(100 * 1024 * 1024), 10);
56716
+ var SEND_FILE_TOOL_NAME = "mcp__claude-threads-permissions__send_file";
56405
56717
  var apiConfig = PLATFORM_TYPE === "slack" ? {
56406
56718
  platformType: "slack",
56407
56719
  botToken: PLATFORM_TOKEN,
@@ -56429,6 +56741,10 @@ function getApi() {
56429
56741
  var allowAllSession = false;
56430
56742
  async function handlePermissionWith(toolName, toolInput, cfg) {
56431
56743
  mcpLogger.debug(`handlePermission called for ${toolName}`);
56744
+ if (toolName === SEND_FILE_TOOL_NAME) {
56745
+ mcpLogger.debug(`Auto-allowing ${toolName} (path validator is the real gate)`);
56746
+ return { behavior: "allow", updatedInput: toolInput };
56747
+ }
56432
56748
  if (cfg.getAllowAll()) {
56433
56749
  mcpLogger.debug(`Auto-allowing ${toolName} (allow all active)`);
56434
56750
  return { behavior: "allow", updatedInput: toolInput };
@@ -56518,6 +56834,51 @@ var permissionInputSchema = {
56518
56834
  tool_name: exports_external.string().describe("Name of the tool requesting permission"),
56519
56835
  input: exports_external.record(exports_external.string(), exports_external.unknown()).describe("Tool input parameters")
56520
56836
  };
56837
+ var sendFileInputSchema = {
56838
+ path: exports_external.string().describe("Absolute path of a file inside the session working directory. The bot will upload it to chat."),
56839
+ caption: exports_external.string().optional().describe("Optional message body / initial comment shown alongside the file.")
56840
+ };
56841
+ async function handleSendFileWith(args, cfg) {
56842
+ if (!cfg.enabled) {
56843
+ return { ok: false, reason: "outbound file sending is disabled by the operator" };
56844
+ }
56845
+ if (!cfg.api.uploadFile) {
56846
+ return { ok: false, reason: "this platform does not support outbound file uploads" };
56847
+ }
56848
+ if (!cfg.threadId) {
56849
+ return { ok: false, reason: "no thread context — file uploads only work inside a session thread" };
56850
+ }
56851
+ if (cfg.allowedRoots.length === 0) {
56852
+ return { ok: false, reason: "no allowed roots configured for outbound file uploads" };
56853
+ }
56854
+ const validated = await validateOutboundPath(args.path, {
56855
+ allowedRoots: cfg.allowedRoots,
56856
+ maxBytes: cfg.maxBytes
56857
+ });
56858
+ if (!validated.ok) {
56859
+ return { ok: false, reason: validated.reason };
56860
+ }
56861
+ try {
56862
+ const result = await cfg.api.uploadFile(validated.resolvedPath, cfg.threadId, {
56863
+ caption: args.caption,
56864
+ filename: validated.basename
56865
+ });
56866
+ return { ok: true, postId: result.postId };
56867
+ } catch (err) {
56868
+ const reason = err instanceof Error ? err.message : String(err);
56869
+ mcpLogger.warn(`send_file upload failed: ${reason}`);
56870
+ return { ok: false, reason };
56871
+ }
56872
+ }
56873
+ async function handleSendFile(args) {
56874
+ return handleSendFileWith(args, {
56875
+ api: getApi(),
56876
+ threadId: PLATFORM_THREAD_ID,
56877
+ enabled: OUTBOUND_FILES_ENABLED,
56878
+ allowedRoots: [SESSION_WORKING_DIR, SESSION_UPLOAD_DIR].filter((p) => p.length > 0),
56879
+ maxBytes: OUTBOUND_FILES_MAX_BYTES
56880
+ });
56881
+ }
56521
56882
  async function main() {
56522
56883
  const server = new McpServer({
56523
56884
  name: "claude-threads-permissions",
@@ -56529,6 +56890,12 @@ async function main() {
56529
56890
  content: [{ type: "text", text: JSON.stringify(result) }]
56530
56891
  };
56531
56892
  });
56893
+ server.tool("send_file", "Send a file from the session working directory directly into the chat thread. " + "Use this when the user asked to receive a file inline, or when you produce an artifact " + "they should see (screenshot, generated audio, plot, document). The path must be absolute " + "and inside the session working directory. Returns { ok: true, postId } on success or " + "{ ok: false, reason } on failure.", sendFileInputSchema, async ({ path: path2, caption }) => {
56894
+ const result = await handleSendFile({ path: path2, caption });
56895
+ return {
56896
+ content: [{ type: "text", text: JSON.stringify(result) }]
56897
+ };
56898
+ });
56532
56899
  const transport = new StdioServerTransport;
56533
56900
  await server.connect(transport);
56534
56901
  mcpLogger.info(`Permission server ready (platform: ${PLATFORM_TYPE})`);
@@ -56538,5 +56905,6 @@ main().catch((err) => {
56538
56905
  process.exit(1);
56539
56906
  });
56540
56907
  export {
56908
+ handleSendFileWith,
56541
56909
  handlePermissionWith
56542
56910
  };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "claude-threads",
3
- "version": "1.11.0",
3
+ "version": "1.12.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",