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.
- package/CHANGELOG.md +12 -0
- package/dist/index.js +369 -97
- package/dist/mcp/mcp-server.js +246 -14
- package/docs/CONFIGURATION.md +4 -2
- package/docs/MCP-TOOLS.md +1 -1
- package/package.json +1 -1
package/dist/mcp/mcp-server.js
CHANGED
|
@@ -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
|
|
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 =
|
|
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 =
|
|
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 ??
|
|
55917
|
-
const path =
|
|
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 =
|
|
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
|
|
57222
|
+
import { join as join6 } from "path";
|
|
57009
57223
|
var log18 = createLogger("gh-emails");
|
|
57010
|
-
var DEFAULT_CONFIG_DIR =
|
|
57011
|
-
var DEFAULT_FILE =
|
|
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 = {
|
package/docs/CONFIGURATION.md
CHANGED
|
@@ -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
|
|
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
|
|
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