claude-threads 1.31.1 β†’ 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 CHANGED
@@ -5,6 +5,16 @@ 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
+
8
18
  ## [1.31.1] - 2026-08-29
9
19
 
10
20
  ### 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>` to manage) |
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>` to manage) |
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)} or ${formatter.formatCode(`${cmd} ${flavor.actions} <n>`)}`);
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;
@@ -75431,7 +75456,7 @@ class SessionManager extends EventEmitter4 {
75431
75456
  return session !== undefined && session.claude.isRunning();
75432
75457
  }
75433
75458
  hasPausedSession(threadId, platformId) {
75434
- if (this.registry.findByThreadId(threadId))
75459
+ if (this.registry.findByThreadId(threadId, platformId))
75435
75460
  return false;
75436
75461
  return this.registry.getPersistedByThreadId(threadId, platformId) !== undefined;
75437
75462
  }
@@ -75743,7 +75768,7 @@ Mention me to start a session in this worktree.`, threadId);
75743
75768
  }
75744
75769
  }
75745
75770
  isUserAllowedInSession(threadId, username, platformId) {
75746
- const session = this.findSessionByThreadId(threadId);
75771
+ const session = this.registry.find(platformId, threadId);
75747
75772
  if (!session) {
75748
75773
  const persisted = this.getPersistedSession(threadId, platformId);
75749
75774
  if (persisted) {
@@ -85468,7 +85493,7 @@ async function handleMessage(client, session, post2, user, options) {
85468
85493
  await onKill?.(username);
85469
85494
  return;
85470
85495
  }
85471
- const activeSession = session.registry.findByThreadId(threadRoot);
85496
+ const activeSession = session.registry.findByThreadId(threadRoot, platformId);
85472
85497
  if (activeSession) {
85473
85498
  const sideMentionActive = leadingOtherUserMention(client, message);
85474
85499
  if (sideMentionActive) {
@@ -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;
@@ -382,8 +382,12 @@ confirmation says so.
382
382
 
383
383
  **Managing:**
384
384
 
385
- - `!routines` β€” numbered list with schedule, creator, and last-run status
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, and last-fire status
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
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "claude-threads",
3
- "version": "1.31.1",
3
+ "version": "1.31.2",
4
4
  "description": "Run Claude Code from Slack or Mattermost. Sessions stream live into threads where your whole team can watch and steer.",
5
5
  "main": "dist/index.js",
6
6
  "type": "module",