claude-threads 1.21.1 → 1.21.2
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 +7 -0
- package/dist/index.js +273 -36
- 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/CHANGELOG.md
CHANGED
|
@@ -5,6 +5,13 @@ All notable changes to this project will be documented in this file.
|
|
|
5
5
|
The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/),
|
|
6
6
|
and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
|
|
7
7
|
|
|
8
|
+
## [1.21.2] - 2026-08-07
|
|
9
|
+
|
|
10
|
+
### Added
|
|
11
|
+
- **Decision bridge: plan approvals and question answers now flow through one surface.** On modern Claude CLIs (verified 2.1.223), `ExitPlanMode` and `AskUserQuestion` block on the MCP permission prompt — making it the authoritative gate — while the rich UI (plan post with 👍/👎, question posts with option reactions) lives in the main bot. Users saw two competing prompts, and reacting only on the bot's UI let the MCP prompt time out into a plan denial. The bot now opens a per-session local socket (a named pipe on Windows); the MCP server forwards plan/question permission requests over it and waits (default 1h, `DECISION_BRIDGE_TIMEOUT_MS`), and the user's reaction on the bot's existing UI resolves them: plans as allow/deny, question answers riding back in the permission response's `updatedInput.answers` — the CLI then tells Claude "Your questions have been answered" / "User has approved your plan" itself (both verified against the real CLI end-to-end, including through the real built `mcp-server.js`). The stdin sends (`'approved'`/answer JSON) are suppressed when a bridge request consumed the decision, and remain as the fallback for older CLIs that don't route these tools through the permission prompt. The bridge degrades gracefully everywhere: creation failure, connect failure, or timeout falls back to the previous behavior (generic prompt for plans, auto-allow for questions). The bridge is session-scoped — it survives `!cd`/`!permissions` respawns and closes with the session; pending decisions are denied on session end or fresh-CLI restart so the MCP child is never stranded. A manual real-CLI verification script ships in `tests/e2e-real-cli/decision-bridge-e2e.ts` (not run in CI — needs live credentials).
|
|
12
|
+
- **Decision-bridge hardening (from two adversarial review rounds over this change).** `!approve`/`!yes` resolves a pending bridge request instead of sending a stdin message that would queue behind the blocked permission call (previously it converted an approval into an hour-long hang). The bridge server aborts the bot-side pending when the requesting MCP client disconnects (timeout, cancelled tool call, dead child), so a stale pending can never swallow the user's later reaction — it falls back to stdin instead. Claude respawns deny pending decisions unconditionally (the MCP child always dies, resume or not; previously only fresh-session respawns did). The bridge socket lives in a fresh `0700` directory (other local users can't connect regardless of umask) with a short path (safe against macOS's 104-byte `sun_path` truncation), and is closed on every teardown path including start/resume failures. multiSelect questions bypass the bridge (single-select answer format is the only one verified against the real CLI) and take the legacy path. Operator knob `DECISION_BRIDGE_TIMEOUT_MS` is now actually forwarded to the MCP child.
|
|
13
|
+
- **The CLI is spawned with `MCP_TOOL_TIMEOUT=3600000` when a decision bridge is configured.** Delayed-decision testing against the real CLI (2.1.223) showed it abandons a pending MCP permission call after ~2 minutes — one retry, then an error — silently capping how long a plan approval or question answer could wait, regardless of the bridge's 1-hour window. With the flag set, decisions held for 150 seconds complete end-to-end (verified empirically); 1 hour matches the bridge's `DECISION_BRIDGE_TIMEOUT_MS` default, and setting `MCP_TOOL_TIMEOUT` in the bot's own env overrides it, like the other forwarded tuning flags. Should the CLI still give up (older CLI, operator override), the bridge's disconnect-abort clears the bot-side pending state so the user's late reaction falls back to the stdin flow instead of being swallowed.
|
|
14
|
+
|
|
8
15
|
## [1.21.1] - 2026-08-07
|
|
9
16
|
|
|
10
17
|
### Fixed
|
package/dist/index.js
CHANGED
|
@@ -14177,7 +14177,7 @@ var require_minimist = __commonJS((exports, module) => {
|
|
|
14177
14177
|
// node_modules/rc/index.js
|
|
14178
14178
|
var require_rc = __commonJS((exports, module) => {
|
|
14179
14179
|
var cc = require_utils();
|
|
14180
|
-
var
|
|
14180
|
+
var join11 = __require("path").join;
|
|
14181
14181
|
var deepExtend = require_deep_extend();
|
|
14182
14182
|
var etc = "/etc";
|
|
14183
14183
|
var win = process.platform === "win32";
|
|
@@ -14203,15 +14203,15 @@ var require_rc = __commonJS((exports, module) => {
|
|
|
14203
14203
|
}
|
|
14204
14204
|
if (!win)
|
|
14205
14205
|
[
|
|
14206
|
-
|
|
14207
|
-
|
|
14206
|
+
join11(etc, name, "config"),
|
|
14207
|
+
join11(etc, name + "rc")
|
|
14208
14208
|
].forEach(addConfigFile);
|
|
14209
14209
|
if (home)
|
|
14210
14210
|
[
|
|
14211
|
-
|
|
14212
|
-
|
|
14213
|
-
|
|
14214
|
-
|
|
14211
|
+
join11(home, ".config", name, "config"),
|
|
14212
|
+
join11(home, ".config", name),
|
|
14213
|
+
join11(home, "." + name, "config"),
|
|
14214
|
+
join11(home, "." + name + "rc")
|
|
14215
14215
|
].forEach(addConfigFile);
|
|
14216
14216
|
addConfigFile(cc.find("." + name + "rc"));
|
|
14217
14217
|
if (env3.config)
|
|
@@ -55960,7 +55960,7 @@ function cleanupBrowserBridgeSockets() {
|
|
|
55960
55960
|
log8.debug(`Browser bridge cleanup failed: ${err}`);
|
|
55961
55961
|
}
|
|
55962
55962
|
}
|
|
55963
|
-
function buildClaudeChildEnv(parentEnv, account) {
|
|
55963
|
+
function buildClaudeChildEnv(parentEnv, account, opts) {
|
|
55964
55964
|
const env = { ...parentEnv };
|
|
55965
55965
|
if (env.MCP_CONNECTION_NONBLOCKING === undefined) {
|
|
55966
55966
|
env.MCP_CONNECTION_NONBLOCKING = "true";
|
|
@@ -55968,6 +55968,9 @@ function buildClaudeChildEnv(parentEnv, account) {
|
|
|
55968
55968
|
if (env.ENABLE_PROMPT_CACHING_1H === undefined) {
|
|
55969
55969
|
env.ENABLE_PROMPT_CACHING_1H = "true";
|
|
55970
55970
|
}
|
|
55971
|
+
if (opts?.decisionBridge && env.MCP_TOOL_TIMEOUT === undefined) {
|
|
55972
|
+
env.MCP_TOOL_TIMEOUT = "3600000";
|
|
55973
|
+
}
|
|
55971
55974
|
if (account?.home) {
|
|
55972
55975
|
env.HOME = account.home;
|
|
55973
55976
|
env.USERPROFILE = account.home;
|
|
@@ -56016,6 +56019,12 @@ function buildPermissionArgs(opts) {
|
|
|
56016
56019
|
PERMISSION_TIMEOUT_MS: String(opts.permissionTimeoutMs),
|
|
56017
56020
|
SESSION_OWNER_USERNAME: opts.sessionOwnerUsername || ""
|
|
56018
56021
|
};
|
|
56022
|
+
if (opts.decisionBridgePath) {
|
|
56023
|
+
mcpEnv.DECISION_BRIDGE_PATH = opts.decisionBridgePath;
|
|
56024
|
+
if (process.env.DECISION_BRIDGE_TIMEOUT_MS) {
|
|
56025
|
+
mcpEnv.DECISION_BRIDGE_TIMEOUT_MS = process.env.DECISION_BRIDGE_TIMEOUT_MS;
|
|
56026
|
+
}
|
|
56027
|
+
}
|
|
56019
56028
|
if (opts.platformConfig.appToken) {
|
|
56020
56029
|
mcpEnv.PLATFORM_APP_TOKEN = opts.platformConfig.appToken;
|
|
56021
56030
|
}
|
|
@@ -56156,7 +56165,8 @@ class ClaudeCli extends EventEmitter2 {
|
|
|
56156
56165
|
workingDir: this.options.workingDir,
|
|
56157
56166
|
uploadDir: this.options.uploadDir,
|
|
56158
56167
|
outboundFiles: this.options.outboundFiles,
|
|
56159
|
-
sessionOwnerUsername: this.options.sessionOwnerUsername
|
|
56168
|
+
sessionOwnerUsername: this.options.sessionOwnerUsername,
|
|
56169
|
+
decisionBridgePath: this.options.decisionBridgePath
|
|
56160
56170
|
});
|
|
56161
56171
|
args.push(...permResult.args);
|
|
56162
56172
|
this.mcpConfigTempFile = permResult.tempFile;
|
|
@@ -56390,7 +56400,9 @@ class ClaudeCli extends EventEmitter2 {
|
|
|
56390
56400
|
return true;
|
|
56391
56401
|
}
|
|
56392
56402
|
buildChildEnv() {
|
|
56393
|
-
return buildClaudeChildEnv(process.env, this.options.account
|
|
56403
|
+
return buildClaudeChildEnv(process.env, this.options.account, {
|
|
56404
|
+
decisionBridge: this.options.decisionBridgePath !== undefined
|
|
56405
|
+
});
|
|
56394
56406
|
}
|
|
56395
56407
|
getMcpServerPath() {
|
|
56396
56408
|
const __filename2 = fileURLToPath3(import.meta.url);
|
|
@@ -57243,6 +57255,109 @@ function isAuthorizedForSession(check) {
|
|
|
57243
57255
|
return sessionAllowedUsers?.has(username) ?? false;
|
|
57244
57256
|
}
|
|
57245
57257
|
|
|
57258
|
+
// src/mcp/decision-bridge.ts
|
|
57259
|
+
import { createServer, createConnection } from "node:net";
|
|
57260
|
+
import { tmpdir as tmpdir2 } from "node:os";
|
|
57261
|
+
import { join as join9 } from "node:path";
|
|
57262
|
+
import { randomUUID as randomUUID2 } from "node:crypto";
|
|
57263
|
+
import { mkdtempSync } from "node:fs";
|
|
57264
|
+
import { rm as rm2 } from "node:fs/promises";
|
|
57265
|
+
|
|
57266
|
+
class BridgeUnavailableError extends Error {
|
|
57267
|
+
}
|
|
57268
|
+
function bridgeSocketPath() {
|
|
57269
|
+
if (process.platform === "win32") {
|
|
57270
|
+
return `\\\\.\\pipe\\ctb-${randomUUID2()}`;
|
|
57271
|
+
}
|
|
57272
|
+
const dir = mkdtempSync(join9(tmpdir2(), "ctb-"));
|
|
57273
|
+
return join9(dir, "b.sock");
|
|
57274
|
+
}
|
|
57275
|
+
|
|
57276
|
+
class DecisionBridgeServer {
|
|
57277
|
+
server;
|
|
57278
|
+
path;
|
|
57279
|
+
liveSockets = new Set;
|
|
57280
|
+
constructor(server, path2) {
|
|
57281
|
+
this.server = server;
|
|
57282
|
+
this.path = path2;
|
|
57283
|
+
}
|
|
57284
|
+
static async create(handler) {
|
|
57285
|
+
const path2 = bridgeSocketPath();
|
|
57286
|
+
const liveSockets = new Set;
|
|
57287
|
+
const server = createServer((socket) => {
|
|
57288
|
+
liveSockets.add(socket);
|
|
57289
|
+
socket.once("close", () => liveSockets.delete(socket));
|
|
57290
|
+
let buffer = "";
|
|
57291
|
+
const aborter = new AbortController;
|
|
57292
|
+
let responded = false;
|
|
57293
|
+
socket.on("data", (chunk) => {
|
|
57294
|
+
buffer += chunk.toString("utf8");
|
|
57295
|
+
const newline = buffer.indexOf(`
|
|
57296
|
+
`);
|
|
57297
|
+
if (newline === -1)
|
|
57298
|
+
return;
|
|
57299
|
+
const line = buffer.slice(0, newline);
|
|
57300
|
+
buffer = "";
|
|
57301
|
+
let request;
|
|
57302
|
+
try {
|
|
57303
|
+
request = JSON.parse(line);
|
|
57304
|
+
} catch {
|
|
57305
|
+
responded = true;
|
|
57306
|
+
socket.end(JSON.stringify({ behavior: "deny", message: "Malformed bridge request" }) + `
|
|
57307
|
+
`);
|
|
57308
|
+
return;
|
|
57309
|
+
}
|
|
57310
|
+
handler(request, aborter.signal).then((response) => {
|
|
57311
|
+
responded = true;
|
|
57312
|
+
socket.end(JSON.stringify(response) + `
|
|
57313
|
+
`);
|
|
57314
|
+
}).catch((err) => {
|
|
57315
|
+
responded = true;
|
|
57316
|
+
if (err instanceof BridgeUnavailableError) {
|
|
57317
|
+
socket.destroy();
|
|
57318
|
+
return;
|
|
57319
|
+
}
|
|
57320
|
+
socket.end(JSON.stringify({
|
|
57321
|
+
behavior: "deny",
|
|
57322
|
+
message: `Bridge handler failed: ${err instanceof Error ? err.message : String(err)}`
|
|
57323
|
+
}) + `
|
|
57324
|
+
`);
|
|
57325
|
+
});
|
|
57326
|
+
});
|
|
57327
|
+
socket.on("close", () => {
|
|
57328
|
+
if (!responded)
|
|
57329
|
+
aborter.abort();
|
|
57330
|
+
});
|
|
57331
|
+
socket.on("error", () => {});
|
|
57332
|
+
});
|
|
57333
|
+
try {
|
|
57334
|
+
await new Promise((resolve4, reject) => {
|
|
57335
|
+
server.once("error", reject);
|
|
57336
|
+
server.listen(path2, () => {
|
|
57337
|
+
server.removeListener("error", reject);
|
|
57338
|
+
resolve4();
|
|
57339
|
+
});
|
|
57340
|
+
});
|
|
57341
|
+
} catch (err) {
|
|
57342
|
+
if (process.platform !== "win32") {
|
|
57343
|
+
await rm2(join9(path2, ".."), { recursive: true, force: true }).catch(() => {});
|
|
57344
|
+
}
|
|
57345
|
+
throw err;
|
|
57346
|
+
}
|
|
57347
|
+
const bridge = new DecisionBridgeServer(server, path2);
|
|
57348
|
+
bridge.liveSockets = liveSockets;
|
|
57349
|
+
return bridge;
|
|
57350
|
+
}
|
|
57351
|
+
async close() {
|
|
57352
|
+
for (const socket of this.liveSockets)
|
|
57353
|
+
socket.destroy();
|
|
57354
|
+
await new Promise((resolve4) => this.server.close(() => resolve4()));
|
|
57355
|
+
if (process.platform !== "win32") {
|
|
57356
|
+
await rm2(join9(this.path, ".."), { recursive: true, force: true }).catch(() => {});
|
|
57357
|
+
}
|
|
57358
|
+
}
|
|
57359
|
+
}
|
|
57360
|
+
|
|
57246
57361
|
// src/commands/registry.ts
|
|
57247
57362
|
var COMMAND_REGISTRY = [
|
|
57248
57363
|
{
|
|
@@ -58183,7 +58298,7 @@ ${avoidCommands.map((c) => `- \`!${c.command}\` - ${c.reason}`).join(`
|
|
|
58183
58298
|
`.trim();
|
|
58184
58299
|
}
|
|
58185
58300
|
// src/session/lifecycle.ts
|
|
58186
|
-
import { randomUUID as
|
|
58301
|
+
import { randomUUID as randomUUID5 } from "crypto";
|
|
58187
58302
|
import { existsSync as existsSync12 } from "fs";
|
|
58188
58303
|
|
|
58189
58304
|
// src/utils/keep-alive.ts
|
|
@@ -58549,23 +58664,23 @@ function updateLastMessage(session, post2) {
|
|
|
58549
58664
|
|
|
58550
58665
|
// src/operations/streaming/handler.ts
|
|
58551
58666
|
init_logger();
|
|
58552
|
-
import { lstat, mkdir as mkdir2, mkdtemp, rm as
|
|
58553
|
-
import { tmpdir as
|
|
58554
|
-
import { join as
|
|
58667
|
+
import { lstat, mkdir as mkdir2, mkdtemp, rm as rm3, writeFile as writeFile2 } from "fs/promises";
|
|
58668
|
+
import { tmpdir as tmpdir3 } from "os";
|
|
58669
|
+
import { join as join10 } from "path";
|
|
58555
58670
|
var log19 = createLogger("streaming");
|
|
58556
58671
|
var UPLOAD_ROOT_DIR = "claude-threads-uploads";
|
|
58557
58672
|
function safeIdSegment(id) {
|
|
58558
58673
|
return id.replace(/[^A-Za-z0-9._-]/g, "_");
|
|
58559
58674
|
}
|
|
58560
58675
|
function getSessionUploadDir(platformId, threadId) {
|
|
58561
|
-
return
|
|
58676
|
+
return join10(tmpdir3(), UPLOAD_ROOT_DIR, `${safeIdSegment(platformId)}-${safeIdSegment(threadId)}`);
|
|
58562
58677
|
}
|
|
58563
58678
|
async function cleanupSessionUploads(platformId, threadId) {
|
|
58564
58679
|
if (!platformId || !threadId)
|
|
58565
58680
|
return;
|
|
58566
58681
|
const dir = getSessionUploadDir(platformId, threadId);
|
|
58567
58682
|
try {
|
|
58568
|
-
await
|
|
58683
|
+
await rm3(dir, { recursive: true, force: true });
|
|
58569
58684
|
} catch (err) {
|
|
58570
58685
|
log19.debug(`Upload cleanup for ${platformId}:${threadId} failed (ignored): ${err}`);
|
|
58571
58686
|
}
|
|
@@ -58591,13 +58706,13 @@ async function saveFilesToUploadDir(platform, uploadDir, files, debug = false) {
|
|
|
58591
58706
|
log19.error(`Upload dir is a symlink, refusing all writes: ${uploadDir}`);
|
|
58592
58707
|
return { saved, skipped };
|
|
58593
58708
|
}
|
|
58594
|
-
const messageDir = await mkdtemp(
|
|
58709
|
+
const messageDir = await mkdtemp(join10(uploadDir, `${Date.now().toString(36)}-`));
|
|
58595
58710
|
const usedNames = new Set;
|
|
58596
58711
|
for (const file of files) {
|
|
58597
58712
|
try {
|
|
58598
58713
|
const buffer = await platform.downloadFile(file.id);
|
|
58599
58714
|
const safeName = dedupeFilename(sanitizeFilename(file.name), usedNames);
|
|
58600
|
-
const absolutePath =
|
|
58715
|
+
const absolutePath = join10(messageDir, safeName);
|
|
58601
58716
|
await writeFile2(absolutePath, buffer, { mode: 384, flag: "wx" });
|
|
58602
58717
|
saved.push({
|
|
58603
58718
|
originalName: file.name,
|
|
@@ -58680,12 +58795,13 @@ function buildRestartCliOptions(session, ctx) {
|
|
|
58680
58795
|
account: ctx.account,
|
|
58681
58796
|
uploadDir: getSessionUploadDir(session.platformId, session.threadId),
|
|
58682
58797
|
outboundFiles: platformMcpConfig.outboundFiles,
|
|
58683
|
-
sessionOwnerUsername: session.startedBy
|
|
58798
|
+
sessionOwnerUsername: session.startedBy,
|
|
58799
|
+
decisionBridgePath: session.decisionBridge?.path
|
|
58684
58800
|
};
|
|
58685
58801
|
}
|
|
58686
58802
|
|
|
58687
58803
|
// src/operations/commands/handler.ts
|
|
58688
|
-
import { randomUUID as
|
|
58804
|
+
import { randomUUID as randomUUID4 } from "crypto";
|
|
58689
58805
|
import { resolve as resolve5 } from "path";
|
|
58690
58806
|
import { existsSync as existsSync11, statSync as statSync3 } from "fs";
|
|
58691
58807
|
|
|
@@ -62103,8 +62219,8 @@ init_emoji();
|
|
|
62103
62219
|
// src/operations/bug-report/handler.ts
|
|
62104
62220
|
import { execSync as execSync2 } from "child_process";
|
|
62105
62221
|
import { writeFileSync as writeFileSync6, unlinkSync as unlinkSync3 } from "fs";
|
|
62106
|
-
import { tmpdir as
|
|
62107
|
-
import { join as
|
|
62222
|
+
import { tmpdir as tmpdir4 } from "os";
|
|
62223
|
+
import { join as join11 } from "path";
|
|
62108
62224
|
|
|
62109
62225
|
// node_modules/@redactpii/node/lib/index.mjs
|
|
62110
62226
|
class Redactor {
|
|
@@ -62684,7 +62800,7 @@ async function createGitHubIssue(title, body, workingDir) {
|
|
|
62684
62800
|
if (!ghStatus.installed || !ghStatus.authenticated) {
|
|
62685
62801
|
throw new Error(ghStatus.error);
|
|
62686
62802
|
}
|
|
62687
|
-
const bodyFile =
|
|
62803
|
+
const bodyFile = join11(tmpdir4(), `bug-body-${Date.now()}.md`);
|
|
62688
62804
|
try {
|
|
62689
62805
|
writeFileSync6(bodyFile, body, "utf-8");
|
|
62690
62806
|
const cmd = `gh issue create --repo "${GITHUB_REPO}" --title "${escapeShell(title)}" --body-file "${bodyFile}"`;
|
|
@@ -66717,6 +66833,8 @@ class MessageManager {
|
|
|
66717
66833
|
emitSessionUpdateCallback;
|
|
66718
66834
|
toolStartTimes = new Map;
|
|
66719
66835
|
taskTracker = new TaskTracker;
|
|
66836
|
+
pendingBridgePlan = null;
|
|
66837
|
+
pendingBridgeQuestion = null;
|
|
66720
66838
|
flushTimer = null;
|
|
66721
66839
|
static DEFAULT_FLUSH_DELAY_MS = 500;
|
|
66722
66840
|
flushDelayMs;
|
|
@@ -67178,9 +67296,66 @@ class MessageManager {
|
|
|
67178
67296
|
logger.debug("Reaction not handled by any executor");
|
|
67179
67297
|
return false;
|
|
67180
67298
|
}
|
|
67299
|
+
handleBridgeRequest(request, signal) {
|
|
67300
|
+
if (request.kind === "plan_approval") {
|
|
67301
|
+
this.pendingBridgePlan?.resolve({ behavior: "deny", message: "Superseded by a newer plan" });
|
|
67302
|
+
return new Promise((resolve5) => {
|
|
67303
|
+
const pending = { resolve: resolve5, input: request.input };
|
|
67304
|
+
this.pendingBridgePlan = pending;
|
|
67305
|
+
signal?.addEventListener("abort", () => {
|
|
67306
|
+
if (this.pendingBridgePlan === pending)
|
|
67307
|
+
this.pendingBridgePlan = null;
|
|
67308
|
+
});
|
|
67309
|
+
});
|
|
67310
|
+
}
|
|
67311
|
+
if (request.kind === "question") {
|
|
67312
|
+
this.pendingBridgeQuestion?.resolve({ behavior: "deny", message: "Superseded by newer questions" });
|
|
67313
|
+
return new Promise((resolve5) => {
|
|
67314
|
+
const pending = { resolve: resolve5, input: request.input };
|
|
67315
|
+
this.pendingBridgeQuestion = pending;
|
|
67316
|
+
signal?.addEventListener("abort", () => {
|
|
67317
|
+
if (this.pendingBridgeQuestion === pending)
|
|
67318
|
+
this.pendingBridgeQuestion = null;
|
|
67319
|
+
});
|
|
67320
|
+
});
|
|
67321
|
+
}
|
|
67322
|
+
return Promise.resolve({ behavior: "deny", message: `Unknown bridge request kind` });
|
|
67323
|
+
}
|
|
67324
|
+
resolveBridgePlan(approved) {
|
|
67325
|
+
const pending = this.pendingBridgePlan;
|
|
67326
|
+
if (!pending)
|
|
67327
|
+
return false;
|
|
67328
|
+
this.pendingBridgePlan = null;
|
|
67329
|
+
pending.resolve(approved ? { behavior: "allow", updatedInput: pending.input } : { behavior: "deny", message: "The user rejected the plan." });
|
|
67330
|
+
return true;
|
|
67331
|
+
}
|
|
67332
|
+
resolveBridgeQuestion(answers) {
|
|
67333
|
+
const pending = this.pendingBridgeQuestion;
|
|
67334
|
+
if (!pending)
|
|
67335
|
+
return false;
|
|
67336
|
+
this.pendingBridgeQuestion = null;
|
|
67337
|
+
const questions = Array.isArray(pending.input.questions) ? pending.input.questions : [];
|
|
67338
|
+
const record = {};
|
|
67339
|
+
for (const a of answers) {
|
|
67340
|
+
const match = questions.find((q) => q.header === a.header);
|
|
67341
|
+
record[match?.question ?? a.header] = a.answer;
|
|
67342
|
+
}
|
|
67343
|
+
pending.resolve({
|
|
67344
|
+
behavior: "allow",
|
|
67345
|
+
updatedInput: { ...pending.input, answers: record }
|
|
67346
|
+
});
|
|
67347
|
+
return true;
|
|
67348
|
+
}
|
|
67349
|
+
denyPendingBridgeRequests(reason) {
|
|
67350
|
+
this.pendingBridgePlan?.resolve({ behavior: "deny", message: reason });
|
|
67351
|
+
this.pendingBridgePlan = null;
|
|
67352
|
+
this.pendingBridgeQuestion?.resolve({ behavior: "deny", message: reason });
|
|
67353
|
+
this.pendingBridgeQuestion = null;
|
|
67354
|
+
}
|
|
67181
67355
|
clearClaudeSessionState() {
|
|
67182
67356
|
this.toolStartTimes.clear();
|
|
67183
67357
|
this.taskTracker.clear();
|
|
67358
|
+
this.denyPendingBridgeRequests("Claude was restarted before a decision was made");
|
|
67184
67359
|
}
|
|
67185
67360
|
reset() {
|
|
67186
67361
|
this.cancelScheduledFlush();
|
|
@@ -67196,6 +67371,7 @@ class MessageManager {
|
|
|
67196
67371
|
this.systemExecutor.reset();
|
|
67197
67372
|
}
|
|
67198
67373
|
dispose() {
|
|
67374
|
+
this.denyPendingBridgeRequests("Session ended before a decision was made");
|
|
67199
67375
|
this.cancelScheduledFlush();
|
|
67200
67376
|
this.postTracker.clearSession(this.sessionId);
|
|
67201
67377
|
this.events.removeAllListeners();
|
|
@@ -67976,7 +68152,7 @@ async function suggestBranchNames(workingDir, userMessage) {
|
|
|
67976
68152
|
|
|
67977
68153
|
// src/operations/worktree/handler.ts
|
|
67978
68154
|
init_worktree();
|
|
67979
|
-
import { randomUUID as
|
|
68155
|
+
import { randomUUID as randomUUID3 } from "crypto";
|
|
67980
68156
|
init_logger();
|
|
67981
68157
|
var log25 = createLogger("worktree");
|
|
67982
68158
|
var sessionLog2 = createSessionLog(log25);
|
|
@@ -68223,7 +68399,7 @@ async function createAndSwitchToWorktree(session, branch, username, options) {
|
|
|
68223
68399
|
transitionTo(session, "restarting");
|
|
68224
68400
|
await session.claude.kill();
|
|
68225
68401
|
await options.flush(session);
|
|
68226
|
-
const newSessionId =
|
|
68402
|
+
const newSessionId = randomUUID3();
|
|
68227
68403
|
session.claudeSessionId = newSessionId;
|
|
68228
68404
|
const needsTitlePrompt = !session.sessionTitle;
|
|
68229
68405
|
const cliOptions = {
|
|
@@ -68320,7 +68496,7 @@ ${fmt.formatItalic("Claude Code restarted in the worktree")}`);
|
|
|
68320
68496
|
transitionTo(session, "restarting");
|
|
68321
68497
|
await session.claude.kill();
|
|
68322
68498
|
await options.flush(session);
|
|
68323
|
-
const newSessionId =
|
|
68499
|
+
const newSessionId = randomUUID3();
|
|
68324
68500
|
session.claudeSessionId = newSessionId;
|
|
68325
68501
|
const needsTitlePrompt = !session.sessionTitle;
|
|
68326
68502
|
const cliOptions = {
|
|
@@ -69244,6 +69420,7 @@ async function restartClaudeSession(session, cliOptions, ctx, actionName) {
|
|
|
69244
69420
|
transitionTo(session, "restarting");
|
|
69245
69421
|
await session.claude.kill();
|
|
69246
69422
|
await ctx.ops.flush(session);
|
|
69423
|
+
session.messageManager?.denyPendingBridgeRequests("Claude was restarted before a decision was made");
|
|
69247
69424
|
if (!cliOptions.resume) {
|
|
69248
69425
|
session.messageManager?.clearClaudeSessionState();
|
|
69249
69426
|
}
|
|
@@ -69325,12 +69502,18 @@ async function approvePendingPlan(session, username, ctx) {
|
|
|
69325
69502
|
}
|
|
69326
69503
|
const { postId } = pendingApproval;
|
|
69327
69504
|
sessionLog5(session).info(`✅ Plan approved by @${username} via command`);
|
|
69505
|
+
const viaBridge = session.messageManager?.resolveBridgePlan(true) ?? false;
|
|
69328
69506
|
const formatter = session.platform.getFormatter();
|
|
69329
69507
|
const statusMessage = `${formatter.formatBold("Plan approved")} by ${formatter.formatUserMention(username)} - starting implementation...`;
|
|
69330
69508
|
await updatePostSuccess(session, postId, statusMessage);
|
|
69331
69509
|
session.messageManager?.clearPendingApproval();
|
|
69332
69510
|
session.messageManager?.clearPendingQuestionSet();
|
|
69333
69511
|
session.planApproved = true;
|
|
69512
|
+
if (viaBridge) {
|
|
69513
|
+
sessionLog5(session).info("Plan approved via decision bridge (!approve)");
|
|
69514
|
+
ctx.ops.startTyping(session);
|
|
69515
|
+
return;
|
|
69516
|
+
}
|
|
69334
69517
|
if (session.claude.isRunning()) {
|
|
69335
69518
|
session.claude.sendMessage("Plan approved! Please proceed with the implementation.");
|
|
69336
69519
|
ctx.ops.startTyping(session);
|
|
@@ -69393,7 +69576,7 @@ async function changeDirectory(session, newDir, username, ctx) {
|
|
|
69393
69576
|
sessionLog5(session).debug(`Stored work summary for context preservation`);
|
|
69394
69577
|
}
|
|
69395
69578
|
session.workingDir = absoluteDir;
|
|
69396
|
-
const newSessionId =
|
|
69579
|
+
const newSessionId = randomUUID4();
|
|
69397
69580
|
session.claudeSessionId = newSessionId;
|
|
69398
69581
|
const appendSystemPrompt = await buildAppendSystemPrompt(session.platform, session.platformId, absoluteDir, session.threadId, session.startedBy, session.sessionAllowedUsers, CHAT_PLATFORM_PROMPT, ctx.state.githubEmailsStore, { userAttribution: session.userAttribution });
|
|
69399
69582
|
const cliOptions = {
|
|
@@ -69910,6 +70093,8 @@ async function cleanupSession(session, ctx, options = {}) {
|
|
|
69910
70093
|
await closeThreadLogger(session, action, details);
|
|
69911
70094
|
}
|
|
69912
70095
|
session.messageManager?.dispose();
|
|
70096
|
+
session.decisionBridge?.close();
|
|
70097
|
+
session.decisionBridge = undefined;
|
|
69913
70098
|
ctx.ops.emitSessionRemove(session.sessionId);
|
|
69914
70099
|
mutableSessions(ctx).delete(session.sessionId);
|
|
69915
70100
|
if (doCleanupPostIndex) {
|
|
@@ -69927,6 +70112,8 @@ function releaseAccountIfHeld(session, ctx) {
|
|
|
69927
70112
|
}
|
|
69928
70113
|
function removeFromRegistry(session, ctx) {
|
|
69929
70114
|
session.messageManager?.dispose();
|
|
70115
|
+
session.decisionBridge?.close();
|
|
70116
|
+
session.decisionBridge = undefined;
|
|
69930
70117
|
ctx.ops.emitSessionRemove(session.sessionId);
|
|
69931
70118
|
mutableSessions(ctx).delete(session.sessionId);
|
|
69932
70119
|
cleanupPostIndex(ctx, session.threadId);
|
|
@@ -69952,6 +70139,20 @@ function findPersistedByThreadId(persisted, threadId) {
|
|
|
69952
70139
|
}
|
|
69953
70140
|
return;
|
|
69954
70141
|
}
|
|
70142
|
+
async function createSessionDecisionBridge(ref) {
|
|
70143
|
+
try {
|
|
70144
|
+
return await DecisionBridgeServer.create(async (request, signal) => {
|
|
70145
|
+
const messageManager = ref.current?.messageManager;
|
|
70146
|
+
if (!messageManager) {
|
|
70147
|
+
throw new BridgeUnavailableError("Session is not ready for decisions yet");
|
|
70148
|
+
}
|
|
70149
|
+
return messageManager.handleBridgeRequest(request, signal);
|
|
70150
|
+
});
|
|
70151
|
+
} catch (err) {
|
|
70152
|
+
log31.warn(`Decision bridge unavailable — falling back to legacy MCP prompts: ${err}`);
|
|
70153
|
+
return null;
|
|
70154
|
+
}
|
|
70155
|
+
}
|
|
69955
70156
|
function createMessageManager(session, ctx) {
|
|
69956
70157
|
const postTracker = new PostTracker;
|
|
69957
70158
|
const messageManager = new MessageManager({
|
|
@@ -69982,10 +70183,20 @@ function createMessageManager(session, ctx) {
|
|
|
69982
70183
|
flushDelayMs: ctx.config.flushDelayMs
|
|
69983
70184
|
});
|
|
69984
70185
|
messageManager.events.on("question:complete", ({ toolUseId: _toolUseId, answers }) => {
|
|
70186
|
+
if (messageManager.resolveBridgeQuestion(answers)) {
|
|
70187
|
+
sessionLog6(session).info("Question answered via decision bridge");
|
|
70188
|
+
ctx.ops.startTyping(session);
|
|
70189
|
+
return;
|
|
70190
|
+
}
|
|
69985
70191
|
const answerJson = JSON.stringify(answers);
|
|
69986
70192
|
session.claude.sendMessage(answerJson);
|
|
69987
70193
|
});
|
|
69988
70194
|
messageManager.events.on("approval:complete", ({ toolUseId: _toolUseId, approved }) => {
|
|
70195
|
+
if (messageManager.resolveBridgePlan(approved)) {
|
|
70196
|
+
sessionLog6(session).info(`Plan ${approved ? "approved" : "denied"} via decision bridge`);
|
|
70197
|
+
ctx.ops.startTyping(session);
|
|
70198
|
+
return;
|
|
70199
|
+
}
|
|
69989
70200
|
const response = approved ? "approved" : "denied";
|
|
69990
70201
|
session.claude.sendMessage(response);
|
|
69991
70202
|
});
|
|
@@ -70251,7 +70462,7 @@ async function startSession(options, username, displayName, replyToPostId, platf
|
|
|
70251
70462
|
const actualThreadId = replyToPostId || (startPost ? startPost.id : "");
|
|
70252
70463
|
const sessionId = ctx.ops.getSessionId(platformId, actualThreadId);
|
|
70253
70464
|
platform.sendTyping(actualThreadId);
|
|
70254
|
-
const claudeSessionId =
|
|
70465
|
+
const claudeSessionId = randomUUID5();
|
|
70255
70466
|
let workingDir = ctx.config.workingDir;
|
|
70256
70467
|
let permissionMode = ctx.config.permissionMode;
|
|
70257
70468
|
let forceInteractivePermissions = false;
|
|
@@ -70305,6 +70516,8 @@ async function startSession(options, username, displayName, replyToPostId, platf
|
|
|
70305
70516
|
if (claudeAccount) {
|
|
70306
70517
|
log31.info(`Session ${sessionId.substring(0, 20)} reserved Claude account "${claudeAccount.id}"`);
|
|
70307
70518
|
}
|
|
70519
|
+
const bridgeSessionRef = {};
|
|
70520
|
+
const decisionBridge = await createSessionDecisionBridge(bridgeSessionRef);
|
|
70308
70521
|
const cliOptions = {
|
|
70309
70522
|
workingDir,
|
|
70310
70523
|
threadId: actualThreadId,
|
|
@@ -70319,9 +70532,16 @@ async function startSession(options, username, displayName, replyToPostId, platf
|
|
|
70319
70532
|
account: claudeAccount ? { id: claudeAccount.id, home: claudeAccount.home, apiKey: claudeAccount.apiKey } : undefined,
|
|
70320
70533
|
uploadDir: getSessionUploadDir(platformId, actualThreadId),
|
|
70321
70534
|
outboundFiles: platformMcpConfig.outboundFiles,
|
|
70322
|
-
sessionOwnerUsername: username
|
|
70535
|
+
sessionOwnerUsername: username,
|
|
70536
|
+
decisionBridgePath: decisionBridge?.path
|
|
70323
70537
|
};
|
|
70324
|
-
|
|
70538
|
+
let claude;
|
|
70539
|
+
try {
|
|
70540
|
+
claude = new ClaudeCli(cliOptions);
|
|
70541
|
+
} catch (err) {
|
|
70542
|
+
decisionBridge?.close();
|
|
70543
|
+
throw err;
|
|
70544
|
+
}
|
|
70325
70545
|
const session = {
|
|
70326
70546
|
platformId,
|
|
70327
70547
|
threadId: actualThreadId,
|
|
@@ -70355,7 +70575,9 @@ async function startSession(options, username, displayName, replyToPostId, platf
|
|
|
70355
70575
|
enabled: ctx.config.threadLogsEnabled ?? true
|
|
70356
70576
|
})
|
|
70357
70577
|
};
|
|
70578
|
+
session.decisionBridge = decisionBridge ?? undefined;
|
|
70358
70579
|
session.messageManager = createMessageManager(session, ctx);
|
|
70580
|
+
bridgeSessionRef.current = session;
|
|
70359
70581
|
session.threadLogger?.logLifecycle("start", {
|
|
70360
70582
|
username,
|
|
70361
70583
|
workingDir: ctx.config.workingDir
|
|
@@ -70382,6 +70604,8 @@ async function startSession(options, username, displayName, replyToPostId, platf
|
|
|
70382
70604
|
await logAndNotify(err, { action: "Start Claude", session });
|
|
70383
70605
|
ctx.ops.stopTyping(session);
|
|
70384
70606
|
session.messageManager?.dispose();
|
|
70607
|
+
session.decisionBridge?.close();
|
|
70608
|
+
session.decisionBridge = undefined;
|
|
70385
70609
|
ctx.ops.emitSessionRemove(session.sessionId);
|
|
70386
70610
|
mutableSessions(ctx).delete(session.sessionId);
|
|
70387
70611
|
releaseAccountIfHeld(session, ctx);
|
|
@@ -70465,6 +70689,8 @@ Please start a new session.`), { action: "Post resume failure notification" });
|
|
|
70465
70689
|
if (state.claudeAccountId && !claudeAccount) {
|
|
70466
70690
|
log31.warn(`Persisted session referenced Claude account "${state.claudeAccountId}" ` + `which is no longer configured — resuming under default env`);
|
|
70467
70691
|
}
|
|
70692
|
+
const resumeBridgeRef = {};
|
|
70693
|
+
const resumeBridge = await createSessionDecisionBridge(resumeBridgeRef);
|
|
70468
70694
|
const cliOptions = {
|
|
70469
70695
|
workingDir: state.workingDir,
|
|
70470
70696
|
threadId: state.threadId,
|
|
@@ -70479,9 +70705,16 @@ Please start a new session.`), { action: "Post resume failure notification" });
|
|
|
70479
70705
|
account: claudeAccount ? { id: claudeAccount.id, home: claudeAccount.home, apiKey: claudeAccount.apiKey } : undefined,
|
|
70480
70706
|
uploadDir: getSessionUploadDir(platformId, state.threadId),
|
|
70481
70707
|
outboundFiles: platformMcpConfig.outboundFiles,
|
|
70482
|
-
sessionOwnerUsername: state.startedBy
|
|
70708
|
+
sessionOwnerUsername: state.startedBy,
|
|
70709
|
+
decisionBridgePath: resumeBridge?.path
|
|
70483
70710
|
};
|
|
70484
|
-
|
|
70711
|
+
let claude;
|
|
70712
|
+
try {
|
|
70713
|
+
claude = new ClaudeCli(cliOptions);
|
|
70714
|
+
} catch (err) {
|
|
70715
|
+
resumeBridge?.close();
|
|
70716
|
+
throw err;
|
|
70717
|
+
}
|
|
70485
70718
|
const session = {
|
|
70486
70719
|
platformId,
|
|
70487
70720
|
threadId: state.threadId,
|
|
@@ -70527,6 +70760,7 @@ Please start a new session.`), { action: "Post resume failure notification" });
|
|
|
70527
70760
|
enabled: ctx.config.threadLogsEnabled ?? true
|
|
70528
70761
|
})
|
|
70529
70762
|
};
|
|
70763
|
+
session.decisionBridge = resumeBridge ?? undefined;
|
|
70530
70764
|
if (!session.worktreeInfo) {
|
|
70531
70765
|
const detected = await detectWorktreeInfo(session.workingDir);
|
|
70532
70766
|
if (detected) {
|
|
@@ -70539,6 +70773,7 @@ Please start a new session.`), { action: "Post resume failure notification" });
|
|
|
70539
70773
|
}
|
|
70540
70774
|
}
|
|
70541
70775
|
session.messageManager = createMessageManager(session, ctx);
|
|
70776
|
+
resumeBridgeRef.current = session;
|
|
70542
70777
|
await session.messageManager.restoreTaskListFromPersistence({
|
|
70543
70778
|
tasksPostId: state.tasksPostId,
|
|
70544
70779
|
lastTasksContent: state.lastTasksContent,
|
|
@@ -70594,6 +70829,8 @@ ${sessionFormatter.formatItalic("Reconnected to Claude session. You can continue
|
|
|
70594
70829
|
} catch (err) {
|
|
70595
70830
|
log31.error(`Failed to resume session ${shortId}`, err instanceof Error ? err : undefined);
|
|
70596
70831
|
session.messageManager?.dispose();
|
|
70832
|
+
session.decisionBridge?.close();
|
|
70833
|
+
session.decisionBridge = undefined;
|
|
70597
70834
|
ctx.ops.emitSessionRemove(sessionId);
|
|
70598
70835
|
mutableSessions(ctx).delete(sessionId);
|
|
70599
70836
|
ctx.state.sessionStore.remove(sessionId);
|
|
@@ -82588,7 +82825,7 @@ class UpdateInstaller {
|
|
|
82588
82825
|
init_logger();
|
|
82589
82826
|
import { spawn as spawn5 } from "child_process";
|
|
82590
82827
|
import { existsSync as existsSync15, statSync as statSync4 } from "fs";
|
|
82591
|
-
import { delimiter, join as
|
|
82828
|
+
import { delimiter, join as join12 } from "path";
|
|
82592
82829
|
var log39 = createLogger("respawn");
|
|
82593
82830
|
function decideRespawn(env5 = process.env, isTTY = !!process.stdout.isTTY) {
|
|
82594
82831
|
if (env5.CLAUDE_THREADS_BIN) {
|
|
@@ -82614,16 +82851,16 @@ function resolveClaudeThreadsBin(_env = process.env, _existsSync = existsSync15,
|
|
|
82614
82851
|
const path10 = _env.PATH || _env.Path || "";
|
|
82615
82852
|
const dirs = path10.split(delimiter).filter(Boolean);
|
|
82616
82853
|
const home = _env.HOME || _env.USERPROFILE;
|
|
82617
|
-
const bunRoot = _env.BUN_INSTALL || (home ?
|
|
82854
|
+
const bunRoot = _env.BUN_INSTALL || (home ? join12(home, ".bun") : null);
|
|
82618
82855
|
if (bunRoot) {
|
|
82619
|
-
const bunBin =
|
|
82856
|
+
const bunBin = join12(bunRoot, "bin");
|
|
82620
82857
|
if (!dirs.includes(bunBin)) {
|
|
82621
82858
|
dirs.push(bunBin);
|
|
82622
82859
|
}
|
|
82623
82860
|
}
|
|
82624
82861
|
for (const dir of dirs) {
|
|
82625
82862
|
for (const name of names) {
|
|
82626
|
-
const candidate =
|
|
82863
|
+
const candidate = join12(dir, name);
|
|
82627
82864
|
if (_existsSync(candidate) && _isFileExecutable(candidate)) {
|
|
82628
82865
|
return candidate;
|
|
82629
82866
|
}
|
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