claude-threads 1.31.0 β 1.31.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 +19 -0
- package/README.md +2 -2
- package/dist/index.js +39 -10
- package/dist/mcp/mcp-server.js +5 -1
- package/docs/CONFIGURATION.md +10 -2
- package/package.json +1 -1
package/CHANGELOG.md
CHANGED
|
@@ -5,6 +5,25 @@ 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.31.2] - 2026-08-30
|
|
11
|
+
|
|
12
|
+
### Added
|
|
13
|
+
- **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.
|
|
14
|
+
|
|
15
|
+
### Security
|
|
16
|
+
- **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.
|
|
17
|
+
|
|
18
|
+
## [1.31.1] - 2026-08-29
|
|
19
|
+
|
|
20
|
+
### Security
|
|
21
|
+
- **Regression guard for the cross-platform resume scoping (1.31.0).** The `resumePausedSession` sink is now covered by a test that fails if the `platformId` scope is removed β a message on one platform must never resume a session persisted under another platform whose thread id collides. The fix shipped correct in 1.31.0 but without this guard.
|
|
22
|
+
|
|
23
|
+
### Fixed
|
|
24
|
+
- **A rejected branch name can no longer break its own error message.** An invalid `!worktree <name>` whose name contains a backtick or newline is sanitized for display, so it stays inside its markdown code span in the error post.
|
|
25
|
+
- **A downgraded "β
Invite to session" reaction is no longer silent.** When a non-owner participant's β
is downgraded to a one-shot allow (only the owner may grant standing membership), the bot now says so, instead of leaving the reactor to assume the invite succeeded.
|
|
26
|
+
|
|
8
27
|
## [1.31.0] - 2026-08-29
|
|
9
28
|
|
|
10
29
|
### Added
|
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
|
@@ -62633,6 +62633,7 @@ class MessageApprovalExecutor extends BaseExecutor {
|
|
|
62633
62633
|
const isInviteAuthorized = pending.sessionOwner !== undefined && user === pending.sessionOwner || ctx.platform.isUserAllowed(user);
|
|
62634
62634
|
if (!isInviteAuthorized) {
|
|
62635
62635
|
ctx.logger.info(`Message approval invite (β
) from @${user} downgraded to allow-once: only the session owner may invite`);
|
|
62636
|
+
await ctx.createPost(`βΉοΈ Only the session owner can invite ${ctx.formatter.formatUserMention(pending.fromUser)} to the session β allowing this one message instead.`, { type: "system" });
|
|
62636
62637
|
const handled2 = await this.handleMessageApprovalResponse(postId, "allow", user, ctx);
|
|
62637
62638
|
ctx.logger.debug(`MessageApprovalExecutor: outcome=allow (invite downgraded), handled=${handled2}`);
|
|
62638
62639
|
return handled2;
|
|
@@ -65226,6 +65227,9 @@ async function postRoutineConfirmation(session, ctx, parsed, requestedBy, opts =
|
|
|
65226
65227
|
});
|
|
65227
65228
|
sessionLog4(session).info(`\uD83D\uDD58 Routine proposal posted for @${requestedBy}${opts.proposedByAgent ? " (agent-proposed)" : ""}: "${parsed.name}"`);
|
|
65228
65229
|
}
|
|
65230
|
+
function postureMarker(item) {
|
|
65231
|
+
return item.requireApproval === false ? " Β· β
autonomous" : " Β· \uD83D\uDC4D approvals";
|
|
65232
|
+
}
|
|
65229
65233
|
async function manageListItems(session, args, username, flavor) {
|
|
65230
65234
|
const formatter = session.platform.getFormatter();
|
|
65231
65235
|
const trimmed = args?.trim();
|
|
@@ -65243,13 +65247,32 @@ async function manageListItems(session, args, username, flavor) {
|
|
|
65243
65247
|
` + `${lines.join(`
|
|
65244
65248
|
`)}
|
|
65245
65249
|
|
|
65246
|
-
` + `${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`"}.`)}`);
|
|
65247
65251
|
session.threadLogger?.logCommand(flavor.command, "list", username);
|
|
65248
65252
|
return;
|
|
65249
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
|
+
}
|
|
65250
65273
|
const match = trimmed.match(new RegExp(`^(${flavor.actions})\\s+(\\d+)$`, "i"));
|
|
65251
65274
|
if (!match) {
|
|
65252
|
-
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`)}`);
|
|
65253
65276
|
return;
|
|
65254
65277
|
}
|
|
65255
65278
|
const [, action, indexArg] = match;
|
|
@@ -65301,7 +65324,7 @@ async function manageRoutines(session, args, username, ctx) {
|
|
|
65301
65324
|
describe: (r, formatter) => {
|
|
65302
65325
|
const status = r.enabled ? "" : " β βΈοΈ paused";
|
|
65303
65326
|
const last = r.lastRunAt ? ` Β· last run ${formatIsoMinute(r.lastRunAt)} (${r.lastRunStatus})` : "";
|
|
65304
|
-
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}`;
|
|
65305
65328
|
},
|
|
65306
65329
|
list: () => ctx.state.routinesStore.list(platformId),
|
|
65307
65330
|
update: (id, patch) => ctx.state.routinesStore.update(platformId, id, patch),
|
|
@@ -65387,7 +65410,7 @@ async function manageWatches(session, args, username, ctx) {
|
|
|
65387
65410
|
describe: (w, formatter) => {
|
|
65388
65411
|
const status = w.enabled ? "" : " β βΈοΈ paused";
|
|
65389
65412
|
const last = w.lastFiredAt ? ` Β· last fired ${formatIsoMinute(w.lastFiredAt)} (${w.lastFireStatus})` : "";
|
|
65390
|
-
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}`;
|
|
65391
65414
|
},
|
|
65392
65415
|
list: () => ctx.state.watchesStore.list(platformId),
|
|
65393
65416
|
update: (id, patch) => ctx.state.watchesStore.update(platformId, id, patch),
|
|
@@ -65482,6 +65505,9 @@ import { randomUUID as randomUUID5 } from "crypto";
|
|
|
65482
65505
|
init_logger();
|
|
65483
65506
|
var log24 = createLogger("worktree");
|
|
65484
65507
|
var sessionLog5 = createSessionLog(log24);
|
|
65508
|
+
function displayBranchName(name) {
|
|
65509
|
+
return name.replace(/[`\r\n]/g, "").slice(0, 100);
|
|
65510
|
+
}
|
|
65485
65511
|
function parseWorktreeError(error) {
|
|
65486
65512
|
const message = error instanceof Error ? error.message : String(error);
|
|
65487
65513
|
const lowerMessage = message.toLowerCase();
|
|
@@ -65652,7 +65678,7 @@ async function handleWorktreeBranchResponse(session, branchName, username, respo
|
|
|
65652
65678
|
return false;
|
|
65653
65679
|
}
|
|
65654
65680
|
if (!isValidBranchName(branchName)) {
|
|
65655
|
-
await postError(session, `Invalid branch name: \`${branchName}\`. Please provide a valid git branch name.`);
|
|
65681
|
+
await postError(session, `Invalid branch name: \`${displayBranchName(branchName)}\`. Please provide a valid git branch name.`);
|
|
65656
65682
|
sessionLog5(session).warn(`\uD83C\uDF3F Invalid branch name: ${branchName}`);
|
|
65657
65683
|
return true;
|
|
65658
65684
|
}
|
|
@@ -65696,7 +65722,7 @@ async function createAndSwitchToWorktree(session, branch, username, options) {
|
|
|
65696
65722
|
if (!await requireSessionOwner(session, username, "manage worktrees"))
|
|
65697
65723
|
return;
|
|
65698
65724
|
if (!isValidBranchName(branch)) {
|
|
65699
|
-
await postError(session, `Invalid branch name: \`${branch}\`. Please provide a valid git branch name.`);
|
|
65725
|
+
await postError(session, `Invalid branch name: \`${displayBranchName(branch)}\`. Please provide a valid git branch name.`);
|
|
65700
65726
|
sessionLog5(session).warn(`\uD83C\uDF3F Rejected invalid branch name: ${branch}`);
|
|
65701
65727
|
return;
|
|
65702
65728
|
}
|
|
@@ -67884,7 +67910,10 @@ class SessionRegistry {
|
|
|
67884
67910
|
find(platformId, threadId) {
|
|
67885
67911
|
return this.sessions.get(this.getSessionId(platformId, threadId));
|
|
67886
67912
|
}
|
|
67887
|
-
findByThreadId(threadId) {
|
|
67913
|
+
findByThreadId(threadId, platformId) {
|
|
67914
|
+
if (platformId !== undefined) {
|
|
67915
|
+
return this.find(platformId, threadId);
|
|
67916
|
+
}
|
|
67888
67917
|
for (const session of this.sessions.values()) {
|
|
67889
67918
|
if (session.threadId === threadId) {
|
|
67890
67919
|
return session;
|
|
@@ -75427,7 +75456,7 @@ class SessionManager extends EventEmitter4 {
|
|
|
75427
75456
|
return session !== undefined && session.claude.isRunning();
|
|
75428
75457
|
}
|
|
75429
75458
|
hasPausedSession(threadId, platformId) {
|
|
75430
|
-
if (this.registry.findByThreadId(threadId))
|
|
75459
|
+
if (this.registry.findByThreadId(threadId, platformId))
|
|
75431
75460
|
return false;
|
|
75432
75461
|
return this.registry.getPersistedByThreadId(threadId, platformId) !== undefined;
|
|
75433
75462
|
}
|
|
@@ -75739,7 +75768,7 @@ Mention me to start a session in this worktree.`, threadId);
|
|
|
75739
75768
|
}
|
|
75740
75769
|
}
|
|
75741
75770
|
isUserAllowedInSession(threadId, username, platformId) {
|
|
75742
|
-
const session = this.
|
|
75771
|
+
const session = this.registry.find(platformId, threadId);
|
|
75743
75772
|
if (!session) {
|
|
75744
75773
|
const persisted = this.getPersistedSession(threadId, platformId);
|
|
75745
75774
|
if (persisted) {
|
|
@@ -85464,7 +85493,7 @@ async function handleMessage(client, session, post2, user, options) {
|
|
|
85464
85493
|
await onKill?.(username);
|
|
85465
85494
|
return;
|
|
85466
85495
|
}
|
|
85467
|
-
const activeSession = session.registry.findByThreadId(threadRoot);
|
|
85496
|
+
const activeSession = session.registry.findByThreadId(threadRoot, platformId);
|
|
85468
85497
|
if (activeSession) {
|
|
85469
85498
|
const sideMentionActive = leadingOtherUserMention(client, message);
|
|
85470
85499
|
if (sideMentionActive) {
|
package/dist/mcp/mcp-server.js
CHANGED
|
@@ -51310,6 +51310,7 @@ class MessageApprovalExecutor extends BaseExecutor {
|
|
|
51310
51310
|
const isInviteAuthorized = pending.sessionOwner !== undefined && user === pending.sessionOwner || ctx.platform.isUserAllowed(user);
|
|
51311
51311
|
if (!isInviteAuthorized) {
|
|
51312
51312
|
ctx.logger.info(`Message approval invite (β
) from @${user} downgraded to allow-once: only the session owner may invite`);
|
|
51313
|
+
await ctx.createPost(`βΉοΈ Only the session owner can invite ${ctx.formatter.formatUserMention(pending.fromUser)} to the session β allowing this one message instead.`, { type: "system" });
|
|
51313
51314
|
const handled2 = await this.handleMessageApprovalResponse(postId, "allow", user, ctx);
|
|
51314
51315
|
ctx.logger.debug(`MessageApprovalExecutor: outcome=allow (invite downgraded), handled=${handled2}`);
|
|
51315
51316
|
return handled2;
|
|
@@ -58100,7 +58101,10 @@ class SessionRegistry {
|
|
|
58100
58101
|
find(platformId, threadId) {
|
|
58101
58102
|
return this.sessions.get(this.getSessionId(platformId, threadId));
|
|
58102
58103
|
}
|
|
58103
|
-
findByThreadId(threadId) {
|
|
58104
|
+
findByThreadId(threadId, platformId) {
|
|
58105
|
+
if (platformId !== undefined) {
|
|
58106
|
+
return this.find(platformId, threadId);
|
|
58107
|
+
}
|
|
58104
58108
|
for (const session of this.sessions.values()) {
|
|
58105
58109
|
if (session.threadId === threadId) {
|
|
58106
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