claude-threads 1.21.1 → 1.22.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.
@@ -51569,6 +51569,8 @@ class MessageManager {
51569
51569
  emitSessionUpdateCallback;
51570
51570
  toolStartTimes = new Map;
51571
51571
  taskTracker = new TaskTracker;
51572
+ pendingBridgePlan = null;
51573
+ pendingBridgeQuestion = null;
51572
51574
  flushTimer = null;
51573
51575
  static DEFAULT_FLUSH_DELAY_MS = 500;
51574
51576
  flushDelayMs;
@@ -52030,9 +52032,66 @@ class MessageManager {
52030
52032
  logger.debug("Reaction not handled by any executor");
52031
52033
  return false;
52032
52034
  }
52035
+ handleBridgeRequest(request, signal) {
52036
+ if (request.kind === "plan_approval") {
52037
+ this.pendingBridgePlan?.resolve({ behavior: "deny", message: "Superseded by a newer plan" });
52038
+ return new Promise((resolve2) => {
52039
+ const pending = { resolve: resolve2, input: request.input };
52040
+ this.pendingBridgePlan = pending;
52041
+ signal?.addEventListener("abort", () => {
52042
+ if (this.pendingBridgePlan === pending)
52043
+ this.pendingBridgePlan = null;
52044
+ });
52045
+ });
52046
+ }
52047
+ if (request.kind === "question") {
52048
+ this.pendingBridgeQuestion?.resolve({ behavior: "deny", message: "Superseded by newer questions" });
52049
+ return new Promise((resolve2) => {
52050
+ const pending = { resolve: resolve2, input: request.input };
52051
+ this.pendingBridgeQuestion = pending;
52052
+ signal?.addEventListener("abort", () => {
52053
+ if (this.pendingBridgeQuestion === pending)
52054
+ this.pendingBridgeQuestion = null;
52055
+ });
52056
+ });
52057
+ }
52058
+ return Promise.resolve({ behavior: "deny", message: `Unknown bridge request kind` });
52059
+ }
52060
+ resolveBridgePlan(approved) {
52061
+ const pending = this.pendingBridgePlan;
52062
+ if (!pending)
52063
+ return false;
52064
+ this.pendingBridgePlan = null;
52065
+ pending.resolve(approved ? { behavior: "allow", updatedInput: pending.input } : { behavior: "deny", message: "The user rejected the plan." });
52066
+ return true;
52067
+ }
52068
+ resolveBridgeQuestion(answers) {
52069
+ const pending = this.pendingBridgeQuestion;
52070
+ if (!pending)
52071
+ return false;
52072
+ this.pendingBridgeQuestion = null;
52073
+ const questions = Array.isArray(pending.input.questions) ? pending.input.questions : [];
52074
+ const record4 = {};
52075
+ for (const a of answers) {
52076
+ const match = questions.find((q) => q.header === a.header);
52077
+ record4[match?.question ?? a.header] = a.answer;
52078
+ }
52079
+ pending.resolve({
52080
+ behavior: "allow",
52081
+ updatedInput: { ...pending.input, answers: record4 }
52082
+ });
52083
+ return true;
52084
+ }
52085
+ denyPendingBridgeRequests(reason) {
52086
+ this.pendingBridgePlan?.resolve({ behavior: "deny", message: reason });
52087
+ this.pendingBridgePlan = null;
52088
+ this.pendingBridgeQuestion?.resolve({ behavior: "deny", message: reason });
52089
+ this.pendingBridgeQuestion = null;
52090
+ }
52033
52091
  clearClaudeSessionState() {
52034
52092
  this.toolStartTimes.clear();
52035
52093
  this.taskTracker.clear();
52094
+ this.denyPendingBridgeRequests("Claude was restarted before a decision was made");
52036
52095
  }
52037
52096
  reset() {
52038
52097
  this.cancelScheduledFlush();
@@ -52048,6 +52107,7 @@ class MessageManager {
52048
52107
  this.systemExecutor.reset();
52049
52108
  }
52050
52109
  dispose() {
52110
+ this.denyPendingBridgeRequests("Session ended before a decision was made");
52051
52111
  this.cancelScheduledFlush();
52052
52112
  this.postTracker.clearSession(this.sessionId);
52053
52113
  this.events.removeAllListeners();
@@ -55751,6 +55811,148 @@ class UsernameAnonymizer {
55751
55811
  return anonymized;
55752
55812
  }
55753
55813
  }
55814
+ // src/mcp/decision-bridge.ts
55815
+ import { createServer, createConnection } from "node:net";
55816
+ import { tmpdir } from "node:os";
55817
+ import { join as join3 } from "node:path";
55818
+ import { randomUUID } from "node:crypto";
55819
+ import { mkdtempSync } from "node:fs";
55820
+ import { rm } from "node:fs/promises";
55821
+
55822
+ class BridgeUnavailableError extends Error {
55823
+ }
55824
+ function bridgeSocketPath() {
55825
+ if (process.platform === "win32") {
55826
+ return `\\\\.\\pipe\\ctb-${randomUUID()}`;
55827
+ }
55828
+ const dir = mkdtempSync(join3(tmpdir(), "ctb-"));
55829
+ return join3(dir, "b.sock");
55830
+ }
55831
+
55832
+ class DecisionBridgeServer {
55833
+ server;
55834
+ path;
55835
+ liveSockets = new Set;
55836
+ constructor(server, path) {
55837
+ this.server = server;
55838
+ this.path = path;
55839
+ }
55840
+ static async create(handler2) {
55841
+ const path = bridgeSocketPath();
55842
+ const liveSockets = new Set;
55843
+ const server = createServer((socket) => {
55844
+ liveSockets.add(socket);
55845
+ socket.once("close", () => liveSockets.delete(socket));
55846
+ let buffer = "";
55847
+ const aborter = new AbortController;
55848
+ let responded = false;
55849
+ socket.on("data", (chunk) => {
55850
+ buffer += chunk.toString("utf8");
55851
+ const newline = buffer.indexOf(`
55852
+ `);
55853
+ if (newline === -1)
55854
+ return;
55855
+ const line = buffer.slice(0, newline);
55856
+ buffer = "";
55857
+ let request;
55858
+ try {
55859
+ request = JSON.parse(line);
55860
+ } catch {
55861
+ responded = true;
55862
+ socket.end(JSON.stringify({ behavior: "deny", message: "Malformed bridge request" }) + `
55863
+ `);
55864
+ return;
55865
+ }
55866
+ handler2(request, aborter.signal).then((response) => {
55867
+ responded = true;
55868
+ socket.end(JSON.stringify(response) + `
55869
+ `);
55870
+ }).catch((err) => {
55871
+ responded = true;
55872
+ if (err instanceof BridgeUnavailableError) {
55873
+ socket.destroy();
55874
+ return;
55875
+ }
55876
+ socket.end(JSON.stringify({
55877
+ behavior: "deny",
55878
+ message: `Bridge handler failed: ${err instanceof Error ? err.message : String(err)}`
55879
+ }) + `
55880
+ `);
55881
+ });
55882
+ });
55883
+ socket.on("close", () => {
55884
+ if (!responded)
55885
+ aborter.abort();
55886
+ });
55887
+ socket.on("error", () => {});
55888
+ });
55889
+ try {
55890
+ await new Promise((resolve4, reject) => {
55891
+ server.once("error", reject);
55892
+ server.listen(path, () => {
55893
+ server.removeListener("error", reject);
55894
+ resolve4();
55895
+ });
55896
+ });
55897
+ } catch (err) {
55898
+ if (process.platform !== "win32") {
55899
+ await rm(join3(path, ".."), { recursive: true, force: true }).catch(() => {});
55900
+ }
55901
+ throw err;
55902
+ }
55903
+ const bridge = new DecisionBridgeServer(server, path);
55904
+ bridge.liveSockets = liveSockets;
55905
+ return bridge;
55906
+ }
55907
+ async close() {
55908
+ for (const socket of this.liveSockets)
55909
+ socket.destroy();
55910
+ await new Promise((resolve4) => this.server.close(() => resolve4()));
55911
+ if (process.platform !== "win32") {
55912
+ await rm(join3(this.path, ".."), { recursive: true, force: true }).catch(() => {});
55913
+ }
55914
+ }
55915
+ }
55916
+ function requestBridgeDecision(path, request, timeoutMs) {
55917
+ return new Promise((resolve4, reject) => {
55918
+ const socket = createConnection(path);
55919
+ let buffer = "";
55920
+ let settled = false;
55921
+ const fail = (err) => {
55922
+ if (settled)
55923
+ return;
55924
+ settled = true;
55925
+ clearTimeout(timer);
55926
+ socket.destroy();
55927
+ reject(err);
55928
+ };
55929
+ const timer = setTimeout(() => fail(new Error(`Bridge decision timed out after ${timeoutMs}ms`)), timeoutMs);
55930
+ socket.on("connect", () => {
55931
+ socket.write(JSON.stringify(request) + `
55932
+ `);
55933
+ });
55934
+ socket.on("data", (chunk) => {
55935
+ buffer += chunk.toString("utf8");
55936
+ const newline = buffer.indexOf(`
55937
+ `);
55938
+ if (newline === -1)
55939
+ return;
55940
+ if (settled)
55941
+ return;
55942
+ settled = true;
55943
+ clearTimeout(timer);
55944
+ socket.destroy();
55945
+ try {
55946
+ resolve4(JSON.parse(buffer.slice(0, newline)));
55947
+ } catch {
55948
+ reject(new Error("Malformed bridge response"));
55949
+ }
55950
+ });
55951
+ socket.on("error", (err) => fail(err));
55952
+ socket.on("close", () => fail(new Error("Bridge connection closed before a decision arrived")));
55953
+ });
55954
+ }
55955
+
55754
55956
  // src/utils/spawn.ts
55755
55957
  import { spawn as nodeSpawn, spawnSync as nodeSpawnSync } from "child_process";
55756
55958
  var isWindows = process.platform === "win32";
@@ -55769,8 +55971,8 @@ import { EventEmitter as EventEmitter2 } from "events";
55769
55971
  import { resolve as resolve4, dirname as dirname5 } from "path";
55770
55972
  import { fileURLToPath as fileURLToPath3 } from "url";
55771
55973
  import { existsSync as existsSync4, readFileSync as readFileSync3, watchFile, unwatchFile, unlinkSync, statSync, readdirSync, writeFileSync } from "fs";
55772
- import { tmpdir } from "os";
55773
- import { join as join3 } from "path";
55974
+ import { tmpdir as tmpdir2 } from "os";
55975
+ import { join as join4 } from "path";
55774
55976
 
55775
55977
  // src/mcp/outbound-env.ts
55776
55978
  var OUTBOUND_ENV = {
@@ -55864,11 +56066,11 @@ function parseRateLimitEvent(event, now = Date.now()) {
55864
56066
  var log8 = createLogger("claude");
55865
56067
  function cleanupBrowserBridgeSockets() {
55866
56068
  try {
55867
- const tempDir = tmpdir();
56069
+ const tempDir = tmpdir2();
55868
56070
  const files = readdirSync(tempDir);
55869
56071
  for (const file2 of files) {
55870
56072
  if (file2.startsWith("claude-mcp-browser-bridge-")) {
55871
- const filePath = join3(tempDir, file2);
56073
+ const filePath = join4(tempDir, file2);
55872
56074
  try {
55873
56075
  const stats = statSync(filePath);
55874
56076
  if (stats.isSocket()) {
@@ -55882,7 +56084,7 @@ function cleanupBrowserBridgeSockets() {
55882
56084
  log8.debug(`Browser bridge cleanup failed: ${err}`);
55883
56085
  }
55884
56086
  }
55885
- function buildClaudeChildEnv(parentEnv, account) {
56087
+ function buildClaudeChildEnv(parentEnv, account, opts) {
55886
56088
  const env = { ...parentEnv };
55887
56089
  if (env.MCP_CONNECTION_NONBLOCKING === undefined) {
55888
56090
  env.MCP_CONNECTION_NONBLOCKING = "true";
@@ -55890,6 +56092,9 @@ function buildClaudeChildEnv(parentEnv, account) {
55890
56092
  if (env.ENABLE_PROMPT_CACHING_1H === undefined) {
55891
56093
  env.ENABLE_PROMPT_CACHING_1H = "true";
55892
56094
  }
56095
+ if (opts?.decisionBridge && env.MCP_TOOL_TIMEOUT === undefined) {
56096
+ env.MCP_TOOL_TIMEOUT = "3600000";
56097
+ }
55893
56098
  if (account?.home) {
55894
56099
  env.HOME = account.home;
55895
56100
  env.USERPROFILE = account.home;
@@ -55913,8 +56118,8 @@ function materializeMcpConfig(config3, sessionId, opts = {}) {
55913
56118
  if (opts.inline) {
55914
56119
  return { mode: "inline", value: JSON.stringify(config3) };
55915
56120
  }
55916
- const dir = opts.tmpDirOverride ?? tmpdir();
55917
- const path = join3(dir, `claude-threads-mcp-${sessionId ?? process.pid}-${Date.now()}.json`);
56121
+ const dir = opts.tmpDirOverride ?? tmpdir2();
56122
+ const path = join4(dir, `claude-threads-mcp-${sessionId ?? process.pid}-${Date.now()}.json`);
55918
56123
  writeFileSync(path, JSON.stringify(config3), { mode: 384 });
55919
56124
  return { mode: "file", path };
55920
56125
  }
@@ -55938,6 +56143,12 @@ function buildPermissionArgs(opts) {
55938
56143
  PERMISSION_TIMEOUT_MS: String(opts.permissionTimeoutMs),
55939
56144
  SESSION_OWNER_USERNAME: opts.sessionOwnerUsername || ""
55940
56145
  };
56146
+ if (opts.decisionBridgePath) {
56147
+ mcpEnv.DECISION_BRIDGE_PATH = opts.decisionBridgePath;
56148
+ if (process.env.DECISION_BRIDGE_TIMEOUT_MS) {
56149
+ mcpEnv.DECISION_BRIDGE_TIMEOUT_MS = process.env.DECISION_BRIDGE_TIMEOUT_MS;
56150
+ }
56151
+ }
55941
56152
  if (opts.platformConfig.appToken) {
55942
56153
  mcpEnv.PLATFORM_APP_TOKEN = opts.platformConfig.appToken;
55943
56154
  }
@@ -56078,7 +56289,8 @@ class ClaudeCli extends EventEmitter2 {
56078
56289
  workingDir: this.options.workingDir,
56079
56290
  uploadDir: this.options.uploadDir,
56080
56291
  outboundFiles: this.options.outboundFiles,
56081
- sessionOwnerUsername: this.options.sessionOwnerUsername
56292
+ sessionOwnerUsername: this.options.sessionOwnerUsername,
56293
+ decisionBridgePath: this.options.decisionBridgePath
56082
56294
  });
56083
56295
  args.push(...permResult.args);
56084
56296
  this.mcpConfigTempFile = permResult.tempFile;
@@ -56089,7 +56301,7 @@ class ClaudeCli extends EventEmitter2 {
56089
56301
  args.push("--append-system-prompt", this.options.appendSystemPrompt);
56090
56302
  }
56091
56303
  if (this.options.sessionId) {
56092
- this.statusFilePath = join3(tmpdir(), `claude-threads-status-${this.options.sessionId}.json`);
56304
+ this.statusFilePath = join4(tmpdir2(), `claude-threads-status-${this.options.sessionId}.json`);
56093
56305
  const statusLineWriterPath = this.getStatusLineWriterPath();
56094
56306
  const statusLineSettings = {
56095
56307
  statusLine: {
@@ -56312,7 +56524,9 @@ class ClaudeCli extends EventEmitter2 {
56312
56524
  return true;
56313
56525
  }
56314
56526
  buildChildEnv() {
56315
- return buildClaudeChildEnv(process.env, this.options.account);
56527
+ return buildClaudeChildEnv(process.env, this.options.account, {
56528
+ decisionBridge: this.options.decisionBridgePath !== undefined
56529
+ });
56316
56530
  }
56317
56531
  getMcpServerPath() {
56318
56532
  const __filename2 = fileURLToPath3(import.meta.url);
@@ -57005,10 +57219,10 @@ init_emoji();
57005
57219
 
57006
57220
  // src/persistence/github-emails-store.ts
57007
57221
  import { homedir as homedir4 } from "os";
57008
- import { join as join5 } from "path";
57222
+ import { join as join6 } from "path";
57009
57223
  var log18 = createLogger("gh-emails");
57010
- var DEFAULT_CONFIG_DIR = join5(homedir4(), ".config", "claude-threads");
57011
- var DEFAULT_FILE = join5(DEFAULT_CONFIG_DIR, "github-emails.yaml");
57224
+ var DEFAULT_CONFIG_DIR = join6(homedir4(), ".config", "claude-threads");
57225
+ var DEFAULT_FILE = join6(DEFAULT_CONFIG_DIR, "github-emails.yaml");
57012
57226
 
57013
57227
  // src/operations/commands/handler.ts
57014
57228
  var log19 = createLogger("commands");
@@ -58380,6 +58594,8 @@ var PLATFORM_CHANNEL_ID = process.env.PLATFORM_CHANNEL_ID || "";
58380
58594
  var PLATFORM_THREAD_ID = process.env.PLATFORM_THREAD_ID || "";
58381
58595
  var ALLOWED_USERS = (process.env.ALLOWED_USERS || "").split(",").map((u) => u.trim()).filter((u) => u.length > 0);
58382
58596
  var PERMISSION_TIMEOUT_MS = parseInt(process.env.PERMISSION_TIMEOUT_MS || "120000", 10);
58597
+ var DECISION_BRIDGE_PATH = process.env.DECISION_BRIDGE_PATH || "";
58598
+ var DECISION_BRIDGE_TIMEOUT_MS = parseInt(process.env.DECISION_BRIDGE_TIMEOUT_MS || "3600000", 10);
58383
58599
  var SESSION_OWNER_USERNAME = process.env.SESSION_OWNER_USERNAME || "";
58384
58600
  var SESSION_WORKING_DIR = process.env[OUTBOUND_ENV.SESSION_WORKING_DIR] || "";
58385
58601
  var SESSION_UPLOAD_DIR = process.env[OUTBOUND_ENV.SESSION_UPLOAD_DIR] || "";
@@ -58442,6 +58658,21 @@ async function handlePermissionWith(toolName, toolInput, cfg) {
58442
58658
  mcpLogger.debug(`Skipping standard prompt for ${toolName} (handler enforces its own gate)`);
58443
58659
  return { behavior: "allow", updatedInput: toolInput };
58444
58660
  }
58661
+ const hasMultiSelect = toolName === "AskUserQuestion" && Array.isArray(toolInput.questions) && toolInput.questions.some((q) => q?.multiSelect === true);
58662
+ if ((toolName === "ExitPlanMode" || toolName === "AskUserQuestion") && cfg.decisionBridge && !hasMultiSelect) {
58663
+ const kind = toolName === "ExitPlanMode" ? "plan_approval" : "question";
58664
+ try {
58665
+ const decision = await (cfg.decisionBridge.request ?? requestBridgeDecision)(cfg.decisionBridge.path, { kind, toolName, input: toolInput }, cfg.decisionBridge.timeoutMs);
58666
+ mcpLogger.info(`${toolName} decided via bridge: ${decision.behavior}`);
58667
+ return {
58668
+ behavior: decision.behavior,
58669
+ updatedInput: decision.updatedInput ?? toolInput,
58670
+ message: decision.message
58671
+ };
58672
+ } catch (err) {
58673
+ mcpLogger.warn(`Decision bridge unavailable for ${toolName} (${err}) — legacy fallback`);
58674
+ }
58675
+ }
58445
58676
  if (toolName === "AskUserQuestion") {
58446
58677
  mcpLogger.debug("Auto-allowing AskUserQuestion (question UI is handled by the main bot)");
58447
58678
  return { behavior: "allow", updatedInput: toolInput };
@@ -58528,7 +58759,8 @@ async function handlePermission(toolName, toolInput) {
58528
58759
  getAllowAll: () => allowAllSession,
58529
58760
  setAllowAll: (v) => {
58530
58761
  allowAllSession = v;
58531
- }
58762
+ },
58763
+ decisionBridge: DECISION_BRIDGE_PATH ? { path: DECISION_BRIDGE_PATH, timeoutMs: DECISION_BRIDGE_TIMEOUT_MS } : undefined
58532
58764
  });
58533
58765
  }
58534
58766
  var permissionInputSchema = {
@@ -211,6 +211,7 @@ Exactly one of `home` or `apiKey` should be set per account. Persisted sessions
211
211
  | `SESSION_TIMEOUT_MS` | Idle timeout in milliseconds. Legacy fallback for `limits.sessionTimeoutMinutes`. | `1800000` (30 min) |
212
212
  | `DEBUG` | Enable verbose logging | - |
213
213
  | `CLAUDE_PATH` | Path to the `claude` binary. Overrides the PATH lookup and the common install locations. | `claude` (from PATH) |
214
+ | `DECISION_BRIDGE_TIMEOUT_MS` | How long the MCP permission server waits for a plan approval or question answer routed through the decision bridge (the bot's reaction UI) before falling back to the legacy behavior (generic prompt for plans, auto-allow for questions). | `3600000` (1 h) |
214
215
  | `CLAUDE_CODE_SUBPROCESS_ENV_SCRUB` | Strip `ANTHROPIC_*`, `AWS_*_TOKEN`, `CLAUDE_CODE_OAUTH_TOKEN`, `GOOGLE_APPLICATION_CREDENTIALS`, and similar from Bash, hook, and stdio-MCP subprocesses Claude spawns. Bot-specific vars like `PLATFORM_TOKEN` pass through. **Also forces permission mode to `default`**; `--dangerously-skip-permissions` will be rejected. Requires Claude CLI 2.1.83+. | - |
215
216
  | `CLAUDE_THREADS_SESSIONS_PATH` | Override the path to the persisted sessions file (default `~/.config/claude-threads/sessions.json`). | - |
216
217
  | `CLAUDE_THREADS_GITHUB_EMAILS_PATH` | Override the path to the GitHub-emails store used for commit attribution. | - |
@@ -218,15 +219,16 @@ Exactly one of `home` or `apiKey` should be set per account. Persisted sessions
218
219
 
219
220
  ### Forwarded to Claude CLI automatically
220
221
 
221
- The bot sets two tuning flags on the Claude child process when they aren't
222
+ The bot sets these tuning flags on the Claude child process when they aren't
222
223
  already present in the bot's environment:
223
224
 
224
225
  | Variable | Effect | Requires |
225
226
  |----------|--------|----------|
226
227
  | `MCP_CONNECTION_NONBLOCKING=true` | Caps `--mcp-config` connects at 5s so a slow MCP server never delays startup | Claude CLI 2.1.89+ |
227
228
  | `ENABLE_PROMPT_CACHING_1H=true` | Opts into 1-hour prompt cache TTL, cutting re-caching cost on long-lived threads | Claude CLI 2.1.108+ |
229
+ | `MCP_TOOL_TIMEOUT=3600000` | Only set when the session has a decision bridge. Without it the CLI abandons a pending MCP permission call after ~2 minutes — far too short for plan approvals and question answers that wait on a human reaction. One hour matches the bridge's own `DECISION_BRIDGE_TIMEOUT_MS` default. Verified against CLI 2.1.223. | — |
228
230
 
229
- Export either with a different value in the bot's own env to disable.
231
+ Export any of them with a different value in the bot's own env to override.
230
232
 
231
233
  ## CLI Options
232
234
 
package/docs/MCP-TOOLS.md CHANGED
@@ -15,7 +15,7 @@ The permission handler the Claude CLI calls before running a tool that needs app
15
15
  | `tool_name` | string | Name of the tool requesting permission. |
16
16
  | `input` | object | The tool's input parameters, shown to the user in the prompt. |
17
17
 
18
- **Guardrail:** Only reactions from users on the session allowlist count. The bot's own reactions (the option emoji it adds) are ignored. If no reaction arrives within `limits.permissionTimeoutSeconds` (default 120), the request is denied.
18
+ **Guardrail:** Only reactions from users on the session allowlist count. The bot's own reactions (the option emoji it adds) are ignored. If no reaction arrives within `limits.permissionTimeoutSeconds` (default 120), the request is denied. Exception: on modern CLIs, `ExitPlanMode` and `AskUserQuestion` don't post a generic prompt at all — they are forwarded over the session's decision bridge to the bot's plan/question reaction UI and wait up to `DECISION_BRIDGE_TIMEOUT_MS` (default 1 hour) for a decision there. To make that hour real, the bot spawns the CLI with `MCP_TOOL_TIMEOUT=3600000` whenever a bridge is configured — without it the CLI abandons a pending permission call after ~2 minutes (verified against CLI 2.1.223). Multi-select questions and any bridge failure fall back to the legacy behavior: generic prompt for plans, auto-allow for questions.
19
19
 
20
20
  ## send_file
21
21
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "claude-threads",
3
- "version": "1.21.1",
3
+ "version": "1.22.0",
4
4
  "description": "Run Claude Code from Slack or Mattermost. Sessions stream live into threads where your whole team can watch and steer.",
5
5
  "main": "dist/index.js",
6
6
  "type": "module",