claude-threads 1.31.1 → 1.32.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +15 -0
- package/README.md +2 -2
- package/dist/index.js +108 -14
- package/dist/mcp/mcp-server.js +4 -1
- package/docs/CONFIGURATION.md +10 -2
- package/package.json +1 -1
package/CHANGELOG.md
CHANGED
|
@@ -5,6 +5,21 @@ 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
|
+
## [Unreleased]
|
|
9
|
+
|
|
10
|
+
## [1.32.0] - 2026-09-01
|
|
11
|
+
|
|
12
|
+
### Added
|
|
13
|
+
- **Slack: shared event source — one Socket Mode connection per app** (#502, thanks @kaza). Slack round-robins Socket Mode envelopes across all of an app's open connections, so a second `SlackClient` on the same app token silently steals events from the first. Now exactly one client (the parent) owns the socket; other clients register as secondaries and receive their channels' events injected by the parent — Web API calls stay independent per instance. The parent mirrors connection state onto secondaries (idempotently re-armed across disconnects), and on reconnect, missed-message recovery runs for the parent and every registered secondary. Zero behavior change for existing single-channel configs; this is the mechanism that unblocks DM auto-discovery on Slack and other multi-channel consumers.
|
|
14
|
+
|
|
15
|
+
## [1.31.2] - 2026-08-30
|
|
16
|
+
|
|
17
|
+
### Added
|
|
18
|
+
- **The approval posture of a routine or watch is now visible and changeable after creation.** `!routines` / `!watches` listings show each item's posture inline (👍 approvals · ✅ autonomous), and a new owner-gated `!routines approval <n> on|off` / `!watches approval <n> on|off` flips it — `off` makes an item run autonomously, `on` restores per-action approval. Turning a watch autonomous is the same sensitive choice the creation card gates behind the owner and an explicit ✅, so the flip command is owner-gated too; the safe approvals-required posture stays the default for older data.
|
|
19
|
+
|
|
20
|
+
### Security
|
|
21
|
+
- **Active-session thread-id lookups are now scoped by `platformId`**, completing the cross-platform privacy boundary that 1.31.0/1.31.1 established for the persisted store. `SessionRegistry.findByThreadId` takes an optional `platformId` and resolves O(1) against the composite key when given; the message router (`handleMessage`) and the in-session authorization check (`isUserAllowedInSession`) now pass it, so a thread id that collides across platforms can no longer resolve to — or authorize a user against — another platform's *active* session, and the router and auth check always agree on which session a message belongs to. Defense-in-depth: real Mattermost (26-char) and Slack (dotted-ts) ids don't collide today.
|
|
22
|
+
|
|
8
23
|
## [1.31.1] - 2026-08-29
|
|
9
24
|
|
|
10
25
|
### Security
|
package/README.md
CHANGED
|
@@ -126,9 +126,9 @@ Type `!help` in any session thread:
|
|
|
126
126
|
| `!remember <text>` | Save a note to the channel's shared memory |
|
|
127
127
|
| `!memory` | Show channel memory (`forget <n\|text>` removes entries) |
|
|
128
128
|
| `!routine <schedule, task>` | Schedule a recurring routine in natural language (confirmed with 👍) |
|
|
129
|
-
| `!routines` | List routines (`pause\|resume\|delete\|run <n
|
|
129
|
+
| `!routines` | List routines (`pause\|resume\|delete\|run <n>`, `approval <n> on\|off` to manage) |
|
|
130
130
|
| `!watch <when ..., task>` | Create an event trigger in natural language (confirmed with 👍) |
|
|
131
|
-
| `!watches` | List event triggers (`pause\|resume\|delete <n
|
|
131
|
+
| `!watches` | List event triggers (`pause\|resume\|delete <n>`, `approval <n> on\|off` to manage) |
|
|
132
132
|
| `!update` | Show auto-update status (`!update now` / `!update defer`) |
|
|
133
133
|
| `!bug <desc>` | Report a bug with context (creates a GitHub issue) |
|
|
134
134
|
| `!approve` | Approve pending plan (alternative to 👍; also `!yes`) |
|
package/dist/index.js
CHANGED
|
@@ -65227,6 +65227,9 @@ async function postRoutineConfirmation(session, ctx, parsed, requestedBy, opts =
|
|
|
65227
65227
|
});
|
|
65228
65228
|
sessionLog4(session).info(`\uD83D\uDD58 Routine proposal posted for @${requestedBy}${opts.proposedByAgent ? " (agent-proposed)" : ""}: "${parsed.name}"`);
|
|
65229
65229
|
}
|
|
65230
|
+
function postureMarker(item) {
|
|
65231
|
+
return item.requireApproval === false ? " · ✅ autonomous" : " · \uD83D\uDC4D approvals";
|
|
65232
|
+
}
|
|
65230
65233
|
async function manageListItems(session, args, username, flavor) {
|
|
65231
65234
|
const formatter = session.platform.getFormatter();
|
|
65232
65235
|
const trimmed = args?.trim();
|
|
@@ -65244,13 +65247,32 @@ async function manageListItems(session, args, username, flavor) {
|
|
|
65244
65247
|
` + `${lines.join(`
|
|
65245
65248
|
`)}
|
|
65246
65249
|
|
|
65247
|
-
` + `${formatter.formatItalic(`Manage with ${"`" + cmd + " " + flavor.actions + " <n>`"}.`)}`);
|
|
65250
|
+
` + `${formatter.formatItalic(`Manage with ${"`" + cmd + " " + flavor.actions + " <n>`"}; set approval posture with ${"`" + cmd + " approval <n> on|off`"}.`)}`);
|
|
65248
65251
|
session.threadLogger?.logCommand(flavor.command, "list", username);
|
|
65249
65252
|
return;
|
|
65250
65253
|
}
|
|
65254
|
+
const approvalMatch = trimmed.match(/^approval\s+(\d+)\s+(on|off)$/i);
|
|
65255
|
+
if (approvalMatch) {
|
|
65256
|
+
const [, indexArg2, mode] = approvalMatch;
|
|
65257
|
+
const item2 = flavor.list()[parseInt(indexArg2, 10) - 1];
|
|
65258
|
+
if (!item2) {
|
|
65259
|
+
await post(session, "warning", `${flavor.emoji} No ${flavor.noun.toLowerCase()} ${indexArg2}. See ${formatter.formatCode(cmd)}.`);
|
|
65260
|
+
return;
|
|
65261
|
+
}
|
|
65262
|
+
if (!await requireSessionOwner(session, username, `manage ${flavor.command}`)) {
|
|
65263
|
+
return;
|
|
65264
|
+
}
|
|
65265
|
+
const requireApproval = mode.toLowerCase() === "on";
|
|
65266
|
+
await flavor.update(item2.id, { requireApproval });
|
|
65267
|
+
await post(session, "success", requireApproval ? `\uD83D\uDC4D ${flavor.noun} ${formatter.formatBold(item2.name)} will ask for approval before each action.` : `✅ ${flavor.noun} ${formatter.formatBold(item2.name)} will run autonomously — no approval prompts. Only leave this on for triggers you fully trust.`);
|
|
65268
|
+
sessionLog4(session).info(`${flavor.emoji} @${username}: ${cmd} approval ${indexArg2} ${mode.toLowerCase()} ("${item2.name}")`);
|
|
65269
|
+
auditCommand(session, flavor.command, `approval ${indexArg2} ${mode.toLowerCase()}`, username);
|
|
65270
|
+
session.threadLogger?.logCommand(flavor.command, `approval ${indexArg2} ${mode.toLowerCase()}`, username);
|
|
65271
|
+
return;
|
|
65272
|
+
}
|
|
65251
65273
|
const match = trimmed.match(new RegExp(`^(${flavor.actions})\\s+(\\d+)$`, "i"));
|
|
65252
65274
|
if (!match) {
|
|
65253
|
-
await post(session, "warning", `${flavor.emoji} Usage: ${formatter.formatCode(cmd)}
|
|
65275
|
+
await post(session, "warning", `${flavor.emoji} Usage: ${formatter.formatCode(cmd)}, ${formatter.formatCode(`${cmd} ${flavor.actions} <n>`)} or ${formatter.formatCode(`${cmd} approval <n> on|off`)}`);
|
|
65254
65276
|
return;
|
|
65255
65277
|
}
|
|
65256
65278
|
const [, action, indexArg] = match;
|
|
@@ -65302,7 +65324,7 @@ async function manageRoutines(session, args, username, ctx) {
|
|
|
65302
65324
|
describe: (r, formatter) => {
|
|
65303
65325
|
const status = r.enabled ? "" : " — ⏸️ paused";
|
|
65304
65326
|
const last = r.lastRunAt ? ` · last run ${formatIsoMinute(r.lastRunAt)} (${r.lastRunStatus})` : "";
|
|
65305
|
-
return `${formatter.formatBold(r.name)} — ${describeSchedule(r.schedule)} · by ${formatter.formatCode("@" + r.createdBy)}${status}${last}`;
|
|
65327
|
+
return `${formatter.formatBold(r.name)} — ${describeSchedule(r.schedule)} · by ${formatter.formatCode("@" + r.createdBy)}${postureMarker(r)}${status}${last}`;
|
|
65306
65328
|
},
|
|
65307
65329
|
list: () => ctx.state.routinesStore.list(platformId),
|
|
65308
65330
|
update: (id, patch) => ctx.state.routinesStore.update(platformId, id, patch),
|
|
@@ -65388,7 +65410,7 @@ async function manageWatches(session, args, username, ctx) {
|
|
|
65388
65410
|
describe: (w, formatter) => {
|
|
65389
65411
|
const status = w.enabled ? "" : " — ⏸️ paused";
|
|
65390
65412
|
const last = w.lastFiredAt ? ` · last fired ${formatIsoMinute(w.lastFiredAt)} (${w.lastFireStatus})` : "";
|
|
65391
|
-
return `${formatter.formatBold(w.name)} — fires when ${w.condition} · by ${formatter.formatCode("@" + w.createdBy)}${status}${last}`;
|
|
65413
|
+
return `${formatter.formatBold(w.name)} — fires when ${w.condition} · by ${formatter.formatCode("@" + w.createdBy)}${postureMarker(w)}${status}${last}`;
|
|
65392
65414
|
},
|
|
65393
65415
|
list: () => ctx.state.watchesStore.list(platformId),
|
|
65394
65416
|
update: (id, patch) => ctx.state.watchesStore.update(platformId, id, patch),
|
|
@@ -67888,7 +67910,10 @@ class SessionRegistry {
|
|
|
67888
67910
|
find(platformId, threadId) {
|
|
67889
67911
|
return this.sessions.get(this.getSessionId(platformId, threadId));
|
|
67890
67912
|
}
|
|
67891
|
-
findByThreadId(threadId) {
|
|
67913
|
+
findByThreadId(threadId, platformId) {
|
|
67914
|
+
if (platformId !== undefined) {
|
|
67915
|
+
return this.find(platformId, threadId);
|
|
67916
|
+
}
|
|
67892
67917
|
for (const session of this.sessions.values()) {
|
|
67893
67918
|
if (session.threadId === threadId) {
|
|
67894
67919
|
return session;
|
|
@@ -71916,8 +71941,13 @@ class SlackClient extends BasePlatformClient {
|
|
|
71916
71941
|
rateLimitRetryAfter = 0;
|
|
71917
71942
|
outboundFiles;
|
|
71918
71943
|
formatter = new SlackFormatter;
|
|
71919
|
-
|
|
71944
|
+
channelClients = new Map;
|
|
71945
|
+
sharedEventSource;
|
|
71946
|
+
socketConnected = false;
|
|
71947
|
+
constructor(platformConfig, sharedEventSource) {
|
|
71920
71948
|
super();
|
|
71949
|
+
this.sharedEventSource = sharedEventSource;
|
|
71950
|
+
this.installStateMirror();
|
|
71921
71951
|
this.platformId = platformConfig.id;
|
|
71922
71952
|
this.displayName = platformConfig.displayName;
|
|
71923
71953
|
this.botToken = platformConfig.botToken;
|
|
@@ -71931,6 +71961,55 @@ class SlackClient extends BasePlatformClient {
|
|
|
71931
71961
|
this.approvals = platformConfig.approvals;
|
|
71932
71962
|
this.ackReaction = normalizeAckReaction(platformConfig.ackReaction, `platforms[${platformConfig.id}].ackReaction`);
|
|
71933
71963
|
}
|
|
71964
|
+
stateMirrors = ["connected", "disconnected", "reconnecting"].map((state) => ({
|
|
71965
|
+
state,
|
|
71966
|
+
handler: (...args) => {
|
|
71967
|
+
for (const secondary of this.channelClients.values()) {
|
|
71968
|
+
secondary.emit(state, ...args);
|
|
71969
|
+
}
|
|
71970
|
+
}
|
|
71971
|
+
}));
|
|
71972
|
+
installStateMirror() {
|
|
71973
|
+
for (const { state, handler: handler2 } of this.stateMirrors) {
|
|
71974
|
+
this.off(state, handler2);
|
|
71975
|
+
this.on(state, handler2);
|
|
71976
|
+
}
|
|
71977
|
+
}
|
|
71978
|
+
onConnectionEstablished() {
|
|
71979
|
+
const wasReconnecting = this.isReconnecting;
|
|
71980
|
+
super.onConnectionEstablished();
|
|
71981
|
+
if (!wasReconnecting)
|
|
71982
|
+
return;
|
|
71983
|
+
for (const secondary of this.channelClients.values()) {
|
|
71984
|
+
secondary.recoverMissedMessages().catch((err) => {
|
|
71985
|
+
log40.warn(`Failed to recover missed messages for ${secondary.platformId}: ${err}`);
|
|
71986
|
+
});
|
|
71987
|
+
}
|
|
71988
|
+
}
|
|
71989
|
+
registerChannelClient(channelId, client) {
|
|
71990
|
+
if (channelId === this.channelId) {
|
|
71991
|
+
throw new Error(`registerChannelClient: ${channelId} is the parent's own channel — a secondary there can never receive events`);
|
|
71992
|
+
}
|
|
71993
|
+
this.channelClients.set(channelId, client);
|
|
71994
|
+
}
|
|
71995
|
+
unregisterChannelClient(channelId, client) {
|
|
71996
|
+
if (client && this.channelClients.get(channelId) !== client)
|
|
71997
|
+
return;
|
|
71998
|
+
this.channelClients.delete(channelId);
|
|
71999
|
+
}
|
|
72000
|
+
_injectSlackEvent(event) {
|
|
72001
|
+
this.handleSlackEvent(event);
|
|
72002
|
+
}
|
|
72003
|
+
disconnect() {
|
|
72004
|
+
if (this.sharedEventSource) {
|
|
72005
|
+
this.sharedEventSource.unregisterChannelClient(this.channelId, this);
|
|
72006
|
+
}
|
|
72007
|
+
try {
|
|
72008
|
+
return super.disconnect();
|
|
72009
|
+
} finally {
|
|
72010
|
+
this.installStateMirror();
|
|
72011
|
+
}
|
|
72012
|
+
}
|
|
71934
72013
|
normalizePlatformUser(slackUser) {
|
|
71935
72014
|
const displayName = slackUser.profile?.display_name || slackUser.profile?.real_name || slackUser.real_name || slackUser.name;
|
|
71936
72015
|
return {
|
|
@@ -72034,6 +72113,16 @@ class SlackClient extends BasePlatformClient {
|
|
|
72034
72113
|
return data;
|
|
72035
72114
|
}
|
|
72036
72115
|
async connect() {
|
|
72116
|
+
if (this.sharedEventSource) {
|
|
72117
|
+
await this.fetchBotUser();
|
|
72118
|
+
if (this.isIntentionalDisconnect)
|
|
72119
|
+
return;
|
|
72120
|
+
this.sharedEventSource.registerChannelClient(this.channelId, this);
|
|
72121
|
+
if (this.sharedEventSource.socketConnected) {
|
|
72122
|
+
this.emit("connected");
|
|
72123
|
+
}
|
|
72124
|
+
return;
|
|
72125
|
+
}
|
|
72037
72126
|
await this.fetchBotUser();
|
|
72038
72127
|
wsLogger.debug(`Slack bot user ID: ${this.botUserId}`);
|
|
72039
72128
|
const response = await this.appApi("POST", "apps.connections.open");
|
|
@@ -72073,12 +72162,8 @@ class SlackClient extends BasePlatformClient {
|
|
|
72073
72162
|
this.handleSocketModeEvent(envelope);
|
|
72074
72163
|
if (envelope.type === "hello") {
|
|
72075
72164
|
clearTimeout(connectionTimeout);
|
|
72165
|
+
this.socketConnected = true;
|
|
72076
72166
|
this.onConnectionEstablished();
|
|
72077
|
-
if (this.isReconnecting && this.lastProcessedTs) {
|
|
72078
|
-
this.recoverMissedMessages().catch((err) => {
|
|
72079
|
-
log40.warn(`Failed to recover missed messages: ${err}`);
|
|
72080
|
-
});
|
|
72081
|
-
}
|
|
72082
72167
|
doResolve();
|
|
72083
72168
|
}
|
|
72084
72169
|
} catch (err) {
|
|
@@ -72086,6 +72171,7 @@ class SlackClient extends BasePlatformClient {
|
|
|
72086
72171
|
}
|
|
72087
72172
|
};
|
|
72088
72173
|
this.ws.onclose = (event) => {
|
|
72174
|
+
this.socketConnected = false;
|
|
72089
72175
|
clearTimeout(connectionTimeout);
|
|
72090
72176
|
wsLogger.info(`Socket Mode: WebSocket disconnected (code: ${event.code}, reason: ${event.reason || "none"}, clean: ${event.wasClean})`);
|
|
72091
72177
|
if (!settled) {
|
|
@@ -72137,6 +72223,14 @@ class SlackClient extends BasePlatformClient {
|
|
|
72137
72223
|
}
|
|
72138
72224
|
}
|
|
72139
72225
|
handleSlackEvent(event) {
|
|
72226
|
+
const eventChannel = event.channel || event.item?.channel;
|
|
72227
|
+
if (eventChannel && eventChannel !== this.channelId) {
|
|
72228
|
+
const secondary = this.channelClients.get(eventChannel);
|
|
72229
|
+
if (secondary) {
|
|
72230
|
+
secondary._injectSlackEvent(event);
|
|
72231
|
+
return;
|
|
72232
|
+
}
|
|
72233
|
+
}
|
|
72140
72234
|
if (event.type === "message" && (!event.subtype || event.subtype === "file_share")) {
|
|
72141
72235
|
if (event.user === this.botUserId || event.bot_id) {
|
|
72142
72236
|
return;
|
|
@@ -75431,7 +75525,7 @@ class SessionManager extends EventEmitter4 {
|
|
|
75431
75525
|
return session !== undefined && session.claude.isRunning();
|
|
75432
75526
|
}
|
|
75433
75527
|
hasPausedSession(threadId, platformId) {
|
|
75434
|
-
if (this.registry.findByThreadId(threadId))
|
|
75528
|
+
if (this.registry.findByThreadId(threadId, platformId))
|
|
75435
75529
|
return false;
|
|
75436
75530
|
return this.registry.getPersistedByThreadId(threadId, platformId) !== undefined;
|
|
75437
75531
|
}
|
|
@@ -75743,7 +75837,7 @@ Mention me to start a session in this worktree.`, threadId);
|
|
|
75743
75837
|
}
|
|
75744
75838
|
}
|
|
75745
75839
|
isUserAllowedInSession(threadId, username, platformId) {
|
|
75746
|
-
const session = this.
|
|
75840
|
+
const session = this.registry.find(platformId, threadId);
|
|
75747
75841
|
if (!session) {
|
|
75748
75842
|
const persisted = this.getPersistedSession(threadId, platformId);
|
|
75749
75843
|
if (persisted) {
|
|
@@ -85468,7 +85562,7 @@ async function handleMessage(client, session, post2, user, options) {
|
|
|
85468
85562
|
await onKill?.(username);
|
|
85469
85563
|
return;
|
|
85470
85564
|
}
|
|
85471
|
-
const activeSession = session.registry.findByThreadId(threadRoot);
|
|
85565
|
+
const activeSession = session.registry.findByThreadId(threadRoot, platformId);
|
|
85472
85566
|
if (activeSession) {
|
|
85473
85567
|
const sideMentionActive = leadingOtherUserMention(client, message);
|
|
85474
85568
|
if (sideMentionActive) {
|
package/dist/mcp/mcp-server.js
CHANGED
|
@@ -58101,7 +58101,10 @@ class SessionRegistry {
|
|
|
58101
58101
|
find(platformId, threadId) {
|
|
58102
58102
|
return this.sessions.get(this.getSessionId(platformId, threadId));
|
|
58103
58103
|
}
|
|
58104
|
-
findByThreadId(threadId) {
|
|
58104
|
+
findByThreadId(threadId, platformId) {
|
|
58105
|
+
if (platformId !== undefined) {
|
|
58106
|
+
return this.find(platformId, threadId);
|
|
58107
|
+
}
|
|
58105
58108
|
for (const session of this.sessions.values()) {
|
|
58106
58109
|
if (session.threadId === threadId) {
|
|
58107
58110
|
return session;
|
package/docs/CONFIGURATION.md
CHANGED
|
@@ -382,8 +382,12 @@ confirmation says so.
|
|
|
382
382
|
|
|
383
383
|
**Managing:**
|
|
384
384
|
|
|
385
|
-
- `!routines` — numbered list with schedule, creator,
|
|
385
|
+
- `!routines` — numbered list with schedule, creator, last-run status, and
|
|
386
|
+
the approval posture (👍 approvals · ✅ autonomous)
|
|
386
387
|
- `!routines pause|resume|delete <n>` — owner-gated
|
|
388
|
+
- `!routines approval <n> on|off` — owner-gated; flip the approval posture
|
|
389
|
+
after creation. `on` restores per-action approval, `off` makes each run
|
|
390
|
+
autonomous (no approval prompts, even on a `skipPermissions` platform)
|
|
387
391
|
- `!routines run <n>` — fire now, outside the schedule (platform-allowed
|
|
388
392
|
users only — not temporarily `!invite`d guests; does not consume the
|
|
389
393
|
period's scheduled fire)
|
|
@@ -452,8 +456,12 @@ all three, and **nothing is saved until someone reacts 👍**.
|
|
|
452
456
|
|
|
453
457
|
**Managing:**
|
|
454
458
|
|
|
455
|
-
- `!watches` — numbered list with condition, creator,
|
|
459
|
+
- `!watches` — numbered list with condition, creator, last-fire status, and
|
|
460
|
+
the approval posture (👍 approvals · ✅ autonomous)
|
|
456
461
|
- `!watches pause|resume|delete <n>` — owner-gated
|
|
462
|
+
- `!watches approval <n> on|off` — owner-gated; flip the approval posture
|
|
463
|
+
after creation. `off` makes each fire autonomous — reserve it for triggers
|
|
464
|
+
you fully trust, since a watch fires on channel content anyone can post
|
|
457
465
|
- (No manual `run` — watches are event-driven; use `!routines run` for
|
|
458
466
|
on-demand work.)
|
|
459
467
|
|
package/package.json
CHANGED