claude-threads 1.25.0 → 1.26.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.
@@ -51087,7 +51087,8 @@ class PromptExecutor extends BaseExecutor {
51087
51087
  return {
51088
51088
  pendingContextPrompt: null,
51089
51089
  pendingExistingWorktreePrompt: null,
51090
- pendingUpdatePrompt: null
51090
+ pendingUpdatePrompt: null,
51091
+ pendingRoutinePrompt: null
51091
51092
  };
51092
51093
  }
51093
51094
  getInitialState() {
@@ -51097,14 +51098,16 @@ class PromptExecutor extends BaseExecutor {
51097
51098
  return {
51098
51099
  pendingContextPrompt: this.state.pendingContextPrompt ? { ...this.state.pendingContextPrompt } : null,
51099
51100
  pendingExistingWorktreePrompt: this.state.pendingExistingWorktreePrompt ? { ...this.state.pendingExistingWorktreePrompt } : null,
51100
- pendingUpdatePrompt: this.state.pendingUpdatePrompt ? { ...this.state.pendingUpdatePrompt } : null
51101
+ pendingUpdatePrompt: this.state.pendingUpdatePrompt ? { ...this.state.pendingUpdatePrompt } : null,
51102
+ pendingRoutinePrompt: this.state.pendingRoutinePrompt ? { ...this.state.pendingRoutinePrompt } : null
51101
51103
  };
51102
51104
  }
51103
51105
  hydrateState(persisted) {
51104
51106
  this.state = {
51105
51107
  pendingContextPrompt: persisted.pendingContextPrompt ?? null,
51106
51108
  pendingExistingWorktreePrompt: persisted.pendingExistingWorktreePrompt ?? null,
51107
- pendingUpdatePrompt: persisted.pendingUpdatePrompt ?? null
51109
+ pendingUpdatePrompt: persisted.pendingUpdatePrompt ?? null,
51110
+ pendingRoutinePrompt: null
51108
51111
  };
51109
51112
  }
51110
51113
  setPendingContextPrompt(prompt) {
@@ -51234,6 +51237,30 @@ class PromptExecutor extends BaseExecutor {
51234
51237
  }
51235
51238
  return true;
51236
51239
  }
51240
+ setPendingRoutinePrompt(prompt) {
51241
+ this.state.pendingRoutinePrompt = prompt;
51242
+ }
51243
+ hasPendingRoutinePrompt() {
51244
+ return this.state.pendingRoutinePrompt !== null;
51245
+ }
51246
+ async handleRoutinePromptResponse(postId, approved, username, ctx) {
51247
+ if (!this.state.pendingRoutinePrompt)
51248
+ return false;
51249
+ if (this.state.pendingRoutinePrompt.postId !== postId)
51250
+ return false;
51251
+ const { parsed, requestedBy } = this.state.pendingRoutinePrompt;
51252
+ const statusMessage = approved ? `✅ ${ctx.formatter.formatBold(`Routine "${parsed.name}" confirmed`)} by ${ctx.formatter.formatUserMention(username)} — saving...` : `❌ ${ctx.formatter.formatBold(`Routine "${parsed.name}" discarded`)} by ${ctx.formatter.formatUserMention(username)}`;
51253
+ try {
51254
+ await ctx.platform.updatePost(postId, statusMessage);
51255
+ } catch (err) {
51256
+ ctx.logger.debug(`Failed to update routine prompt post: ${err}`);
51257
+ }
51258
+ this.state.pendingRoutinePrompt = null;
51259
+ if (this.events) {
51260
+ this.events.emit("routine-prompt:complete", { approved, parsed, requestedBy, postId });
51261
+ }
51262
+ return true;
51263
+ }
51237
51264
  async handleReaction(postId, emoji4, user, action, ctx) {
51238
51265
  ctx.logger.debug(`PromptExecutor.handleReaction: postId=${postId.substring(0, 8)}, emoji=${emoji4}, user=${user}, action=${action}`);
51239
51266
  if (action !== "added") {
@@ -51294,6 +51321,18 @@ class PromptExecutor extends BaseExecutor {
51294
51321
  ctx.logger.debug(`PromptExecutor: emoji ${emoji4} not valid for update prompt, ignoring`);
51295
51322
  return false;
51296
51323
  }
51324
+ if (this.state.pendingRoutinePrompt?.postId === postId) {
51325
+ if (isApprovalEmoji(emoji4)) {
51326
+ ctx.logger.debug(`Routine prompt reaction from @${user}: approve`);
51327
+ return this.handleRoutinePromptResponse(postId, true, user, ctx);
51328
+ }
51329
+ if (isDenialEmoji(emoji4)) {
51330
+ ctx.logger.debug(`Routine prompt reaction from @${user}: discard`);
51331
+ return this.handleRoutinePromptResponse(postId, false, user, ctx);
51332
+ }
51333
+ ctx.logger.debug(`PromptExecutor: emoji ${emoji4} not valid for routine prompt, ignoring`);
51334
+ return false;
51335
+ }
51297
51336
  ctx.logger.debug(`PromptExecutor: no pending prompt state matches postId=${postId.substring(0, 8)}`);
51298
51337
  return false;
51299
51338
  }
@@ -51910,6 +51949,9 @@ class MessageManager {
51910
51949
  clearPendingUpdatePrompt() {
51911
51950
  this.promptExecutor.clearPendingUpdatePrompt();
51912
51951
  }
51952
+ setPendingRoutinePrompt(prompt) {
51953
+ this.promptExecutor.setPendingRoutinePrompt(prompt);
51954
+ }
51913
51955
  setPendingBugReport(report) {
51914
51956
  this.bugReportExecutor.setPendingBugReport(report);
51915
51957
  }
@@ -52184,6 +52226,9 @@ import { resolve as resolve2, dirname as dirname2 } from "path";
52184
52226
  import { homedir } from "os";
52185
52227
 
52186
52228
  // node_modules/js-yaml/dist/js-yaml.mjs
52229
+ function getDefaultExportFromCjs(x) {
52230
+ return x && x.__esModule && Object.prototype.hasOwnProperty.call(x, "default") ? x["default"] : x;
52231
+ }
52187
52232
  var jsYaml = {};
52188
52233
  var loader = {};
52189
52234
  var common = {};
@@ -55287,6 +55332,7 @@ function requireJsYaml() {
55287
55332
  return jsYaml;
55288
55333
  }
55289
55334
  var jsYamlExports = requireJsYaml();
55335
+ var yaml = /* @__PURE__ */ getDefaultExportFromCjs(jsYamlExports);
55290
55336
 
55291
55337
  // src/config/index.ts
55292
55338
  var CONFIG_PATH = resolve2(homedir(), ".config", "claude-threads", "config.yaml");
@@ -55395,7 +55441,48 @@ function formatReleaseNotes(notes, formatter) {
55395
55441
  // src/utils/keep-alive.ts
55396
55442
  import { spawn } from "child_process";
55397
55443
  var log5 = createLogger("keepalive");
55398
-
55444
+ function keepAliveSpawnSpec(platform, parentPid) {
55445
+ switch (platform) {
55446
+ case "darwin":
55447
+ return {
55448
+ command: "caffeinate",
55449
+ args: ["-s", "-i", "-w", String(parentPid)],
55450
+ stdio: "ignore"
55451
+ };
55452
+ case "linux":
55453
+ return {
55454
+ command: "systemd-inhibit",
55455
+ args: [
55456
+ "--what=sleep:idle:handle-lid-switch",
55457
+ "--why=Claude Code session active",
55458
+ "--mode=block",
55459
+ "cat"
55460
+ ],
55461
+ stdio: ["pipe", "ignore", "ignore"]
55462
+ };
55463
+ default:
55464
+ return null;
55465
+ }
55466
+ }
55467
+ function linuxFallbackScript(parentPid) {
55468
+ return `while kill -0 ${parentPid} 2>/dev/null; do xdg-screensaver reset 2>/dev/null || true; sleep 60; done`;
55469
+ }
55470
+ function windowsScript(parentPid) {
55471
+ return `
55472
+ Add-Type -TypeDefinition @"
55473
+ using System;
55474
+ using System.Runtime.InteropServices;
55475
+ public class PowerState {
55476
+ [DllImport("kernel32.dll", CharSet = CharSet.Auto, SetLastError = true)]
55477
+ public static extern uint SetThreadExecutionState(uint esFlags);
55478
+ }
55479
+ "@
55480
+ # ES_CONTINUOUS | ES_SYSTEM_REQUIRED
55481
+ [PowerState]::SetThreadExecutionState(0x80000001) | Out-Null
55482
+ # Keep running until killed or the parent process exits
55483
+ while (Get-Process -Id ${parentPid} -ErrorAction SilentlyContinue) { Start-Sleep -Seconds 60 }
55484
+ `;
55485
+ }
55399
55486
  class KeepAliveManager {
55400
55487
  activeSessionCount = 0;
55401
55488
  keepAliveProcess = null;
@@ -55472,8 +55559,11 @@ class KeepAliveManager {
55472
55559
  }
55473
55560
  startMacOSKeepAlive() {
55474
55561
  try {
55475
- this.keepAliveProcess = spawn("caffeinate", ["-s", "-i"], {
55476
- stdio: "ignore",
55562
+ const spec = keepAliveSpawnSpec("darwin", process.pid);
55563
+ if (!spec)
55564
+ return;
55565
+ this.keepAliveProcess = spawn(spec.command, spec.args, {
55566
+ stdio: spec.stdio,
55477
55567
  detached: false
55478
55568
  });
55479
55569
  this.keepAliveProcess.on("error", (err) => {
@@ -55493,14 +55583,11 @@ class KeepAliveManager {
55493
55583
  }
55494
55584
  startLinuxKeepAlive() {
55495
55585
  try {
55496
- this.keepAliveProcess = spawn("systemd-inhibit", [
55497
- "--what=sleep:idle:handle-lid-switch",
55498
- "--why=Claude Code session active",
55499
- "--mode=block",
55500
- "sleep",
55501
- "infinity"
55502
- ], {
55503
- stdio: "ignore",
55586
+ const spec = keepAliveSpawnSpec("linux", process.pid);
55587
+ if (!spec)
55588
+ return;
55589
+ this.keepAliveProcess = spawn(spec.command, spec.args, {
55590
+ stdio: spec.stdio,
55504
55591
  detached: false
55505
55592
  });
55506
55593
  this.keepAliveProcess.on("error", (err) => {
@@ -55522,10 +55609,7 @@ class KeepAliveManager {
55522
55609
  }
55523
55610
  startLinuxKeepAliveFallback() {
55524
55611
  try {
55525
- this.keepAliveProcess = spawn("bash", [
55526
- "-c",
55527
- `while true; do xdg-screensaver reset 2>/dev/null || true; sleep 60; done`
55528
- ], {
55612
+ this.keepAliveProcess = spawn("bash", ["-c", linuxFallbackScript(process.pid)], {
55529
55613
  stdio: "ignore",
55530
55614
  detached: false
55531
55615
  });
@@ -55543,20 +55627,7 @@ class KeepAliveManager {
55543
55627
  }
55544
55628
  startWindowsKeepAlive() {
55545
55629
  try {
55546
- const script = `
55547
- Add-Type -TypeDefinition @"
55548
- using System;
55549
- using System.Runtime.InteropServices;
55550
- public class PowerState {
55551
- [DllImport("kernel32.dll", CharSet = CharSet.Auto, SetLastError = true)]
55552
- public static extern uint SetThreadExecutionState(uint esFlags);
55553
- }
55554
- "@
55555
- # ES_CONTINUOUS | ES_SYSTEM_REQUIRED
55556
- [PowerState]::SetThreadExecutionState(0x80000001) | Out-Null
55557
- # Keep running until killed
55558
- while ($true) { Start-Sleep -Seconds 60 }
55559
- `;
55630
+ const script = windowsScript(process.pid);
55560
55631
  this.keepAliveProcess = spawn("powershell", ["-NoProfile", "-Command", script], {
55561
55632
  stdio: "ignore",
55562
55633
  detached: false,
@@ -56759,6 +56830,28 @@ var COMMAND_REGISTRY = [
56759
56830
  { name: "forget", description: "Remove one entry (by number or matching text), or all", args: "<n|text> | all" }
56760
56831
  ]
56761
56832
  },
56833
+ {
56834
+ command: "routine",
56835
+ description: "Create a scheduled routine from a natural-language request (confirmed with \uD83D\uDC4D before saving)",
56836
+ args: "<schedule, task>",
56837
+ category: "settings",
56838
+ audience: "user",
56839
+ claudeNotes: "User decisions, not yours"
56840
+ },
56841
+ {
56842
+ command: "routines",
56843
+ description: "List scheduled routines; pause/resume/delete/run manage them",
56844
+ args: "[pause|resume|delete|run <n>]",
56845
+ category: "settings",
56846
+ audience: "user",
56847
+ claudeNotes: "User decisions, not yours",
56848
+ subcommands: [
56849
+ { name: "pause", description: "Pause a routine", args: "<n>" },
56850
+ { name: "resume", description: "Resume a paused routine", args: "<n>" },
56851
+ { name: "delete", description: "Delete a routine", args: "<n>" },
56852
+ { name: "run", description: "Run a routine now, outside its schedule", args: "<n>" }
56853
+ ]
56854
+ },
56762
56855
  {
56763
56856
  command: "update",
56764
56857
  description: "Show auto-update status",
@@ -57033,6 +57126,30 @@ var handleMemory = async (ctx, args) => {
57033
57126
  await ctx.client.createPost(`⚠️ Usage: ${ctx.formatter.formatCode("!memory")} or ${ctx.formatter.formatCode("!memory forget <n|text>")} or ${ctx.formatter.formatCode("!memory forget all")}`, ctx.threadId);
57034
57127
  return { handled: true };
57035
57128
  };
57129
+ var handleRoutine = async (ctx, args) => {
57130
+ if (ctx.commandContext === "first-message") {
57131
+ return { handled: false };
57132
+ }
57133
+ if (!ctx.isAllowed) {
57134
+ return { handled: true };
57135
+ }
57136
+ if (!args?.trim()) {
57137
+ await ctx.client.createPost(`⚠️ Usage: ${ctx.formatter.formatCode("!routine every weekday at 9:00, <task>")}`, ctx.threadId);
57138
+ return { handled: true };
57139
+ }
57140
+ await ctx.sessionManager.createRoutine(ctx.threadId, args, ctx.username);
57141
+ return { handled: true };
57142
+ };
57143
+ var handleRoutines = async (ctx, args) => {
57144
+ if (ctx.commandContext === "first-message") {
57145
+ return { handled: false };
57146
+ }
57147
+ if (!ctx.isAllowed) {
57148
+ return { handled: true };
57149
+ }
57150
+ await ctx.sessionManager.manageRoutines(ctx.threadId, args, ctx.username);
57151
+ return { handled: true };
57152
+ };
57036
57153
  var handleCd = async (ctx, args) => {
57037
57154
  if (!args) {
57038
57155
  return { handled: false };
@@ -57224,6 +57341,8 @@ handlers.set("kick", handleKick);
57224
57341
  handlers.set("github-email", handleGitHubEmail);
57225
57342
  handlers.set("remember", handleRemember);
57226
57343
  handlers.set("memory", handleMemory);
57344
+ handlers.set("routine", handleRoutine);
57345
+ handlers.set("routines", handleRoutines);
57227
57346
  handlers.set("cd", handleCd);
57228
57347
  handlers.set("permissions", handlePermissions);
57229
57348
  handlers.set("mentions", handleMentions);
@@ -57627,23 +57746,200 @@ var log20 = createLogger("gh-emails");
57627
57746
  var DEFAULT_CONFIG_DIR = join7(homedir5(), ".config", "claude-threads");
57628
57747
  var DEFAULT_FILE = join7(DEFAULT_CONFIG_DIR, "github-emails.yaml");
57629
57748
 
57749
+ // src/persistence/routines-store.ts
57750
+ import { existsSync as existsSync6, mkdirSync as mkdirSync2, readFileSync as readFileSync5 } from "fs";
57751
+ import { homedir as homedir6 } from "os";
57752
+ import { join as join8 } from "path";
57753
+ import { randomUUID as randomUUID2 } from "crypto";
57754
+
57755
+ // src/persistence/atomic-file.ts
57756
+ import { chmodSync as chmodSync2, renameSync as renameSync2, writeFileSync as writeFileSync3 } from "fs";
57757
+
57758
+ class SerialQueue {
57759
+ tail = Promise.resolve();
57760
+ run(fn) {
57761
+ const next = this.tail.then(fn, fn);
57762
+ this.tail = next.catch(() => {
57763
+ return;
57764
+ });
57765
+ return next;
57766
+ }
57767
+ }
57768
+ function writeFileAtomic(file2, content) {
57769
+ const tempFile = `${file2}.tmp`;
57770
+ writeFileSync3(tempFile, content, { encoding: "utf-8", mode: 384 });
57771
+ renameSync2(tempFile, file2);
57772
+ chmodSync2(file2, 384);
57773
+ }
57774
+
57775
+ // src/persistence/routines-store.ts
57776
+ var log21 = createLogger("routines");
57777
+ var DEFAULT_CONFIG_DIR2 = join8(homedir6(), ".config", "claude-threads");
57778
+ var DEFAULT_FILE2 = join8(DEFAULT_CONFIG_DIR2, "routines.yaml");
57779
+ var STORE_VERSION = 1;
57780
+ var DEFAULT_MAX_ROUTINES = 10;
57781
+ var SCHEDULE_PRESETS = ["hourly", "daily", "weekdays", "weekly"];
57782
+ var TIME_RE = /^([01]\d|2[0-3]):[0-5]\d$/;
57783
+ function isValidTimezone(tz) {
57784
+ if (typeof tz !== "string" || !tz)
57785
+ return false;
57786
+ try {
57787
+ new Intl.DateTimeFormat("en-US", { timeZone: tz });
57788
+ return true;
57789
+ } catch {
57790
+ return false;
57791
+ }
57792
+ }
57793
+ function validateSchedule(schedule) {
57794
+ if (!SCHEDULE_PRESETS.includes(schedule.preset)) {
57795
+ return `unknown preset "${String(schedule.preset)}" (expected ${SCHEDULE_PRESETS.join("/")})`;
57796
+ }
57797
+ if (!isValidTimezone(schedule.timezone)) {
57798
+ return `invalid timezone "${String(schedule.timezone)}"`;
57799
+ }
57800
+ if (schedule.preset === "hourly") {
57801
+ return null;
57802
+ }
57803
+ if (!schedule.time || !TIME_RE.test(schedule.time)) {
57804
+ return `invalid time "${String(schedule.time)}" (expected HH:MM, 24h)`;
57805
+ }
57806
+ if (schedule.preset === "weekly") {
57807
+ const weekday = schedule.weekday;
57808
+ if (typeof weekday !== "number" || !Number.isInteger(weekday) || weekday < 1 || weekday > 7) {
57809
+ return `invalid weekday "${String(weekday)}" (expected 1=Mon … 7=Sun)`;
57810
+ }
57811
+ }
57812
+ return null;
57813
+ }
57814
+ class RoutinesStore {
57815
+ file;
57816
+ configDir;
57817
+ queue = new SerialQueue;
57818
+ constructor(filePath) {
57819
+ const effective = filePath ?? process.env.CLAUDE_THREADS_ROUTINES_PATH;
57820
+ if (effective) {
57821
+ this.file = effective;
57822
+ this.configDir = join8(effective, "..");
57823
+ } else {
57824
+ this.file = DEFAULT_FILE2;
57825
+ this.configDir = DEFAULT_CONFIG_DIR2;
57826
+ }
57827
+ if (!existsSync6(this.configDir)) {
57828
+ mkdirSync2(this.configDir, { recursive: true, mode: 448 });
57829
+ }
57830
+ }
57831
+ list(platformId) {
57832
+ return this.loadRaw().routines[platformId] ?? [];
57833
+ }
57834
+ get(platformId, id) {
57835
+ return this.list(platformId).find((r) => r.id === id);
57836
+ }
57837
+ add(platformId, routine, maxRoutines = DEFAULT_MAX_ROUTINES) {
57838
+ return this.runExclusive(() => {
57839
+ const scheduleError = validateSchedule(routine.schedule);
57840
+ if (scheduleError)
57841
+ return { ok: false, error: scheduleError };
57842
+ const name = routine.name.trim().slice(0, 80);
57843
+ const prompt = routine.prompt.trim().slice(0, 2000);
57844
+ if (!name || !prompt)
57845
+ return { ok: false, error: "name and prompt are required" };
57846
+ const data = this.loadRaw();
57847
+ const existing = data.routines[platformId] ?? [];
57848
+ if (existing.length >= maxRoutines) {
57849
+ return { ok: false, error: `routine limit reached (${maxRoutines}); delete one first` };
57850
+ }
57851
+ const full = {
57852
+ ...routine,
57853
+ name,
57854
+ prompt,
57855
+ id: randomUUID2().slice(0, 8),
57856
+ createdAt: new Date().toISOString(),
57857
+ enabled: true,
57858
+ consecutiveFailures: 0
57859
+ };
57860
+ data.routines[platformId] = [...existing, full];
57861
+ this.writeAtomic(data);
57862
+ log21.info(`Routine "${full.name}" created on ${platformId} by @${full.createdBy}`);
57863
+ return { ok: true, routine: full };
57864
+ });
57865
+ }
57866
+ update(platformId, id, patch) {
57867
+ return this.runExclusive(() => {
57868
+ const data = this.loadRaw();
57869
+ const routines = data.routines[platformId] ?? [];
57870
+ const idx = routines.findIndex((r) => r.id === id);
57871
+ if (idx < 0)
57872
+ return;
57873
+ routines[idx] = { ...routines[idx], ...patch };
57874
+ this.writeAtomic(data);
57875
+ return routines[idx];
57876
+ });
57877
+ }
57878
+ remove(platformId, id) {
57879
+ return this.runExclusive(() => {
57880
+ const data = this.loadRaw();
57881
+ const routines = data.routines[platformId] ?? [];
57882
+ const idx = routines.findIndex((r) => r.id === id);
57883
+ if (idx < 0)
57884
+ return;
57885
+ const [removed] = routines.splice(idx, 1);
57886
+ if (routines.length === 0)
57887
+ delete data.routines[platformId];
57888
+ this.writeAtomic(data);
57889
+ log21.info(`Routine "${removed.name}" removed from ${platformId}`);
57890
+ return removed;
57891
+ });
57892
+ }
57893
+ runExclusive(fn) {
57894
+ return this.queue.run(fn);
57895
+ }
57896
+ loadRaw() {
57897
+ if (!existsSync6(this.file)) {
57898
+ return { version: STORE_VERSION, routines: {} };
57899
+ }
57900
+ try {
57901
+ const parsed = yaml.load(readFileSync5(this.file, "utf-8"));
57902
+ if (!parsed || typeof parsed !== "object") {
57903
+ return { version: STORE_VERSION, routines: {} };
57904
+ }
57905
+ const routines = parsed.routines && typeof parsed.routines === "object" ? parsed.routines : {};
57906
+ for (const list of Object.values(routines)) {
57907
+ for (const r of list) {
57908
+ r.enabled = r.enabled ?? true;
57909
+ r.consecutiveFailures = r.consecutiveFailures ?? 0;
57910
+ }
57911
+ }
57912
+ return { version: parsed.version ?? STORE_VERSION, routines };
57913
+ } catch (err) {
57914
+ log21.warn(`Failed to read ${this.file}: ${err.message} — starting empty`);
57915
+ return { version: STORE_VERSION, routines: {} };
57916
+ }
57917
+ }
57918
+ writeAtomic(data) {
57919
+ writeFileAtomic(this.file, yaml.dump(data, { sortKeys: true, lineWidth: -1 }));
57920
+ }
57921
+ }
57922
+
57923
+ // src/routines/parser.ts
57924
+ var log22 = createLogger("routines");
57925
+
57630
57926
  // src/operations/commands/handler.ts
57631
- var log21 = createLogger("commands");
57632
- var sessionLog4 = createSessionLog(log21);
57927
+ var log23 = createLogger("commands");
57928
+ var sessionLog4 = createSessionLog(log23);
57633
57929
  // src/operations/suggestions/branch.ts
57634
57930
  import { exec as exec2 } from "child_process";
57635
57931
  import { promisify as promisify2 } from "util";
57636
57932
  var execAsync2 = promisify2(exec2);
57637
- var log22 = createLogger("branch");
57933
+ var log24 = createLogger("branch");
57638
57934
 
57639
57935
  // src/operations/worktree/handler.ts
57640
- var log23 = createLogger("worktree");
57641
- var sessionLog5 = createSessionLog(log23);
57936
+ var log25 = createLogger("worktree");
57937
+ var sessionLog5 = createSessionLog(log25);
57642
57938
  // src/operations/events/handler.ts
57643
- var log24 = createLogger("events");
57644
- var sessionLog6 = createSessionLog(log24);
57939
+ var log26 = createLogger("events");
57940
+ var sessionLog6 = createSessionLog(log26);
57645
57941
  // src/operations/monitor/handler.ts
57646
- var log25 = createLogger("monitor");
57942
+ var log27 = createLogger("monitor");
57647
57943
  var DEFAULT_INTERVAL_MS = 60 * 1000;
57648
57944
  // src/utils/websocket.ts
57649
57945
  var WS;
@@ -57725,7 +58021,7 @@ ${code}
57725
58021
 
57726
58022
  // src/platform/mattermost/upload.ts
57727
58023
  import { readFile } from "fs/promises";
57728
- var log26 = createLogger("mm-upload");
58024
+ var log28 = createLogger("mm-upload");
57729
58025
  async function uploadFileMattermost(args) {
57730
58026
  const { url: url2, token, channelId, threadId, filePath, filename, caption } = args;
57731
58027
  const buffer = await readFile(filePath);
@@ -57733,7 +58029,7 @@ async function uploadFileMattermost(args) {
57733
58029
  const arrayBuffer = buffer.buffer.slice(buffer.byteOffset, buffer.byteOffset + buffer.byteLength);
57734
58030
  const formData = new FormData;
57735
58031
  formData.append("files", new Blob([arrayBuffer]), filename);
57736
- log26.debug(`POST /files (${buffer.length} bytes, ${filename})`);
58032
+ log28.debug(`POST /files (${buffer.length} bytes, ${filename})`);
57737
58033
  const uploadResponse = await fetch(uploadUrl, {
57738
58034
  method: "POST",
57739
58035
  headers: {
@@ -57757,7 +58053,7 @@ async function uploadFileMattermost(args) {
57757
58053
  root_id: threadId,
57758
58054
  file_ids: [fileInfo.id]
57759
58055
  };
57760
- log26.debug(`POST /posts (file_ids=[${fileInfo.id}])`);
58056
+ log28.debug(`POST /posts (file_ids=[${fileInfo.id}])`);
57761
58057
  const postResponse = await fetch(postUrl, {
57762
58058
  method: "POST",
57763
58059
  headers: {
@@ -58251,7 +58547,7 @@ ${code}
58251
58547
 
58252
58548
  // src/platform/slack/upload.ts
58253
58549
  import { readFile as readFile2 } from "fs/promises";
58254
- var log27 = createLogger("slack-upload");
58550
+ var log29 = createLogger("slack-upload");
58255
58551
  var DEFAULT_API_URL = "https://slack.com/api";
58256
58552
  async function uploadFileSlack(args) {
58257
58553
  const { botToken, channelId, threadTs, filePath, filename, caption } = args;
@@ -58259,7 +58555,7 @@ async function uploadFileSlack(args) {
58259
58555
  const buffer = await readFile2(filePath);
58260
58556
  const params = new URLSearchParams({ filename, length: String(buffer.length) });
58261
58557
  const step1Url = `${apiUrl}/files.getUploadURLExternal?${params.toString()}`;
58262
- log27.debug(`GET files.getUploadURLExternal (${buffer.length} bytes, ${filename})`);
58558
+ log29.debug(`GET files.getUploadURLExternal (${buffer.length} bytes, ${filename})`);
58263
58559
  const step1Response = await fetch(step1Url, {
58264
58560
  method: "GET",
58265
58561
  headers: {
@@ -58277,7 +58573,7 @@ async function uploadFileSlack(args) {
58277
58573
  const uploadUrl = step1Data.upload_url;
58278
58574
  const fileId = step1Data.file_id;
58279
58575
  const arrayBuffer = buffer.buffer.slice(buffer.byteOffset, buffer.byteOffset + buffer.byteLength);
58280
- log27.debug(`POST <upload_url>`);
58576
+ log29.debug(`POST <upload_url>`);
58281
58577
  const step2Response = await fetch(uploadUrl, {
58282
58578
  method: "POST",
58283
58579
  headers: {
@@ -58297,7 +58593,7 @@ async function uploadFileSlack(args) {
58297
58593
  if (caption !== undefined) {
58298
58594
  step3Body.initial_comment = caption;
58299
58595
  }
58300
- log27.debug(`POST files.completeUploadExternal (file_id=${fileId}, thread_ts=${threadTs})`);
58596
+ log29.debug(`POST files.completeUploadExternal (file_id=${fileId}, thread_ts=${threadTs})`);
58301
58597
  const step3Response = await fetch(`${apiUrl}/files.completeUploadExternal`, {
58302
58598
  method: "POST",
58303
58599
  headers: {
@@ -58315,7 +58611,7 @@ async function uploadFileSlack(args) {
58315
58611
  throw new Error(`Slack completeUploadExternal error: ${step3Data.error || "unknown"}`);
58316
58612
  }
58317
58613
  if (!step3Data.ts) {
58318
- log27.warn(`Slack completeUploadExternal returned no ts; using fileId ${fileId} as postId. ` + `Do not use this id for updatePost/addReaction.`);
58614
+ log29.warn(`Slack completeUploadExternal returned no ts; using fileId ${fileId} as postId. ` + `Do not use this id for updatePost/addReaction.`);
58319
58615
  }
58320
58616
  return { fileId, postId: step3Data.ts ?? fileId };
58321
58617
  }
@@ -245,6 +245,65 @@ platforms:
245
245
  `CLAUDE_CODE_REMOTE` is set (unless `CLAUDE_CODE_REMOTE_MEMORY_DIR` is
246
246
  configured) — the repo layer will be inert in such environments.
247
247
 
248
+ ### Routines (`routines`, default: enabled)
249
+
250
+ Scheduled recurring work, Claude Tag-style: a routine fires on its schedule
251
+ as a **bot-initiated session thread** in the channel — a completely normal
252
+ session (platform permission mode, account-pool balancing, channel memory,
253
+ distillation) whose task is the routine's prompt.
254
+
255
+ ```yaml
256
+ platforms:
257
+ - id: mattermost-main
258
+ type: mattermost
259
+ # ... credentials ...
260
+ routines: true # default; `false` disables the scheduler + commands
261
+
262
+ limits:
263
+ maxRoutines: 10 # per-platform cap (default 10)
264
+ ```
265
+
266
+ **Creating** (natural language, confirmed before saving):
267
+
268
+ ```
269
+ !routine every weekday at 9am, summarize the open review threads
270
+ ```
271
+
272
+ A haiku pass parses the request into a structured schedule (presets: hourly /
273
+ daily / weekdays / weekly — hourly is the floor), the bot posts the parsed
274
+ result, and **nothing is saved until someone reacts 👍**. Timezones: name one
275
+ explicitly ("9am Pacific"); otherwise the bot host's timezone is used and the
276
+ confirmation says so.
277
+
278
+ **Managing:**
279
+
280
+ - `!routines` — numbered list with schedule, creator, and last-run status
281
+ - `!routines pause|resume|delete <n>` — owner-gated
282
+ - `!routines run <n>` — fire now, outside the schedule (platform-allowed
283
+ users only — not temporarily `!invite`d guests; does not consume the
284
+ period's scheduled fire)
285
+
286
+ **Semantics & guardrails:**
287
+
288
+ - Runs fire **as their creator** and are re-authorized on every fire — a
289
+ creator who loses platform authorization disables the routine (with a
290
+ channel notice), mirroring Claude Tag.
291
+ - At most one fire per period (hour/day/week), evaluated on the wall clock in
292
+ the routine's timezone (DST-safe). A window missed entirely (bot offline)
293
+ is skipped, not back-filled.
294
+ - 3 consecutive failed runs auto-disable the routine with a channel notice;
295
+ `!routines resume <n>` re-arms it.
296
+ - Runs count against `MAX_SESSIONS`; at the limit a fire is retried within
297
+ its window and otherwise skipped.
298
+ - **Each run starts a full Claude session on your subscription** — the
299
+ confirmation and `!routines` listing both say so.
300
+ - Routines are scoped per platform instance (same privacy boundary as
301
+ memory) and stored at `~/.config/claude-threads/routines.yaml` (0600;
302
+ override with `CLAUDE_THREADS_ROUTINES_PATH`).
303
+ - The natural-language parse uses one haiku `claude -p` call — the same
304
+ bot-process-credentials caveat as memory distillation applies in OAuth
305
+ account pools.
306
+
248
307
  ## Claude Accounts (optional, multi-account mode)
249
308
 
250
309
  By default every session spawns `claude` with the bot's own `process.env`, so they all share one subscription's token budget. Add a `claudeAccounts` block to spread load across multiple accounts. Omit the block entirely to stay in single-account mode (unchanged behavior).
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "claude-threads",
3
- "version": "1.25.0",
3
+ "version": "1.26.0",
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",