claude-threads 1.15.2 → 1.16.1
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 +26 -0
- package/dist/index.js +301 -69
- package/dist/mcp/mcp-server.js +130 -43
- package/docs/CONFIGURATION.md +21 -0
- package/package.json +7 -5
package/CHANGELOG.md
CHANGED
|
@@ -7,6 +7,32 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
|
|
|
7
7
|
|
|
8
8
|
## [Unreleased]
|
|
9
9
|
|
|
10
|
+
## [1.16.1] - 2026-05-22
|
|
11
|
+
|
|
12
|
+
### Security
|
|
13
|
+
- **Fail-closed authorization on the user-driven paths that invoke Claude.** A confirmed bypass let an unauthorized Slack user (not in `allowedUsers`, non-empty allowlist) reach Claude: the bot posted both the "not authorized" warning and a real answer in the same thread. Rather than chase the one leaking caller, authorization is now deny-by-default at the functions that spawn or message Claude on a user's behalf: `startSession`, `sendFollowUp`, and `resumePausedSession`, plus the resume-from-reaction path. A single helper, `isAuthorizedForSession`, encodes every tier (global allowlist, empty-allowlist allow-all, per-session owner and invited users) and refuses a missing, empty, or `unknown` username. Every user-facing path now routes through that one helper instead of duplicating the check. The biggest gap was message-driven resume, which ran purely from persisted state with no identity check; it now takes a `username` and verifies it against the persisted session allowlist before resuming. The two system-triggered resume paths (bulk restore after a bot restart) stay ungated by design, since they carry no user identity. Legitimate username-less follow-ups (passthrough slash commands like `/context`, already authorized upstream) pass an explicit `system` flag so they are not caught. The mid-session approval flow is untouched: it still adds approved users to the session allowlist, so the check passes once approval is granted. (#388)
|
|
14
|
+
- **hono 4.12.18 → 4.12.21** picks up four upstream security fixes: GHSA-2gcr-mfcq-wcc3 (`app.mount()` stripped the mount prefix from the raw, undecoded pathname, so percent-encoded paths could be cut at the wrong offset and reach a sub-app with the wrong path), GHSA-xrhx-7g5j-rcj5 (`hono/ip-restriction` compared IPs by string equality, so non-canonical IPv6 forms such as compressed or hex IPv4-mapped addresses slipped past static deny rules), GHSA-3hrh-pfw6-9m5x (the cookie helper didn't sanitize `sameSite`/`priority`, allowing Set-Cookie injection via `;`, `\r`, `\n`), and GHSA-f577-qrjj-4474 (`hono/jwt` accepted any two-part Authorization header regardless of scheme, not just `Bearer`). claude-threads reaches hono through `@hono/node-server` on the inbound webhook surface, so the mount-prefix and Set-Cookie fixes are the relevant ones. (#386)
|
|
15
|
+
|
|
16
|
+
### Changed
|
|
17
|
+
- **Production deps** bumped: `@hono/node-server` 2.0.2 → 2.0.3 (preserves headers mutated after raw Response construction), `express-rate-limit` 8.5.1 → 8.5.2 (simplified IPv6 key generation). Lockfile-only. (#386)
|
|
18
|
+
- **Dev deps** bumped: `@types/bun` 1.3.13 → 1.3.14, `@types/node` 25.7.0 → 25.9.1, `@types/react` 19.2.14 → 19.2.15, `eslint` 10.3.0 → 10.4.0 (adds `includeIgnoreFile()` and a `for-direction` sequence-expression check, both additive), `lint-staged` 17.0.4 → 17.0.5, `typescript-eslint` 8.59.3 → 8.59.4 (fixes a `no-floating-promises` stack overflow on recursive types). Lockfile-only. (#385)
|
|
19
|
+
|
|
20
|
+
### Fixed
|
|
21
|
+
- **Multiple pasted screenshots no longer silently vanish.** When you paste several clipboard images into one chat message, the platforms hand them all the same filename (`image.png`). The save path wrote each one with the `wx` (`O_EXCL`) flag, so the second and later files hit `EEXIST`, got caught, and were reported as a download failure, so they never reached Claude. Saves now dedupe filenames within a single message, inserting a numeric suffix before the extension (`image.png`, `image_1.png`, `image_2.png`); files without an extension get `report`, `report_1`, and so on. The unique name is resolved before the write, so the `wx` flag still does its job against symlink races. (#387)
|
|
22
|
+
- **Inbound attachments are no longer capped at 100 MB.** A hard 100 MB ceiling on incoming files meant anything larger was skipped with a "too large" warning, which surprised people sharing big assets. The cap is gone; an attachment of any reported size is downloaded and written to disk. One tradeoff worth knowing: `downloadFile` still buffers the whole file in memory via `arrayBuffer()`, so a very large attachment is held in RAM while it's written. Streaming that download is a separate change. (#387)
|
|
23
|
+
|
|
24
|
+
## [1.16.0] - 2026-05-14
|
|
25
|
+
|
|
26
|
+
### Added
|
|
27
|
+
- **Per-platform channel-verbosity controls.** New `sessionHeader` and `stickyMessage` settings, both `full` (default) / `minimal` (one-line status bar) / `hidden` (no post). Reachable three ways: setup wizard ("How verbose should the bot be in this channel?"), CLI flags (`--session-header`, `--sticky-message`, applied to every platform), or per-platform YAML for split values. `hidden` for the header means Claude's reply is the first message in the thread; `hidden` for the sticky stops the `channel_post` bump entirely. Update notices still ride along in `minimal`. Session-header mode persists per session — resume preserves the user's choice. Old `sessions.json` defaults to `full`. The pre-existing top-level `stickyMessage: { description, footer }` block is unchanged. (#384, closes #383)
|
|
28
|
+
|
|
29
|
+
### Security
|
|
30
|
+
- **`ws` 8.20.0 → 8.20.1** picks up a fix for an uninitialized memory disclosure in `websocket.close()`. Triggered when a `TypedArray` (e.g. `Float32Array`) is passed as the `reason` argument instead of a string or `Buffer` — uninitialized memory was leaked to the remote peer. claude-threads doesn't pass typed arrays to `close()` anywhere we control, but the Mattermost client and inbound webhook surface use `ws` as a transitive dependency. (#382)
|
|
31
|
+
|
|
32
|
+
### Changed
|
|
33
|
+
- **Production deps** bumped: `@hono/node-server` 2.0.1 → 2.0.2 (serve-static fixes), `react` 19.2.5 → 19.2.6 (RSC type hardening), `semver` 7.7.4 → 7.8.0 (new `truncate` function), `ink-scroll-view` 0.3.6 → 0.3.7. Lockfile-only. (#382)
|
|
34
|
+
- **Dev deps** bumped: `@types/node` 25.6.0 → 25.7.0, `lint-staged` 16.4.0 → 17.0.4, `typescript-eslint` 8.59.2 → 8.59.3. lint-staged 17 drops Node 20 support, but it's a dev dep — the bot's runtime floor is unchanged. Lockfile-only. (#381)
|
|
35
|
+
|
|
10
36
|
## [1.15.2] - 2026-05-06
|
|
11
37
|
|
|
12
38
|
### Security
|
package/dist/index.js
CHANGED
|
@@ -48344,6 +48344,18 @@ var jsYaml = {
|
|
|
48344
48344
|
};
|
|
48345
48345
|
|
|
48346
48346
|
// src/config/types.ts
|
|
48347
|
+
var OVERHEAD_VISIBILITY_VALUES = ["full", "minimal", "hidden"];
|
|
48348
|
+
var DEFAULT_OVERHEAD_VISIBILITY = "full";
|
|
48349
|
+
function isOverheadVisibility(value) {
|
|
48350
|
+
return typeof value === "string" && OVERHEAD_VISIBILITY_VALUES.includes(value);
|
|
48351
|
+
}
|
|
48352
|
+
function resolveOverheadVisibility(value, fieldPath) {
|
|
48353
|
+
if (value === undefined || value === null)
|
|
48354
|
+
return DEFAULT_OVERHEAD_VISIBILITY;
|
|
48355
|
+
if (isOverheadVisibility(value))
|
|
48356
|
+
return value;
|
|
48357
|
+
throw new Error(`Invalid ${fieldPath}: expected one of ${OVERHEAD_VISIBILITY_VALUES.join(", ")}, got ${JSON.stringify(value)}`);
|
|
48358
|
+
}
|
|
48347
48359
|
var LIMITS_DEFAULTS = {
|
|
48348
48360
|
maxSessions: 5,
|
|
48349
48361
|
sessionTimeoutMinutes: 30,
|
|
@@ -48648,6 +48660,26 @@ var PERMISSION_MODE_CHOICES = [
|
|
|
48648
48660
|
function permissionModeChoiceIndex(mode) {
|
|
48649
48661
|
return PERMISSION_MODE_CHOICES.findIndex((c) => c.value === mode);
|
|
48650
48662
|
}
|
|
48663
|
+
var OVERHEAD_VISIBILITY_CHOICES = [
|
|
48664
|
+
{
|
|
48665
|
+
title: "Full (default)",
|
|
48666
|
+
value: "full",
|
|
48667
|
+
description: "Per-thread session header + channel sticky with active-sessions list"
|
|
48668
|
+
},
|
|
48669
|
+
{
|
|
48670
|
+
title: "Minimal",
|
|
48671
|
+
value: "minimal",
|
|
48672
|
+
description: "One-line status bar only — drops the table and sessions list"
|
|
48673
|
+
},
|
|
48674
|
+
{
|
|
48675
|
+
title: "Hidden",
|
|
48676
|
+
value: "hidden",
|
|
48677
|
+
description: "No header post, no sticky — Claude's reply is the first message in the thread"
|
|
48678
|
+
}
|
|
48679
|
+
];
|
|
48680
|
+
function overheadVisibilityChoiceIndex(mode) {
|
|
48681
|
+
return OVERHEAD_VISIBILITY_CHOICES.findIndex((c) => c.value === mode);
|
|
48682
|
+
}
|
|
48651
48683
|
var __dirname2 = dirname2(fileURLToPath(import.meta.url));
|
|
48652
48684
|
var SLACK_MANIFEST_PATH = join2(__dirname2, "..", "docs", "slack-app-manifest.yaml");
|
|
48653
48685
|
var onCancel = () => {
|
|
@@ -49417,6 +49449,10 @@ async function setupMattermostPlatform(id, existing) {
|
|
|
49417
49449
|
permissionMode: existingMattermost.permissionMode,
|
|
49418
49450
|
skipPermissions: existingMattermost.skipPermissions
|
|
49419
49451
|
}) : "auto";
|
|
49452
|
+
const existingHeader = existingMattermost?.sessionHeader;
|
|
49453
|
+
const existingSticky = existingMattermost?.stickyMessage;
|
|
49454
|
+
const hasSplitVerbosity = existingHeader !== undefined && existingSticky !== undefined && existingHeader !== existingSticky;
|
|
49455
|
+
let lastChannelVerbosity = existingHeader ?? existingSticky ?? DEFAULT_OVERHEAD_VISIBILITY;
|
|
49420
49456
|
while (true) {
|
|
49421
49457
|
console.log("");
|
|
49422
49458
|
console.log(dim(" Now enter your Mattermost credentials:"));
|
|
@@ -49521,6 +49557,20 @@ async function setupMattermostPlatform(id, existing) {
|
|
|
49521
49557
|
choices: PERMISSION_MODE_CHOICES,
|
|
49522
49558
|
initial: permissionModeChoiceIndex(lastPermissionMode)
|
|
49523
49559
|
}, { onCancel });
|
|
49560
|
+
let channelVerbosity = lastChannelVerbosity;
|
|
49561
|
+
if (hasSplitVerbosity) {
|
|
49562
|
+
console.log("");
|
|
49563
|
+
console.log(dim(` Channel verbosity: keeping split values from current config ` + `(sessionHeader=${existingHeader}, stickyMessage=${existingSticky}). ` + `Edit YAML directly to change.`));
|
|
49564
|
+
} else {
|
|
49565
|
+
const result = await import_prompts.default({
|
|
49566
|
+
type: "select",
|
|
49567
|
+
name: "channelVerbosity",
|
|
49568
|
+
message: "How verbose should the bot be in this channel?",
|
|
49569
|
+
choices: OVERHEAD_VISIBILITY_CHOICES,
|
|
49570
|
+
initial: overheadVisibilityChoiceIndex(lastChannelVerbosity)
|
|
49571
|
+
}, { onCancel });
|
|
49572
|
+
channelVerbosity = result.channelVerbosity;
|
|
49573
|
+
}
|
|
49524
49574
|
lastUrl = basicSettings.url;
|
|
49525
49575
|
lastDisplayName = basicSettings.displayName;
|
|
49526
49576
|
lastToken = finalToken;
|
|
@@ -49528,6 +49578,7 @@ async function setupMattermostPlatform(id, existing) {
|
|
|
49528
49578
|
lastBotName = basicSettings.botName;
|
|
49529
49579
|
lastAllowedUsers = allowedUsers.join(",");
|
|
49530
49580
|
lastPermissionMode = permissionMode;
|
|
49581
|
+
lastChannelVerbosity = channelVerbosity;
|
|
49531
49582
|
console.log("");
|
|
49532
49583
|
console.log(dim(" Validating credentials..."));
|
|
49533
49584
|
const validationResult = await validateMattermostCredentials(basicSettings.url, finalToken, basicSettings.channelId);
|
|
@@ -49586,7 +49637,8 @@ async function setupMattermostPlatform(id, existing) {
|
|
|
49586
49637
|
channelId: basicSettings.channelId,
|
|
49587
49638
|
botName: basicSettings.botName,
|
|
49588
49639
|
allowedUsers,
|
|
49589
|
-
permissionMode: lastPermissionMode
|
|
49640
|
+
permissionMode: lastPermissionMode,
|
|
49641
|
+
...hasSplitVerbosity ? { sessionHeader: existingHeader, stickyMessage: existingSticky } : lastChannelVerbosity !== DEFAULT_OVERHEAD_VISIBILITY ? { sessionHeader: lastChannelVerbosity, stickyMessage: lastChannelVerbosity } : {}
|
|
49590
49642
|
};
|
|
49591
49643
|
}
|
|
49592
49644
|
}
|
|
@@ -49703,6 +49755,10 @@ async function setupSlackPlatform(id, existing) {
|
|
|
49703
49755
|
permissionMode: existingSlack.permissionMode,
|
|
49704
49756
|
skipPermissions: existingSlack.skipPermissions
|
|
49705
49757
|
}) : "auto";
|
|
49758
|
+
const existingHeader = existingSlack?.sessionHeader;
|
|
49759
|
+
const existingSticky = existingSlack?.stickyMessage;
|
|
49760
|
+
const hasSplitVerbosity = existingHeader !== undefined && existingSticky !== undefined && existingHeader !== existingSticky;
|
|
49761
|
+
let lastChannelVerbosity = existingHeader ?? existingSticky ?? DEFAULT_OVERHEAD_VISIBILITY;
|
|
49706
49762
|
while (true) {
|
|
49707
49763
|
console.log("");
|
|
49708
49764
|
console.log(dim(" Now enter your Slack credentials:"));
|
|
@@ -49818,6 +49874,20 @@ async function setupSlackPlatform(id, existing) {
|
|
|
49818
49874
|
choices: PERMISSION_MODE_CHOICES,
|
|
49819
49875
|
initial: permissionModeChoiceIndex(lastPermissionMode)
|
|
49820
49876
|
}, { onCancel });
|
|
49877
|
+
let channelVerbosity = lastChannelVerbosity;
|
|
49878
|
+
if (hasSplitVerbosity) {
|
|
49879
|
+
console.log("");
|
|
49880
|
+
console.log(dim(` Channel verbosity: keeping split values from current config ` + `(sessionHeader=${existingHeader}, stickyMessage=${existingSticky}). ` + `Edit YAML directly to change.`));
|
|
49881
|
+
} else {
|
|
49882
|
+
const result = await import_prompts.default({
|
|
49883
|
+
type: "select",
|
|
49884
|
+
name: "channelVerbosity",
|
|
49885
|
+
message: "How verbose should the bot be in this channel?",
|
|
49886
|
+
choices: OVERHEAD_VISIBILITY_CHOICES,
|
|
49887
|
+
initial: overheadVisibilityChoiceIndex(lastChannelVerbosity)
|
|
49888
|
+
}, { onCancel });
|
|
49889
|
+
channelVerbosity = result.channelVerbosity;
|
|
49890
|
+
}
|
|
49821
49891
|
lastDisplayName = basicSettings.displayName;
|
|
49822
49892
|
lastBotToken = finalBotToken;
|
|
49823
49893
|
lastAppToken = finalAppToken;
|
|
@@ -49825,6 +49895,7 @@ async function setupSlackPlatform(id, existing) {
|
|
|
49825
49895
|
lastBotName = basicSettings.botName;
|
|
49826
49896
|
lastAllowedUsers = allowedUsers.join(",");
|
|
49827
49897
|
lastPermissionMode = permissionMode;
|
|
49898
|
+
lastChannelVerbosity = channelVerbosity;
|
|
49828
49899
|
console.log("");
|
|
49829
49900
|
console.log(dim(" Validating credentials..."));
|
|
49830
49901
|
const validationResult = await validateSlackCredentials(finalBotToken, finalAppToken, basicSettings.channelId);
|
|
@@ -49889,7 +49960,8 @@ async function setupSlackPlatform(id, existing) {
|
|
|
49889
49960
|
channelId: basicSettings.channelId,
|
|
49890
49961
|
botName: basicSettings.botName,
|
|
49891
49962
|
allowedUsers,
|
|
49892
|
-
permissionMode: lastPermissionMode
|
|
49963
|
+
permissionMode: lastPermissionMode,
|
|
49964
|
+
...hasSplitVerbosity ? { sessionHeader: existingHeader, stickyMessage: existingSticky } : lastChannelVerbosity !== DEFAULT_OVERHEAD_VISIBILITY ? { sessionHeader: lastChannelVerbosity, stickyMessage: lastChannelVerbosity } : {}
|
|
49893
49965
|
};
|
|
49894
49966
|
}
|
|
49895
49967
|
}
|
|
@@ -50283,6 +50355,24 @@ function sanitizeFilename(name) {
|
|
|
50283
50355
|
}
|
|
50284
50356
|
return cleaned;
|
|
50285
50357
|
}
|
|
50358
|
+
function dedupeFilename(name, used) {
|
|
50359
|
+
if (!used.has(name)) {
|
|
50360
|
+
used.add(name);
|
|
50361
|
+
return name;
|
|
50362
|
+
}
|
|
50363
|
+
const lastDot = name.lastIndexOf(".");
|
|
50364
|
+
const hasExt = lastDot > 0;
|
|
50365
|
+
const stem = hasExt ? name.slice(0, lastDot) : name;
|
|
50366
|
+
const ext = hasExt ? name.slice(lastDot) : "";
|
|
50367
|
+
let counter = 1;
|
|
50368
|
+
let candidate = `${stem}_${counter}${ext}`;
|
|
50369
|
+
while (used.has(candidate)) {
|
|
50370
|
+
counter += 1;
|
|
50371
|
+
candidate = `${stem}_${counter}${ext}`;
|
|
50372
|
+
}
|
|
50373
|
+
used.add(candidate);
|
|
50374
|
+
return candidate;
|
|
50375
|
+
}
|
|
50286
50376
|
function formatBytes(bytes) {
|
|
50287
50377
|
if (bytes < 1024)
|
|
50288
50378
|
return `${bytes} B`;
|
|
@@ -53513,6 +53603,18 @@ function getSessionStatus(session) {
|
|
|
53513
53603
|
return "idle";
|
|
53514
53604
|
}
|
|
53515
53605
|
|
|
53606
|
+
// src/session/authorization.ts
|
|
53607
|
+
function isAuthorizedForSession(check) {
|
|
53608
|
+
const { username, platform, sessionAllowedUsers } = check;
|
|
53609
|
+
if (!username || username === "unknown") {
|
|
53610
|
+
return false;
|
|
53611
|
+
}
|
|
53612
|
+
if (platform.isUserAllowed(username)) {
|
|
53613
|
+
return true;
|
|
53614
|
+
}
|
|
53615
|
+
return sessionAllowedUsers?.has(username) ?? false;
|
|
53616
|
+
}
|
|
53617
|
+
|
|
53516
53618
|
// src/claude/cli.ts
|
|
53517
53619
|
init_spawn();
|
|
53518
53620
|
init_logger();
|
|
@@ -54746,7 +54848,7 @@ function createPassthroughHandler(slashCommand) {
|
|
|
54746
54848
|
return { handled: false };
|
|
54747
54849
|
}
|
|
54748
54850
|
if (ctx.isAllowed) {
|
|
54749
|
-
await ctx.sessionManager.sendFollowUp(ctx.threadId, `/${slashCommand}
|
|
54851
|
+
await ctx.sessionManager.sendFollowUp(ctx.threadId, `/${slashCommand}`, undefined, undefined, undefined, { system: true });
|
|
54750
54852
|
}
|
|
54751
54853
|
return { handled: true };
|
|
54752
54854
|
};
|
|
@@ -54794,7 +54896,7 @@ async function handleDynamicSlashCommand(command, args, ctx) {
|
|
|
54794
54896
|
}
|
|
54795
54897
|
if (ctx.isAllowed) {
|
|
54796
54898
|
const fullCommand = args ? `/${command} ${args}` : `/${command}`;
|
|
54797
|
-
await ctx.sessionManager.sendFollowUp(ctx.threadId, fullCommand);
|
|
54899
|
+
await ctx.sessionManager.sendFollowUp(ctx.threadId, fullCommand, undefined, undefined, undefined, { system: true });
|
|
54798
54900
|
}
|
|
54799
54901
|
return { handled: true };
|
|
54800
54902
|
}
|
|
@@ -55315,7 +55417,6 @@ import { lstat, mkdir as mkdir2, mkdtemp, rm as rm2, writeFile as writeFile2 } f
|
|
|
55315
55417
|
import { tmpdir as tmpdir2 } from "os";
|
|
55316
55418
|
import { join as join9 } from "path";
|
|
55317
55419
|
var log18 = createLogger("streaming");
|
|
55318
|
-
var MAX_UPLOAD_SIZE = 100 * 1024 * 1024;
|
|
55319
55420
|
var UPLOAD_ROOT_DIR = "claude-threads-uploads";
|
|
55320
55421
|
function safeIdSegment(id) {
|
|
55321
55422
|
return id.replace(/[^A-Za-z0-9._-]/g, "_");
|
|
@@ -55355,18 +55456,11 @@ async function saveFilesToUploadDir(platform, uploadDir, files, debug = false) {
|
|
|
55355
55456
|
return { saved, skipped };
|
|
55356
55457
|
}
|
|
55357
55458
|
const messageDir = await mkdtemp(join9(uploadDir, `${Date.now().toString(36)}-`));
|
|
55459
|
+
const usedNames = new Set;
|
|
55358
55460
|
for (const file of files) {
|
|
55359
|
-
if (file.size > MAX_UPLOAD_SIZE) {
|
|
55360
|
-
skipped.push({
|
|
55361
|
-
name: file.name,
|
|
55362
|
-
reason: `File too large (${formatBytes(file.size)} > ${formatBytes(MAX_UPLOAD_SIZE)} limit)`,
|
|
55363
|
-
suggestion: "Split the file or share it via an external link"
|
|
55364
|
-
});
|
|
55365
|
-
continue;
|
|
55366
|
-
}
|
|
55367
55461
|
try {
|
|
55368
55462
|
const buffer = await platform.downloadFile(file.id);
|
|
55369
|
-
const safeName = sanitizeFilename(file.name);
|
|
55463
|
+
const safeName = dedupeFilename(sanitizeFilename(file.name), usedNames);
|
|
55370
55464
|
const absolutePath = join9(messageDir, safeName);
|
|
55371
55465
|
await writeFile2(absolutePath, buffer, { mode: 384, flag: "wx" });
|
|
55372
55466
|
saved.push({
|
|
@@ -63980,6 +64074,13 @@ async function buildStickyMessage(sessions, platformId, config, formatter, getTh
|
|
|
63980
64074
|
const otherPlatformSessions = allSessions.filter((s) => s.platformId !== platformId);
|
|
63981
64075
|
const totalCount = allSessions.length;
|
|
63982
64076
|
const statusBar = await buildStatusBar(totalCount, config, formatter, platformId);
|
|
64077
|
+
if (config.overhead === "minimal") {
|
|
64078
|
+
return [
|
|
64079
|
+
formatter.formatHorizontalRule(),
|
|
64080
|
+
statusBar
|
|
64081
|
+
].join(`
|
|
64082
|
+
`);
|
|
64083
|
+
}
|
|
63983
64084
|
const activeSessionIds = new Set(sessions.keys());
|
|
63984
64085
|
const historySessions = sessionStore ? sessionStore.getHistory(platformId, activeSessionIds).slice(0, 5) : [];
|
|
63985
64086
|
if (totalCount === 0) {
|
|
@@ -64119,7 +64220,35 @@ async function validateLastMessageIds(platform, sessions) {
|
|
|
64119
64220
|
});
|
|
64120
64221
|
await Promise.all(validationPromises);
|
|
64121
64222
|
}
|
|
64223
|
+
var hiddenCleanupDone = new Set;
|
|
64224
|
+
function clearHiddenCleanupTracking(platformId) {
|
|
64225
|
+
hiddenCleanupDone.delete(platformId);
|
|
64226
|
+
}
|
|
64122
64227
|
async function updateStickyMessageImpl(platform, sessions, config) {
|
|
64228
|
+
if (config.overhead === "hidden") {
|
|
64229
|
+
if (!hiddenCleanupDone.has(platform.platformId)) {
|
|
64230
|
+
hiddenCleanupDone.add(platform.platformId);
|
|
64231
|
+
const existing = stickyPostIds.get(platform.platformId);
|
|
64232
|
+
if (existing) {
|
|
64233
|
+
log21.info(`sticky[${platform.platformId}] hidden mode: removing leftover ${formatShortId(existing)}`);
|
|
64234
|
+
try {
|
|
64235
|
+
await platform.unpinPost(existing);
|
|
64236
|
+
} catch {}
|
|
64237
|
+
try {
|
|
64238
|
+
await platform.deletePost(existing);
|
|
64239
|
+
} catch {}
|
|
64240
|
+
stickyPostIds.delete(platform.platformId);
|
|
64241
|
+
if (sessionStore) {
|
|
64242
|
+
sessionStore.removeStickyPostId(platform.platformId);
|
|
64243
|
+
}
|
|
64244
|
+
}
|
|
64245
|
+
try {
|
|
64246
|
+
const botUser = await platform.getBotUser();
|
|
64247
|
+
cleanupOldStickyMessages(platform, botUser.id, false, new Set).catch(() => {});
|
|
64248
|
+
} catch {}
|
|
64249
|
+
}
|
|
64250
|
+
return;
|
|
64251
|
+
}
|
|
64123
64252
|
const platformSessions = [...sessions.values()].filter((s) => s.platformId === platform.platformId);
|
|
64124
64253
|
log21.debug(`updateStickyMessage for ${platform.platformId}, ${platformSessions.length} sessions`);
|
|
64125
64254
|
for (const s of platformSessions) {
|
|
@@ -64199,8 +64328,11 @@ async function updateStickyMessageImpl(platform, sessions, config) {
|
|
|
64199
64328
|
log21.error(`Failed to update sticky message for ${platform.platformId}`, err instanceof Error ? err : undefined);
|
|
64200
64329
|
}
|
|
64201
64330
|
}
|
|
64202
|
-
async function updateAllStickyMessages(platforms, sessions, config) {
|
|
64203
|
-
const updates = [...platforms.values()].map((platform) =>
|
|
64331
|
+
async function updateAllStickyMessages(platforms, sessions, config, overheadByPlatform) {
|
|
64332
|
+
const updates = [...platforms.values()].map((platform) => {
|
|
64333
|
+
const overhead = overheadByPlatform?.get(platform.platformId) ?? config.overhead;
|
|
64334
|
+
return updateStickyMessage(platform, sessions, { ...config, overhead });
|
|
64335
|
+
});
|
|
64204
64336
|
await Promise.all(updates);
|
|
64205
64337
|
}
|
|
64206
64338
|
function markNeedsBump(platformId) {
|
|
@@ -65986,49 +66118,62 @@ async function requestMessageApproval(session, username, message, ctx) {
|
|
|
65986
66118
|
fromUser: username
|
|
65987
66119
|
});
|
|
65988
66120
|
}
|
|
65989
|
-
async function
|
|
65990
|
-
if (!session.sessionStartPostId)
|
|
65991
|
-
return;
|
|
66121
|
+
async function buildSessionHeaderStatusBar(session, ctx) {
|
|
65992
66122
|
const formatter = session.platform.getFormatter();
|
|
65993
|
-
const worktreeContext = session.worktreeInfo ? { path: session.worktreeInfo.worktreePath, branch: session.worktreeInfo.branch } : undefined;
|
|
65994
|
-
const shortDir = shortenPath(session.workingDir, undefined, worktreeContext);
|
|
65995
66123
|
const effectiveMode = effectivePermissionMode({
|
|
65996
66124
|
override: session.permissionModeOverride,
|
|
65997
66125
|
sessionHasInteractiveOverride: session.forceInteractivePermissions,
|
|
65998
66126
|
botWideMode: ctx.config.permissionMode
|
|
65999
66127
|
});
|
|
66000
66128
|
const permMode = permissionModeDisplay(effectiveMode).chip;
|
|
66001
|
-
const
|
|
66002
|
-
|
|
66003
|
-
const versionStr = formatVersionString();
|
|
66004
|
-
statusItems.push(formatter.formatCode(versionStr));
|
|
66129
|
+
const items = [];
|
|
66130
|
+
items.push(formatter.formatCode(formatVersionString()));
|
|
66005
66131
|
if (session.usageStats) {
|
|
66006
66132
|
const stats = session.usageStats;
|
|
66007
|
-
|
|
66133
|
+
items.push(formatter.formatCode(`\uD83E\uDD16 ${stats.modelDisplayName}`));
|
|
66008
66134
|
const contextPercent = Math.round(stats.contextTokens / stats.contextWindowSize * 100);
|
|
66009
|
-
|
|
66010
|
-
|
|
66011
|
-
statusItems.push(formatter.formatCode(`\uD83D\uDCB0 $${stats.totalCostUSD.toFixed(2)}`));
|
|
66135
|
+
items.push(formatter.formatCode(`${formatContextBar(contextPercent)} ${contextPercent}%`));
|
|
66136
|
+
items.push(formatter.formatCode(`\uD83D\uDCB0 $${stats.totalCostUSD.toFixed(2)}`));
|
|
66012
66137
|
}
|
|
66013
|
-
|
|
66138
|
+
items.push(formatter.formatCode(permMode));
|
|
66014
66139
|
if (session.messageManager?.getPendingApproval()?.type === "plan") {
|
|
66015
|
-
|
|
66140
|
+
items.push(formatter.formatCode("\uD83D\uDCCB Plan pending"));
|
|
66016
66141
|
} else if (session.planApproved) {
|
|
66017
|
-
|
|
66142
|
+
items.push(formatter.formatCode("\uD83D\uDD28 Implementing"));
|
|
66018
66143
|
}
|
|
66019
66144
|
if (ctx.config.chromeEnabled) {
|
|
66020
|
-
|
|
66145
|
+
items.push(formatter.formatCode("\uD83C\uDF10 Chrome"));
|
|
66021
66146
|
}
|
|
66022
66147
|
if (keepAlive.isActive()) {
|
|
66023
|
-
|
|
66148
|
+
items.push(formatter.formatCode("\uD83D\uDC93 Keep-alive"));
|
|
66024
66149
|
}
|
|
66025
66150
|
const battery = await formatBatteryStatus();
|
|
66026
66151
|
if (battery) {
|
|
66027
|
-
|
|
66152
|
+
items.push(formatter.formatCode(battery));
|
|
66028
66153
|
}
|
|
66029
|
-
|
|
66030
|
-
|
|
66031
|
-
|
|
66154
|
+
items.push(formatter.formatCode(`⏱️ ${formatUptime(session.startedAt)}`));
|
|
66155
|
+
return items.join(" · ");
|
|
66156
|
+
}
|
|
66157
|
+
async function updateSessionHeader(session, ctx) {
|
|
66158
|
+
if (session.sessionHeaderMode === "hidden")
|
|
66159
|
+
return;
|
|
66160
|
+
if (!session.sessionStartPostId)
|
|
66161
|
+
return;
|
|
66162
|
+
const formatter = session.platform.getFormatter();
|
|
66163
|
+
const statusBar = await buildSessionHeaderStatusBar(session, ctx);
|
|
66164
|
+
const updateInfo = getUpdateInfo();
|
|
66165
|
+
const updateNotice = updateInfo ? `> ⚠️ ${formatter.formatBold("Update available:")} v${updateInfo.current} → v${updateInfo.latest} - Run ${formatter.formatCode("bun install -g claude-threads")}
|
|
66166
|
+
|
|
66167
|
+
` : undefined;
|
|
66168
|
+
if (session.sessionHeaderMode === "minimal") {
|
|
66169
|
+
const msg2 = [updateNotice, statusBar].filter(Boolean).join(`
|
|
66170
|
+
`);
|
|
66171
|
+
await updatePost(session, session.sessionStartPostId, msg2);
|
|
66172
|
+
return;
|
|
66173
|
+
}
|
|
66174
|
+
const worktreeContext = session.worktreeInfo ? { path: session.worktreeInfo.worktreePath, branch: session.worktreeInfo.branch } : undefined;
|
|
66175
|
+
const shortDir = shortenPath(session.workingDir, undefined, worktreeContext);
|
|
66176
|
+
const otherParticipants = [...session.sessionAllowedUsers].filter((u) => u !== session.startedBy).map((u) => formatter.formatUserMention(u)).join(", ");
|
|
66032
66177
|
const items = [];
|
|
66033
66178
|
if (session.sessionTitle) {
|
|
66034
66179
|
items.push(["\uD83D\uDCDD", "Topic", session.sessionTitle]);
|
|
@@ -66074,10 +66219,6 @@ async function updateSessionHeader(session, ctx) {
|
|
|
66074
66219
|
const logPath = getLogFilePath(session.platform.platformId, session.claudeSessionId);
|
|
66075
66220
|
const shortLogPath = logPath.replace(process.env.HOME || "", "~");
|
|
66076
66221
|
items.push(["\uD83D\uDCCB", "Log File", formatter.formatCode(shortLogPath)]);
|
|
66077
|
-
const updateInfo = getUpdateInfo();
|
|
66078
|
-
const updateNotice = updateInfo ? `> ⚠️ ${formatter.formatBold("Update available:")} v${updateInfo.current} → v${updateInfo.latest} - Run ${formatter.formatCode("bun install -g claude-threads")}
|
|
66079
|
-
|
|
66080
|
-
` : undefined;
|
|
66081
66222
|
const msg = [
|
|
66082
66223
|
updateNotice,
|
|
66083
66224
|
statusBar,
|
|
@@ -66085,8 +66226,7 @@ async function updateSessionHeader(session, ctx) {
|
|
|
66085
66226
|
formatter.formatKeyValueList(items)
|
|
66086
66227
|
].filter((item) => item !== null && item !== undefined).join(`
|
|
66087
66228
|
`);
|
|
66088
|
-
|
|
66089
|
-
await updatePost(session, postId, msg);
|
|
66229
|
+
await updatePost(session, session.sessionStartPostId, msg);
|
|
66090
66230
|
}
|
|
66091
66231
|
async function showUpdateStatus(session, updateManager, ctx) {
|
|
66092
66232
|
const formatter = session.platform.getFormatter();
|
|
@@ -66586,6 +66726,17 @@ function maybeInjectMetadataReminder(message, session, ctx, fullSession) {
|
|
|
66586
66726
|
}
|
|
66587
66727
|
return message;
|
|
66588
66728
|
}
|
|
66729
|
+
function resumeSessionHeaderMode(persisted, platformConfigured) {
|
|
66730
|
+
return persisted ?? platformConfigured ?? DEFAULT_OVERHEAD_VISIBILITY;
|
|
66731
|
+
}
|
|
66732
|
+
function resolveSessionHeaderMode(configured, replyToPostId, platformId) {
|
|
66733
|
+
const mode = configured ?? DEFAULT_OVERHEAD_VISIBILITY;
|
|
66734
|
+
if (mode === "hidden" && !replyToPostId) {
|
|
66735
|
+
log30.error(`sessionHeader: hidden requires a replyToPostId for ${platformId}; ` + `downgrading this session to 'minimal' so the header post is still short.`);
|
|
66736
|
+
return "minimal";
|
|
66737
|
+
}
|
|
66738
|
+
return mode;
|
|
66739
|
+
}
|
|
66589
66740
|
async function startSession(options, username, displayName, replyToPostId, platformId, ctx, triggeringPostId, initialOptions) {
|
|
66590
66741
|
const threadId = replyToPostId || "";
|
|
66591
66742
|
const existingSessionId = ctx.ops.getSessionId(platformId, threadId);
|
|
@@ -66599,6 +66750,10 @@ async function startSession(options, username, displayName, replyToPostId, platf
|
|
|
66599
66750
|
if (!platform) {
|
|
66600
66751
|
throw new Error(`Platform '${platformId}' not found. Call addPlatform() first.`);
|
|
66601
66752
|
}
|
|
66753
|
+
if (!isAuthorizedForSession({ username, platform, sessionAllowedUsers: undefined })) {
|
|
66754
|
+
log30.warn(`auth.denied.startSession: @${username || "unknown"} not authorized to start session in ${threadId.substring(0, 8)}...`);
|
|
66755
|
+
return;
|
|
66756
|
+
}
|
|
66602
66757
|
const activeOrPending = ctx.state.sessions.size + pendingStartsCount;
|
|
66603
66758
|
if (activeOrPending >= ctx.config.maxSessions) {
|
|
66604
66759
|
const formatter2 = platform.getFormatter();
|
|
@@ -66611,13 +66766,18 @@ async function startSession(options, username, displayName, replyToPostId, platf
|
|
|
66611
66766
|
return;
|
|
66612
66767
|
}
|
|
66613
66768
|
pendingStartsCount++;
|
|
66769
|
+
const sessionHeaderMode = resolveSessionHeaderMode(ctx.ops.getPlatformOverhead(platformId).sessionHeader, replyToPostId, platformId);
|
|
66614
66770
|
const startFormatter = platform.getFormatter();
|
|
66615
|
-
const
|
|
66616
|
-
|
|
66617
|
-
|
|
66618
|
-
|
|
66771
|
+
const skipHeaderPost = sessionHeaderMode === "hidden";
|
|
66772
|
+
let startPost;
|
|
66773
|
+
if (!skipHeaderPost) {
|
|
66774
|
+
startPost = await withErrorHandling(() => platform.createPost(startFormatter.formatItalic("Claude Threads session starting..."), replyToPostId), { action: "Create session post" });
|
|
66775
|
+
if (!startPost) {
|
|
66776
|
+
releasePendingStart();
|
|
66777
|
+
return;
|
|
66778
|
+
}
|
|
66619
66779
|
}
|
|
66620
|
-
const actualThreadId = replyToPostId || startPost.id;
|
|
66780
|
+
const actualThreadId = replyToPostId || (startPost ? startPost.id : "");
|
|
66621
66781
|
const sessionId = ctx.ops.getSessionId(platformId, actualThreadId);
|
|
66622
66782
|
platform.sendTyping(actualThreadId);
|
|
66623
66783
|
const claudeSessionId = randomUUID4();
|
|
@@ -66631,13 +66791,23 @@ async function startSession(options, username, displayName, replyToPostId, platf
|
|
|
66631
66791
|
const requestedDir = initialOptions.workingDir.startsWith("~") ? initialOptions.workingDir.replace("~", process.env.HOME || "") : initialOptions.workingDir;
|
|
66632
66792
|
const resolvedDir = resolve6(requestedDir);
|
|
66633
66793
|
if (!existsSync12(resolvedDir)) {
|
|
66634
|
-
|
|
66794
|
+
const msg = `❌ Directory does not exist: ${formatter.formatCode(initialOptions.workingDir)}`;
|
|
66795
|
+
if (startPost) {
|
|
66796
|
+
await platform.updatePost(startPost.id, msg);
|
|
66797
|
+
} else {
|
|
66798
|
+
await platform.createPost(msg, replyToPostId);
|
|
66799
|
+
}
|
|
66635
66800
|
releasePendingStart();
|
|
66636
66801
|
return;
|
|
66637
66802
|
}
|
|
66638
66803
|
const { statSync: statSync4 } = await import("fs");
|
|
66639
66804
|
if (!statSync4(resolvedDir).isDirectory()) {
|
|
66640
|
-
|
|
66805
|
+
const msg = `❌ Not a directory: ${formatter.formatCode(initialOptions.workingDir)}`;
|
|
66806
|
+
if (startPost) {
|
|
66807
|
+
await platform.updatePost(startPost.id, msg);
|
|
66808
|
+
} else {
|
|
66809
|
+
await platform.createPost(msg, replyToPostId);
|
|
66810
|
+
}
|
|
66641
66811
|
releasePendingStart();
|
|
66642
66812
|
return;
|
|
66643
66813
|
}
|
|
@@ -66695,7 +66865,8 @@ async function startSession(options, username, displayName, replyToPostId, platf
|
|
|
66695
66865
|
sessionAllowedUsers: new Set([username]),
|
|
66696
66866
|
forceInteractivePermissions,
|
|
66697
66867
|
permissionModeOverride: sessionPermissionModeOverride,
|
|
66698
|
-
sessionStartPostId: startPost.id,
|
|
66868
|
+
sessionStartPostId: startPost ? startPost.id : null,
|
|
66869
|
+
sessionHeaderMode,
|
|
66699
66870
|
timers: createSessionTimers(),
|
|
66700
66871
|
lifecycle: createSessionLifecycle(),
|
|
66701
66872
|
timeoutWarningPosted: false,
|
|
@@ -66714,7 +66885,9 @@ async function startSession(options, username, displayName, replyToPostId, platf
|
|
|
66714
66885
|
});
|
|
66715
66886
|
mutableSessions(ctx).set(sessionId, session);
|
|
66716
66887
|
releasePendingStart();
|
|
66717
|
-
|
|
66888
|
+
if (startPost) {
|
|
66889
|
+
ctx.ops.registerPost(startPost.id, actualThreadId);
|
|
66890
|
+
}
|
|
66718
66891
|
ctx.ops.emitSessionAdd(session);
|
|
66719
66892
|
sessionLog6(session).info(`▶ Session started by @${username}`);
|
|
66720
66893
|
fireMetadataSuggestions(session, options.prompt, ctx);
|
|
@@ -66847,6 +67020,7 @@ Please start a new session.`), { action: "Post resume failure notification" });
|
|
|
66847
67020
|
sessionAllowedUsers: new Set(state.sessionAllowedUsers),
|
|
66848
67021
|
forceInteractivePermissions: state.forceInteractivePermissions ?? false,
|
|
66849
67022
|
sessionStartPostId: state.sessionStartPostId ?? null,
|
|
67023
|
+
sessionHeaderMode: resumeSessionHeaderMode(state.sessionHeaderMode, ctx.ops.getPlatformOverhead(platformId).sessionHeader),
|
|
66850
67024
|
timers: createSessionTimers(),
|
|
66851
67025
|
lifecycle: createResumedLifecycle(state.resumeFailCount ?? 0),
|
|
66852
67026
|
timeoutWarningPosted: false,
|
|
@@ -66947,9 +67121,15 @@ ${failFormatter.formatItalic("Your previous conversation context is preserved, b
|
|
|
66947
67121
|
await ctx.ops.updateStickyMessage();
|
|
66948
67122
|
}
|
|
66949
67123
|
}
|
|
66950
|
-
async function sendFollowUp(session, message, files, ctx, username, displayName) {
|
|
67124
|
+
async function sendFollowUp(session, message, files, ctx, username, displayName, options) {
|
|
66951
67125
|
if (!session.claude.isRunning())
|
|
66952
67126
|
return;
|
|
67127
|
+
if (!options?.system) {
|
|
67128
|
+
if (!isAuthorizedForSession({ username, platform: session.platform, sessionAllowedUsers: session.sessionAllowedUsers })) {
|
|
67129
|
+
sessionLog6(session).warn(`auth.denied.sendFollowUp: @${username || "unknown"} not authorized`);
|
|
67130
|
+
return;
|
|
67131
|
+
}
|
|
67132
|
+
}
|
|
66953
67133
|
if (session.needsContextPromptOnNextMessage) {
|
|
66954
67134
|
session.needsContextPromptOnNextMessage = false;
|
|
66955
67135
|
await session.messageManager?.prepareForUserMessage();
|
|
@@ -66972,7 +67152,7 @@ async function sendFollowUp(session, message, files, ctx, username, displayName)
|
|
|
66972
67152
|
session.messageCount++;
|
|
66973
67153
|
await session.messageManager.handleUserMessage(messageToSend, files, username, displayName);
|
|
66974
67154
|
}
|
|
66975
|
-
async function resumePausedSession(threadId, message, files, ctx) {
|
|
67155
|
+
async function resumePausedSession(threadId, message, files, ctx, username) {
|
|
66976
67156
|
const persisted = ctx.state.sessionStore.load();
|
|
66977
67157
|
const state = findPersistedByThreadId(persisted, threadId);
|
|
66978
67158
|
if (!state) {
|
|
@@ -66980,6 +67160,16 @@ async function resumePausedSession(threadId, message, files, ctx) {
|
|
|
66980
67160
|
return;
|
|
66981
67161
|
}
|
|
66982
67162
|
const shortId = threadId.substring(0, 8);
|
|
67163
|
+
const platform = ctx.state.platforms.get(state.platformId);
|
|
67164
|
+
if (!platform) {
|
|
67165
|
+
log30.warn(`auth.denied.resume: platform '${state.platformId}' not found for ${shortId}...`);
|
|
67166
|
+
return;
|
|
67167
|
+
}
|
|
67168
|
+
const sessionAllowedUsers = new Set(state.sessionAllowedUsers || [state.startedBy].filter(Boolean));
|
|
67169
|
+
if (!isAuthorizedForSession({ username, platform, sessionAllowedUsers })) {
|
|
67170
|
+
log30.warn(`auth.denied.resume: @${username || "unknown"} not authorized to resume ${shortId}...`);
|
|
67171
|
+
return;
|
|
67172
|
+
}
|
|
66983
67173
|
log30.info(`\uD83D\uDD04 Resuming paused session ${shortId}... for new message`);
|
|
66984
67174
|
await resumeSession(state, ctx);
|
|
66985
67175
|
const session = ctx.ops.findSessionByThreadId(threadId);
|
|
@@ -67497,9 +67687,9 @@ async function tryResumeFromReaction(deps, platformId, postId, username) {
|
|
|
67497
67687
|
const sessionId = `${platformId}:${persistedSession.threadId}`;
|
|
67498
67688
|
if (deps.registry.hasById(sessionId))
|
|
67499
67689
|
return false;
|
|
67500
|
-
const allowedUsers = new Set(persistedSession.sessionAllowedUsers);
|
|
67501
67690
|
const platform = deps.platforms.get(platformId);
|
|
67502
|
-
|
|
67691
|
+
const sessionAllowedUsers = new Set(persistedSession.sessionAllowedUsers || [persistedSession.startedBy].filter(Boolean));
|
|
67692
|
+
if (!platform || !isAuthorizedForSession({ username, platform, sessionAllowedUsers })) {
|
|
67503
67693
|
if (platform) {
|
|
67504
67694
|
await platform.createPost(`⚠️ @${username} is not authorized to resume this session`, persistedSession.threadId);
|
|
67505
67695
|
}
|
|
@@ -67581,6 +67771,7 @@ class SessionManager extends EventEmitter4 {
|
|
|
67581
67771
|
isShuttingDown = false;
|
|
67582
67772
|
customDescription;
|
|
67583
67773
|
customFooter;
|
|
67774
|
+
platformOverhead = new Map;
|
|
67584
67775
|
autoUpdateManager = null;
|
|
67585
67776
|
accountPool;
|
|
67586
67777
|
constructor(workingDir, permissionModeOrSkipFlag = "default", chromeEnabled = false, worktreeMode = "prompt", sessionsPath, threadLogsEnabled = true, threadLogsRetentionDays = 30, limits, claudeAccounts) {
|
|
@@ -67612,8 +67803,12 @@ class SessionManager extends EventEmitter4 {
|
|
|
67612
67803
|
cleanupWorktrees: this.limits.cleanupWorktrees
|
|
67613
67804
|
});
|
|
67614
67805
|
}
|
|
67615
|
-
addPlatform(platformId, client) {
|
|
67806
|
+
addPlatform(platformId, client, overhead) {
|
|
67616
67807
|
this.platforms.set(platformId, client);
|
|
67808
|
+
this.platformOverhead.set(platformId, {
|
|
67809
|
+
sessionHeader: overhead?.sessionHeader ?? DEFAULT_OVERHEAD_VISIBILITY,
|
|
67810
|
+
stickyMessage: overhead?.stickyMessage ?? DEFAULT_OVERHEAD_VISIBILITY
|
|
67811
|
+
});
|
|
67617
67812
|
client.on("message", (post2, user) => this.handleMessage(platformId, post2, user));
|
|
67618
67813
|
client.on("reaction", (reaction, user) => {
|
|
67619
67814
|
if (user) {
|
|
@@ -67626,6 +67821,9 @@ class SessionManager extends EventEmitter4 {
|
|
|
67626
67821
|
}
|
|
67627
67822
|
});
|
|
67628
67823
|
client.on("channel_post", () => {
|
|
67824
|
+
if (this.platformOverhead.get(platformId)?.stickyMessage === "hidden") {
|
|
67825
|
+
return;
|
|
67826
|
+
}
|
|
67629
67827
|
markNeedsBump(platformId);
|
|
67630
67828
|
this.updateStickyMessage();
|
|
67631
67829
|
});
|
|
@@ -67633,6 +67831,8 @@ class SessionManager extends EventEmitter4 {
|
|
|
67633
67831
|
}
|
|
67634
67832
|
removePlatform(platformId) {
|
|
67635
67833
|
this.platforms.delete(platformId);
|
|
67834
|
+
this.platformOverhead.delete(platformId);
|
|
67835
|
+
clearHiddenCleanupTracking(platformId);
|
|
67636
67836
|
}
|
|
67637
67837
|
setAutoUpdateManager(manager) {
|
|
67638
67838
|
this.autoUpdateManager = manager;
|
|
@@ -67718,7 +67918,11 @@ class SessionManager extends EventEmitter4 {
|
|
|
67718
67918
|
getClaudeAccount: (id) => this.accountPool.get(id),
|
|
67719
67919
|
releaseClaudeAccount: (id) => this.accountPool.release(id),
|
|
67720
67920
|
markClaudeAccountCooling: (id, untilMs) => this.accountPool.markCooling(id, untilMs),
|
|
67721
|
-
getClaudeAccountPoolStatus: () => this.accountPool.status()
|
|
67921
|
+
getClaudeAccountPoolStatus: () => this.accountPool.status(),
|
|
67922
|
+
getPlatformOverhead: (pid) => this.platformOverhead.get(pid) ?? {
|
|
67923
|
+
sessionHeader: DEFAULT_OVERHEAD_VISIBILITY,
|
|
67924
|
+
stickyMessage: DEFAULT_OVERHEAD_VISIBILITY
|
|
67925
|
+
}
|
|
67722
67926
|
};
|
|
67723
67927
|
return createSessionContext(config, state, ops);
|
|
67724
67928
|
}
|
|
@@ -67878,7 +68082,8 @@ class SessionManager extends EventEmitter4 {
|
|
|
67878
68082
|
pullRequestUrl: session.pullRequestUrl,
|
|
67879
68083
|
messageCount: session.messageCount,
|
|
67880
68084
|
resumeFailCount: session.lifecycle.resumeFailCount,
|
|
67881
|
-
claudeAccountId: session.claudeAccountId
|
|
68085
|
+
claudeAccountId: session.claudeAccountId,
|
|
68086
|
+
sessionHeaderMode: session.sessionHeaderMode
|
|
67882
68087
|
};
|
|
67883
68088
|
this.sessionStore.save(session.sessionId, state);
|
|
67884
68089
|
}
|
|
@@ -67895,6 +68100,10 @@ class SessionManager extends EventEmitter4 {
|
|
|
67895
68100
|
});
|
|
67896
68101
|
}
|
|
67897
68102
|
async updateStickyMessage() {
|
|
68103
|
+
const overheadByPlatform = new Map;
|
|
68104
|
+
for (const [platformId, overhead] of this.platformOverhead) {
|
|
68105
|
+
overheadByPlatform.set(platformId, overhead.stickyMessage);
|
|
68106
|
+
}
|
|
67898
68107
|
await updateAllStickyMessages(this.platforms, this.registry.getSessions(), {
|
|
67899
68108
|
maxSessions: this.limits.maxSessions,
|
|
67900
68109
|
chromeEnabled: this.chromeEnabled,
|
|
@@ -67905,7 +68114,7 @@ class SessionManager extends EventEmitter4 {
|
|
|
67905
68114
|
description: this.customDescription,
|
|
67906
68115
|
footer: this.customFooter,
|
|
67907
68116
|
accountPoolStatus: this.accountPool.isEmpty ? undefined : this.accountPool.status()
|
|
67908
|
-
});
|
|
68117
|
+
}, overheadByPlatform);
|
|
67909
68118
|
}
|
|
67910
68119
|
async updateAllStickyMessages() {
|
|
67911
68120
|
await this.updateStickyMessage();
|
|
@@ -68066,11 +68275,11 @@ class SessionManager extends EventEmitter4 {
|
|
|
68066
68275
|
}
|
|
68067
68276
|
return;
|
|
68068
68277
|
}
|
|
68069
|
-
async sendFollowUp(threadId, message, files, username, displayName) {
|
|
68278
|
+
async sendFollowUp(threadId, message, files, username, displayName, options) {
|
|
68070
68279
|
const session = this.findSessionByThreadId(threadId);
|
|
68071
68280
|
if (!session || !session.claude.isRunning())
|
|
68072
68281
|
return;
|
|
68073
|
-
await sendFollowUp(session, message, files, this.getContext(), username, displayName);
|
|
68282
|
+
await sendFollowUp(session, message, files, this.getContext(), username, displayName, options);
|
|
68074
68283
|
}
|
|
68075
68284
|
isSessionActive() {
|
|
68076
68285
|
return this.registry.size > 0;
|
|
@@ -68084,8 +68293,8 @@ class SessionManager extends EventEmitter4 {
|
|
|
68084
68293
|
return false;
|
|
68085
68294
|
return this.registry.getPersistedByThreadId(threadId) !== undefined;
|
|
68086
68295
|
}
|
|
68087
|
-
async resumePausedSession(threadId, message, files) {
|
|
68088
|
-
await resumePausedSession(threadId, message, files, this.getContext());
|
|
68296
|
+
async resumePausedSession(threadId, message, files, username) {
|
|
68297
|
+
await resumePausedSession(threadId, message, files, this.getContext(), username);
|
|
68089
68298
|
}
|
|
68090
68299
|
getPersistedSession(threadId) {
|
|
68091
68300
|
return this.registry.getPersistedByThreadId(threadId);
|
|
@@ -78138,7 +78347,7 @@ async function handleMessage(client, session, post2, user, options) {
|
|
|
78138
78347
|
}
|
|
78139
78348
|
const files2 = post2.metadata?.files;
|
|
78140
78349
|
if (content || files2?.length) {
|
|
78141
|
-
await session.resumePausedSession(threadRoot, content, files2);
|
|
78350
|
+
await session.resumePausedSession(threadRoot, content, files2, username);
|
|
78142
78351
|
}
|
|
78143
78352
|
return;
|
|
78144
78353
|
}
|
|
@@ -79149,7 +79358,7 @@ function wirePlatformEvents(platformId, client, session, ui) {
|
|
|
79149
79358
|
ui.addLog({ level: "error", component: platformId, message });
|
|
79150
79359
|
});
|
|
79151
79360
|
}
|
|
79152
|
-
program.name("claude-threads").version(VERSION).description("Share Claude Code sessions in Mattermost").option("--url <url>", "Mattermost server URL").option("--token <token>", "Mattermost bot token").option("--channel <id>", "Mattermost channel ID").option("--bot-name <name>", "Bot mention name (default: claude-code)").option("--allowed-users <users>", "Comma-separated allowed usernames").option("--permission-mode <mode>", "Permission mode: default | auto | bypass (default: from config)").option("--skip-permissions", "[deprecated] Alias for --permission-mode bypass").option("--no-skip-permissions", "[deprecated] Alias for --permission-mode default").option("--chrome", "Enable Claude in Chrome integration").option("--no-chrome", "Disable Claude in Chrome integration").option("--worktree-mode <mode>", "Git worktree mode: off, prompt, require (default: prompt)").option("--keep-alive", "Enable system sleep prevention (default: enabled)").option("--no-keep-alive", "Disable system sleep prevention").option("--setup", "Run interactive setup wizard (reconfigure existing settings)").option("--debug", "Enable debug logging").option("--skip-version-check", "Skip Claude CLI version compatibility check").option("--auto-restart", "Enable auto-restart on updates (default when autoUpdate enabled)").option("--no-auto-restart", "Disable auto-restart on updates").option("--headless", "Run without interactive UI (logs to stdout)").parse();
|
|
79361
|
+
program.name("claude-threads").version(VERSION).description("Share Claude Code sessions in Mattermost").option("--url <url>", "Mattermost server URL").option("--token <token>", "Mattermost bot token").option("--channel <id>", "Mattermost channel ID").option("--bot-name <name>", "Bot mention name (default: claude-code)").option("--allowed-users <users>", "Comma-separated allowed usernames").option("--permission-mode <mode>", "Permission mode: default | auto | bypass (default: from config)").option("--skip-permissions", "[deprecated] Alias for --permission-mode bypass").option("--no-skip-permissions", "[deprecated] Alias for --permission-mode default").option("--chrome", "Enable Claude in Chrome integration").option("--no-chrome", "Disable Claude in Chrome integration").option("--worktree-mode <mode>", "Git worktree mode: off, prompt, require (default: prompt)").option("--session-header <mode>", "Per-thread session header: full | minimal | hidden. Overrides per-platform config.").option("--sticky-message <mode>", "Channel sticky message: full | minimal | hidden. Overrides per-platform config.").option("--keep-alive", "Enable system sleep prevention (default: enabled)").option("--no-keep-alive", "Disable system sleep prevention").option("--setup", "Run interactive setup wizard (reconfigure existing settings)").option("--debug", "Enable debug logging").option("--skip-version-check", "Skip Claude CLI version compatibility check").option("--auto-restart", "Enable auto-restart on updates (default when autoUpdate enabled)").option("--no-auto-restart", "Disable auto-restart on updates").option("--headless", "Run without interactive UI (logs to stdout)").parse();
|
|
79153
79362
|
var opts = program.opts();
|
|
79154
79363
|
var forcedInteractive = !!process.env.CLAUDE_THREADS_INTERACTIVE;
|
|
79155
79364
|
var isHeadless = opts.headless || !forcedInteractive && (!process.stdout.isTTY || !process.stdin.isTTY);
|
|
@@ -79237,6 +79446,14 @@ async function startWithoutDaemon() {
|
|
|
79237
79446
|
console.error(red(` ❌ Invalid --permission-mode: "${opts.permissionMode}". Must be one of: default, auto, bypass.`));
|
|
79238
79447
|
process.exit(1);
|
|
79239
79448
|
}
|
|
79449
|
+
if (opts.sessionHeader !== undefined && !isOverheadVisibility(opts.sessionHeader)) {
|
|
79450
|
+
console.error(red(` ❌ Invalid --session-header: "${opts.sessionHeader}". Must be one of: ${OVERHEAD_VISIBILITY_VALUES.join(", ")}.`));
|
|
79451
|
+
process.exit(1);
|
|
79452
|
+
}
|
|
79453
|
+
if (opts.stickyMessage !== undefined && !isOverheadVisibility(opts.stickyMessage)) {
|
|
79454
|
+
console.error(red(` ❌ Invalid --sticky-message: "${opts.stickyMessage}". Must be one of: ${OVERHEAD_VISIBILITY_VALUES.join(", ")}.`));
|
|
79455
|
+
process.exit(1);
|
|
79456
|
+
}
|
|
79240
79457
|
const cliArgs = {
|
|
79241
79458
|
url: opts.url,
|
|
79242
79459
|
token: opts.token,
|
|
@@ -79247,7 +79464,9 @@ async function startWithoutDaemon() {
|
|
|
79247
79464
|
permissionMode: opts.permissionMode,
|
|
79248
79465
|
chrome: opts.chrome,
|
|
79249
79466
|
worktreeMode: opts.worktreeMode,
|
|
79250
|
-
keepAlive: opts.keepAlive
|
|
79467
|
+
keepAlive: opts.keepAlive,
|
|
79468
|
+
sessionHeader: opts.sessionHeader,
|
|
79469
|
+
stickyMessage: opts.stickyMessage
|
|
79251
79470
|
};
|
|
79252
79471
|
if (opts.setup) {
|
|
79253
79472
|
await runOnboarding(true);
|
|
@@ -79268,6 +79487,16 @@ async function startWithoutDaemon() {
|
|
|
79268
79487
|
if (cliArgs.keepAlive !== undefined) {
|
|
79269
79488
|
newConfig.keepAlive = cliArgs.keepAlive;
|
|
79270
79489
|
}
|
|
79490
|
+
if (cliArgs.sessionHeader !== undefined) {
|
|
79491
|
+
for (const p of newConfig.platforms) {
|
|
79492
|
+
p.sessionHeader = cliArgs.sessionHeader;
|
|
79493
|
+
}
|
|
79494
|
+
}
|
|
79495
|
+
if (cliArgs.stickyMessage !== undefined) {
|
|
79496
|
+
for (const p of newConfig.platforms) {
|
|
79497
|
+
p.stickyMessage = cliArgs.stickyMessage;
|
|
79498
|
+
}
|
|
79499
|
+
}
|
|
79271
79500
|
const keepAliveEnabled = newConfig.keepAlive !== false;
|
|
79272
79501
|
if (!newConfig.platforms || newConfig.platforms.length === 0) {
|
|
79273
79502
|
throw new Error("No platforms configured. Run with --setup to configure.");
|
|
@@ -79441,7 +79670,10 @@ async function startWithoutDaemon() {
|
|
|
79441
79670
|
});
|
|
79442
79671
|
const client = createPlatformClient(platformConfig);
|
|
79443
79672
|
platforms.set(platformConfig.id, client);
|
|
79444
|
-
session.addPlatform(platformConfig.id, client
|
|
79673
|
+
session.addPlatform(platformConfig.id, client, {
|
|
79674
|
+
sessionHeader: resolveOverheadVisibility(platformConfig.sessionHeader, `platforms[${platformConfig.id}].sessionHeader`),
|
|
79675
|
+
stickyMessage: resolveOverheadVisibility(platformConfig.stickyMessage, `platforms[${platformConfig.id}].stickyMessage`)
|
|
79676
|
+
});
|
|
79445
79677
|
wirePlatformEvents(platformConfig.id, client, session, ui);
|
|
79446
79678
|
}
|
|
79447
79679
|
const enabledPlatforms = Array.from(platforms.entries()).filter(([id]) => platformEnabledState.get(id) ?? true);
|
package/dist/mcp/mcp-server.js
CHANGED
|
@@ -3035,6 +3035,9 @@ var require_data = __commonJS((exports, module) => {
|
|
|
3035
3035
|
var require_utils = __commonJS((exports, module) => {
|
|
3036
3036
|
var isUUID = RegExp.prototype.test.bind(/^[\da-f]{8}-[\da-f]{4}-[\da-f]{4}-[\da-f]{4}-[\da-f]{12}$/iu);
|
|
3037
3037
|
var isIPv4 = RegExp.prototype.test.bind(/^(?:(?:25[0-5]|2[0-4]\d|1\d{2}|[1-9]\d|\d)\.){3}(?:25[0-5]|2[0-4]\d|1\d{2}|[1-9]\d|\d)$/u);
|
|
3038
|
+
var isHexPair = RegExp.prototype.test.bind(/^[\da-f]{2}$/iu);
|
|
3039
|
+
var isUnreserved = RegExp.prototype.test.bind(/^[\da-z\-._~]$/iu);
|
|
3040
|
+
var isPathCharacter = RegExp.prototype.test.bind(/^[\da-z\-._~!$&'()*+,;=:@/]$/iu);
|
|
3038
3041
|
function stringArrayToHexStripped(input) {
|
|
3039
3042
|
let acc = "";
|
|
3040
3043
|
let code = 0;
|
|
@@ -3228,27 +3231,77 @@ var require_utils = __commonJS((exports, module) => {
|
|
|
3228
3231
|
}
|
|
3229
3232
|
return output.join("");
|
|
3230
3233
|
}
|
|
3231
|
-
|
|
3232
|
-
|
|
3233
|
-
|
|
3234
|
-
|
|
3235
|
-
|
|
3236
|
-
|
|
3237
|
-
|
|
3238
|
-
|
|
3239
|
-
|
|
3240
|
-
|
|
3234
|
+
var HOST_DELIMS = { "@": "%40", "/": "%2F", "?": "%3F", "#": "%23", ":": "%3A" };
|
|
3235
|
+
var HOST_DELIM_RE = /[@/?#:]/g;
|
|
3236
|
+
var HOST_DELIM_NO_COLON_RE = /[@/?#]/g;
|
|
3237
|
+
function reescapeHostDelimiters(host, isIP) {
|
|
3238
|
+
const re = isIP ? HOST_DELIM_NO_COLON_RE : HOST_DELIM_RE;
|
|
3239
|
+
re.lastIndex = 0;
|
|
3240
|
+
return host.replace(re, (ch) => HOST_DELIMS[ch]);
|
|
3241
|
+
}
|
|
3242
|
+
function normalizePercentEncoding(input, decodeUnreserved = false) {
|
|
3243
|
+
if (input.indexOf("%") === -1) {
|
|
3244
|
+
return input;
|
|
3241
3245
|
}
|
|
3242
|
-
|
|
3243
|
-
|
|
3246
|
+
let output = "";
|
|
3247
|
+
for (let i = 0;i < input.length; i++) {
|
|
3248
|
+
if (input[i] === "%" && i + 2 < input.length) {
|
|
3249
|
+
const hex = input.slice(i + 1, i + 3);
|
|
3250
|
+
if (isHexPair(hex)) {
|
|
3251
|
+
const normalizedHex = hex.toUpperCase();
|
|
3252
|
+
const decoded = String.fromCharCode(parseInt(normalizedHex, 16));
|
|
3253
|
+
if (decodeUnreserved && isUnreserved(decoded)) {
|
|
3254
|
+
output += decoded;
|
|
3255
|
+
} else {
|
|
3256
|
+
output += "%" + normalizedHex;
|
|
3257
|
+
}
|
|
3258
|
+
i += 2;
|
|
3259
|
+
continue;
|
|
3260
|
+
}
|
|
3261
|
+
}
|
|
3262
|
+
output += input[i];
|
|
3244
3263
|
}
|
|
3245
|
-
|
|
3246
|
-
|
|
3264
|
+
return output;
|
|
3265
|
+
}
|
|
3266
|
+
function normalizePathEncoding(input) {
|
|
3267
|
+
let output = "";
|
|
3268
|
+
for (let i = 0;i < input.length; i++) {
|
|
3269
|
+
if (input[i] === "%" && i + 2 < input.length) {
|
|
3270
|
+
const hex = input.slice(i + 1, i + 3);
|
|
3271
|
+
if (isHexPair(hex)) {
|
|
3272
|
+
const normalizedHex = hex.toUpperCase();
|
|
3273
|
+
const decoded = String.fromCharCode(parseInt(normalizedHex, 16));
|
|
3274
|
+
if (decoded !== "." && isUnreserved(decoded)) {
|
|
3275
|
+
output += decoded;
|
|
3276
|
+
} else {
|
|
3277
|
+
output += "%" + normalizedHex;
|
|
3278
|
+
}
|
|
3279
|
+
i += 2;
|
|
3280
|
+
continue;
|
|
3281
|
+
}
|
|
3282
|
+
}
|
|
3283
|
+
if (isPathCharacter(input[i])) {
|
|
3284
|
+
output += input[i];
|
|
3285
|
+
} else {
|
|
3286
|
+
output += escape(input[i]);
|
|
3287
|
+
}
|
|
3247
3288
|
}
|
|
3248
|
-
|
|
3249
|
-
|
|
3289
|
+
return output;
|
|
3290
|
+
}
|
|
3291
|
+
function escapePreservingEscapes(input) {
|
|
3292
|
+
let output = "";
|
|
3293
|
+
for (let i = 0;i < input.length; i++) {
|
|
3294
|
+
if (input[i] === "%" && i + 2 < input.length) {
|
|
3295
|
+
const hex = input.slice(i + 1, i + 3);
|
|
3296
|
+
if (isHexPair(hex)) {
|
|
3297
|
+
output += "%" + hex.toUpperCase();
|
|
3298
|
+
i += 2;
|
|
3299
|
+
continue;
|
|
3300
|
+
}
|
|
3301
|
+
}
|
|
3302
|
+
output += escape(input[i]);
|
|
3250
3303
|
}
|
|
3251
|
-
return
|
|
3304
|
+
return output;
|
|
3252
3305
|
}
|
|
3253
3306
|
function recomposeAuthority(component) {
|
|
3254
3307
|
const uriTokens = [];
|
|
@@ -3263,7 +3316,7 @@ var require_utils = __commonJS((exports, module) => {
|
|
|
3263
3316
|
if (ipV6res.isIPV6 === true) {
|
|
3264
3317
|
host = `[${ipV6res.escapedHost}]`;
|
|
3265
3318
|
} else {
|
|
3266
|
-
host =
|
|
3319
|
+
host = reescapeHostDelimiters(host, false);
|
|
3267
3320
|
}
|
|
3268
3321
|
}
|
|
3269
3322
|
uriTokens.push(host);
|
|
@@ -3277,7 +3330,10 @@ var require_utils = __commonJS((exports, module) => {
|
|
|
3277
3330
|
module.exports = {
|
|
3278
3331
|
nonSimpleDomain,
|
|
3279
3332
|
recomposeAuthority,
|
|
3280
|
-
|
|
3333
|
+
reescapeHostDelimiters,
|
|
3334
|
+
normalizePercentEncoding,
|
|
3335
|
+
normalizePathEncoding,
|
|
3336
|
+
escapePreservingEscapes,
|
|
3281
3337
|
removeDotSegments,
|
|
3282
3338
|
isIPv4,
|
|
3283
3339
|
isUUID,
|
|
@@ -3462,11 +3518,11 @@ var require_schemes = __commonJS((exports, module) => {
|
|
|
3462
3518
|
|
|
3463
3519
|
// node_modules/fast-uri/index.js
|
|
3464
3520
|
var require_fast_uri = __commonJS((exports, module) => {
|
|
3465
|
-
var { normalizeIPv6, removeDotSegments, recomposeAuthority,
|
|
3521
|
+
var { normalizeIPv6, removeDotSegments, recomposeAuthority, normalizePercentEncoding, normalizePathEncoding, escapePreservingEscapes, reescapeHostDelimiters, isIPv4, nonSimpleDomain } = require_utils();
|
|
3466
3522
|
var { SCHEMES, getSchemeHandler } = require_schemes();
|
|
3467
3523
|
function normalize(uri, options) {
|
|
3468
3524
|
if (typeof uri === "string") {
|
|
3469
|
-
uri =
|
|
3525
|
+
uri = normalizeString(uri, options);
|
|
3470
3526
|
} else if (typeof uri === "object") {
|
|
3471
3527
|
uri = parse6(serialize(uri, options), options);
|
|
3472
3528
|
}
|
|
@@ -3532,19 +3588,9 @@ var require_fast_uri = __commonJS((exports, module) => {
|
|
|
3532
3588
|
return target;
|
|
3533
3589
|
}
|
|
3534
3590
|
function equal(uriA, uriB, options) {
|
|
3535
|
-
|
|
3536
|
-
|
|
3537
|
-
|
|
3538
|
-
} else if (typeof uriA === "object") {
|
|
3539
|
-
uriA = serialize(normalizeComponentEncoding(uriA, true), { ...options, skipEscape: true });
|
|
3540
|
-
}
|
|
3541
|
-
if (typeof uriB === "string") {
|
|
3542
|
-
uriB = unescape(uriB);
|
|
3543
|
-
uriB = serialize(normalizeComponentEncoding(parse6(uriB, options), true), { ...options, skipEscape: true });
|
|
3544
|
-
} else if (typeof uriB === "object") {
|
|
3545
|
-
uriB = serialize(normalizeComponentEncoding(uriB, true), { ...options, skipEscape: true });
|
|
3546
|
-
}
|
|
3547
|
-
return uriA.toLowerCase() === uriB.toLowerCase();
|
|
3591
|
+
const normalizedA = normalizeComparableURI(uriA, options);
|
|
3592
|
+
const normalizedB = normalizeComparableURI(uriB, options);
|
|
3593
|
+
return normalizedA !== undefined && normalizedB !== undefined && normalizedA.toLowerCase() === normalizedB.toLowerCase();
|
|
3548
3594
|
}
|
|
3549
3595
|
function serialize(cmpts, opts) {
|
|
3550
3596
|
const component = {
|
|
@@ -3570,12 +3616,12 @@ var require_fast_uri = __commonJS((exports, module) => {
|
|
|
3570
3616
|
schemeHandler.serialize(component, options);
|
|
3571
3617
|
if (component.path !== undefined) {
|
|
3572
3618
|
if (!options.skipEscape) {
|
|
3573
|
-
component.path =
|
|
3619
|
+
component.path = escapePreservingEscapes(component.path);
|
|
3574
3620
|
if (component.scheme !== undefined) {
|
|
3575
3621
|
component.path = component.path.split("%3A").join(":");
|
|
3576
3622
|
}
|
|
3577
3623
|
} else {
|
|
3578
|
-
component.path =
|
|
3624
|
+
component.path = normalizePercentEncoding(component.path);
|
|
3579
3625
|
}
|
|
3580
3626
|
}
|
|
3581
3627
|
if (options.reference !== "suffix" && component.scheme) {
|
|
@@ -3610,7 +3656,16 @@ var require_fast_uri = __commonJS((exports, module) => {
|
|
|
3610
3656
|
return uriTokens.join("");
|
|
3611
3657
|
}
|
|
3612
3658
|
var URI_PARSE = /^(?:([^#/:?]+):)?(?:\/\/((?:([^#/?@]*)@)?(\[[^#/?\]]+\]|[^#/:?]*)(?::(\d*))?))?([^#?]*)(?:\?([^#]*))?(?:#((?:.|[\n\r])*))?/u;
|
|
3613
|
-
function
|
|
3659
|
+
function getParseError(parsed, matches) {
|
|
3660
|
+
if (matches[2] !== undefined && parsed.path && parsed.path[0] !== "/") {
|
|
3661
|
+
return 'URI path must start with "/" when authority is present.';
|
|
3662
|
+
}
|
|
3663
|
+
if (typeof parsed.port === "number" && (parsed.port < 0 || parsed.port > 65535)) {
|
|
3664
|
+
return "URI port is malformed.";
|
|
3665
|
+
}
|
|
3666
|
+
return;
|
|
3667
|
+
}
|
|
3668
|
+
function parseWithStatus(uri, opts) {
|
|
3614
3669
|
const options = Object.assign({}, opts);
|
|
3615
3670
|
const parsed = {
|
|
3616
3671
|
scheme: undefined,
|
|
@@ -3621,6 +3676,7 @@ var require_fast_uri = __commonJS((exports, module) => {
|
|
|
3621
3676
|
query: undefined,
|
|
3622
3677
|
fragment: undefined
|
|
3623
3678
|
};
|
|
3679
|
+
let malformedAuthorityOrPort = false;
|
|
3624
3680
|
let isIP = false;
|
|
3625
3681
|
if (options.reference === "suffix") {
|
|
3626
3682
|
if (options.scheme) {
|
|
@@ -3641,6 +3697,11 @@ var require_fast_uri = __commonJS((exports, module) => {
|
|
|
3641
3697
|
if (isNaN(parsed.port)) {
|
|
3642
3698
|
parsed.port = matches[5];
|
|
3643
3699
|
}
|
|
3700
|
+
const parseError = getParseError(parsed, matches);
|
|
3701
|
+
if (parseError !== undefined) {
|
|
3702
|
+
parsed.error = parsed.error || parseError;
|
|
3703
|
+
malformedAuthorityOrPort = true;
|
|
3704
|
+
}
|
|
3644
3705
|
if (parsed.host) {
|
|
3645
3706
|
const ipv4result = isIPv4(parsed.host);
|
|
3646
3707
|
if (ipv4result === false) {
|
|
@@ -3679,14 +3740,18 @@ var require_fast_uri = __commonJS((exports, module) => {
|
|
|
3679
3740
|
parsed.scheme = unescape(parsed.scheme);
|
|
3680
3741
|
}
|
|
3681
3742
|
if (parsed.host !== undefined) {
|
|
3682
|
-
parsed.host = unescape(parsed.host);
|
|
3743
|
+
parsed.host = reescapeHostDelimiters(unescape(parsed.host), isIP);
|
|
3683
3744
|
}
|
|
3684
3745
|
}
|
|
3685
3746
|
if (parsed.path) {
|
|
3686
|
-
parsed.path =
|
|
3747
|
+
parsed.path = normalizePathEncoding(parsed.path);
|
|
3687
3748
|
}
|
|
3688
3749
|
if (parsed.fragment) {
|
|
3689
|
-
|
|
3750
|
+
try {
|
|
3751
|
+
parsed.fragment = encodeURI(decodeURIComponent(parsed.fragment));
|
|
3752
|
+
} catch {
|
|
3753
|
+
parsed.error = parsed.error || "URI malformed";
|
|
3754
|
+
}
|
|
3690
3755
|
}
|
|
3691
3756
|
}
|
|
3692
3757
|
if (schemeHandler && schemeHandler.parse) {
|
|
@@ -3695,7 +3760,29 @@ var require_fast_uri = __commonJS((exports, module) => {
|
|
|
3695
3760
|
} else {
|
|
3696
3761
|
parsed.error = parsed.error || "URI can not be parsed.";
|
|
3697
3762
|
}
|
|
3698
|
-
return parsed;
|
|
3763
|
+
return { parsed, malformedAuthorityOrPort };
|
|
3764
|
+
}
|
|
3765
|
+
function parse6(uri, opts) {
|
|
3766
|
+
return parseWithStatus(uri, opts).parsed;
|
|
3767
|
+
}
|
|
3768
|
+
function normalizeString(uri, opts) {
|
|
3769
|
+
return normalizeStringWithStatus(uri, opts).normalized;
|
|
3770
|
+
}
|
|
3771
|
+
function normalizeStringWithStatus(uri, opts) {
|
|
3772
|
+
const { parsed, malformedAuthorityOrPort } = parseWithStatus(uri, opts);
|
|
3773
|
+
return {
|
|
3774
|
+
normalized: malformedAuthorityOrPort ? uri : serialize(parsed, opts),
|
|
3775
|
+
malformedAuthorityOrPort
|
|
3776
|
+
};
|
|
3777
|
+
}
|
|
3778
|
+
function normalizeComparableURI(uri, opts) {
|
|
3779
|
+
if (typeof uri === "string") {
|
|
3780
|
+
const { normalized, malformedAuthorityOrPort } = normalizeStringWithStatus(uri, opts);
|
|
3781
|
+
return malformedAuthorityOrPort ? undefined : normalized;
|
|
3782
|
+
}
|
|
3783
|
+
if (typeof uri === "object") {
|
|
3784
|
+
return serialize(uri, opts);
|
|
3785
|
+
}
|
|
3699
3786
|
}
|
|
3700
3787
|
var fastUri = {
|
|
3701
3788
|
SCHEMES,
|
|
@@ -50956,7 +51043,6 @@ function formatBytes(bytes) {
|
|
|
50956
51043
|
|
|
50957
51044
|
// src/operations/streaming/handler.ts
|
|
50958
51045
|
var log2 = createLogger("streaming");
|
|
50959
|
-
var MAX_UPLOAD_SIZE = 100 * 1024 * 1024;
|
|
50960
51046
|
async function postSkippedFilesFeedback(platform, threadId, skipped) {
|
|
50961
51047
|
if (skipped.length === 0)
|
|
50962
51048
|
return;
|
|
@@ -54449,6 +54535,7 @@ var pausedPlatforms = new Map;
|
|
|
54449
54535
|
var lastCleanupTime = new Map;
|
|
54450
54536
|
var CLEANUP_THROTTLE_MS = 5 * 60 * 1000;
|
|
54451
54537
|
var CLEANUP_MAX_AGE_MS = 60 * 60 * 1000;
|
|
54538
|
+
var hiddenCleanupDone = new Set;
|
|
54452
54539
|
// node_modules/@redactpii/node/lib/index.mjs
|
|
54453
54540
|
class Redactor {
|
|
54454
54541
|
apiKey;
|
|
@@ -55722,7 +55809,7 @@ function createPassthroughHandler(slashCommand) {
|
|
|
55722
55809
|
return { handled: false };
|
|
55723
55810
|
}
|
|
55724
55811
|
if (ctx.isAllowed) {
|
|
55725
|
-
await ctx.sessionManager.sendFollowUp(ctx.threadId, `/${slashCommand}
|
|
55812
|
+
await ctx.sessionManager.sendFollowUp(ctx.threadId, `/${slashCommand}`, undefined, undefined, undefined, { system: true });
|
|
55726
55813
|
}
|
|
55727
55814
|
return { handled: true };
|
|
55728
55815
|
};
|
package/docs/CONFIGURATION.md
CHANGED
|
@@ -57,6 +57,8 @@ platforms:
|
|
|
57
57
|
| `botName` | No | Mention name (default: `claude-code`) |
|
|
58
58
|
| `allowedUsers` | No | List of usernames who can use the bot |
|
|
59
59
|
| `skipPermissions` | No | Auto-approve actions (default: `false`) |
|
|
60
|
+
| `sessionHeader` | No | Per-thread header visibility: `full` (default) / `minimal` (status bar only) / `hidden` (no header post) |
|
|
61
|
+
| `stickyMessage` | No | Channel sticky visibility: `full` (default) / `minimal` (status bar only) / `hidden` (no sticky, no bumping) |
|
|
60
62
|
|
|
61
63
|
### Slack
|
|
62
64
|
|
|
@@ -71,6 +73,23 @@ platforms:
|
|
|
71
73
|
| `botName` | No | Mention name (default: `claude`) |
|
|
72
74
|
| `allowedUsers` | No | List of Slack usernames |
|
|
73
75
|
| `skipPermissions` | No | Auto-approve actions (default: `false`) |
|
|
76
|
+
| `sessionHeader` | No | Per-thread header visibility: `full` (default) / `minimal` (status bar only) / `hidden` (no header post) |
|
|
77
|
+
| `stickyMessage` | No | Channel sticky visibility: `full` (default) / `minimal` (status bar only) / `hidden` (no sticky, no bumping) |
|
|
78
|
+
|
|
79
|
+
### Quieting the bot's overhead messages
|
|
80
|
+
|
|
81
|
+
Both the per-thread session header and the channel sticky message default to `full` for backward compatibility. To strip them down on a noisy channel, set the per-platform fields in `config.yaml`:
|
|
82
|
+
|
|
83
|
+
```yaml
|
|
84
|
+
platforms:
|
|
85
|
+
- id: mattermost-main
|
|
86
|
+
type: mattermost
|
|
87
|
+
# ... credentials ...
|
|
88
|
+
sessionHeader: hidden # no header post — Claude's reply is the first message in the thread
|
|
89
|
+
stickyMessage: minimal # one-line status bar at the channel bottom, no sessions list
|
|
90
|
+
```
|
|
91
|
+
|
|
92
|
+
Note: the per-platform `stickyMessage: <mode>` field is distinct from the top-level `Config.stickyMessage: { description, footer }` block, which still customizes the full sticky for platforms not in `hidden` mode.
|
|
74
93
|
|
|
75
94
|
## Claude Accounts (optional, multi-account mode)
|
|
76
95
|
|
|
@@ -139,6 +158,8 @@ Options:
|
|
|
139
158
|
--chrome Enable Chrome integration
|
|
140
159
|
--no-chrome Disable Chrome integration
|
|
141
160
|
--worktree-mode <mode> Git worktree mode: off, prompt, require
|
|
161
|
+
--session-header <mode> Per-thread header: full | minimal | hidden (overrides per-platform config)
|
|
162
|
+
--sticky-message <mode> Channel sticky: full | minimal | hidden (overrides per-platform config)
|
|
142
163
|
--setup Re-run setup wizard
|
|
143
164
|
--debug Enable debug logging
|
|
144
165
|
--version Show version
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "claude-threads",
|
|
3
|
-
"version": "1.
|
|
3
|
+
"version": "1.16.1",
|
|
4
4
|
"description": "Share Claude Code sessions live in a Mattermost channel with interactive features",
|
|
5
5
|
"main": "dist/index.js",
|
|
6
6
|
"type": "module",
|
|
@@ -71,7 +71,7 @@
|
|
|
71
71
|
"commander": "^14.0.2",
|
|
72
72
|
"diff": "^8.0.3",
|
|
73
73
|
"express-rate-limit": "^8.3.0",
|
|
74
|
-
"hono": "^4.12.
|
|
74
|
+
"hono": "^4.12.18",
|
|
75
75
|
"ink": "^6.6.0",
|
|
76
76
|
"ink-scroll-view": "^0.3.5",
|
|
77
77
|
"js-yaml": "^4.1.1",
|
|
@@ -94,7 +94,7 @@
|
|
|
94
94
|
"@types/ws": "^8.18.0",
|
|
95
95
|
"eslint": "^10.1.0",
|
|
96
96
|
"husky": "^9.1.7",
|
|
97
|
-
"lint-staged": "^
|
|
97
|
+
"lint-staged": "^17.0.4",
|
|
98
98
|
"prettier": "^3.4.2",
|
|
99
99
|
"typescript": "^6.0.2",
|
|
100
100
|
"typescript-eslint": "^8.57.2"
|
|
@@ -115,7 +115,8 @@
|
|
|
115
115
|
"express-rate-limit": "$express-rate-limit",
|
|
116
116
|
"flatted": ">=3.4.0",
|
|
117
117
|
"picomatch": ">=2.3.2",
|
|
118
|
-
"path-to-regexp": ">=8.4.0"
|
|
118
|
+
"path-to-regexp": ">=8.4.0",
|
|
119
|
+
"fast-uri": ">=3.1.2"
|
|
119
120
|
},
|
|
120
121
|
"resolutions": {
|
|
121
122
|
"hono": "$hono",
|
|
@@ -123,6 +124,7 @@
|
|
|
123
124
|
"express-rate-limit": "$express-rate-limit",
|
|
124
125
|
"flatted": ">=3.4.0",
|
|
125
126
|
"picomatch": ">=2.3.2",
|
|
126
|
-
"path-to-regexp": ">=8.4.0"
|
|
127
|
+
"path-to-regexp": ">=8.4.0",
|
|
128
|
+
"fast-uri": ">=3.1.2"
|
|
127
129
|
}
|
|
128
130
|
}
|