claude-threads 1.28.0 → 1.29.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.
@@ -51265,7 +51265,8 @@ class PromptExecutor extends BaseExecutor {
51265
51265
  pendingContextPrompt: null,
51266
51266
  pendingExistingWorktreePrompt: null,
51267
51267
  pendingUpdatePrompt: null,
51268
- pendingRoutinePrompt: null
51268
+ pendingRoutinePrompt: null,
51269
+ pendingWatchPrompt: null
51269
51270
  };
51270
51271
  }
51271
51272
  getInitialState() {
@@ -51276,7 +51277,8 @@ class PromptExecutor extends BaseExecutor {
51276
51277
  pendingContextPrompt: this.state.pendingContextPrompt ? { ...this.state.pendingContextPrompt } : null,
51277
51278
  pendingExistingWorktreePrompt: this.state.pendingExistingWorktreePrompt ? { ...this.state.pendingExistingWorktreePrompt } : null,
51278
51279
  pendingUpdatePrompt: this.state.pendingUpdatePrompt ? { ...this.state.pendingUpdatePrompt } : null,
51279
- pendingRoutinePrompt: this.state.pendingRoutinePrompt ? { ...this.state.pendingRoutinePrompt } : null
51280
+ pendingRoutinePrompt: this.state.pendingRoutinePrompt ? { ...this.state.pendingRoutinePrompt } : null,
51281
+ pendingWatchPrompt: this.state.pendingWatchPrompt ? { ...this.state.pendingWatchPrompt } : null
51280
51282
  };
51281
51283
  }
51282
51284
  hydrateState(persisted) {
@@ -51284,7 +51286,8 @@ class PromptExecutor extends BaseExecutor {
51284
51286
  pendingContextPrompt: persisted.pendingContextPrompt ?? null,
51285
51287
  pendingExistingWorktreePrompt: persisted.pendingExistingWorktreePrompt ?? null,
51286
51288
  pendingUpdatePrompt: persisted.pendingUpdatePrompt ?? null,
51287
- pendingRoutinePrompt: null
51289
+ pendingRoutinePrompt: null,
51290
+ pendingWatchPrompt: null
51288
51291
  };
51289
51292
  }
51290
51293
  setPendingContextPrompt(prompt) {
@@ -51420,24 +51423,36 @@ class PromptExecutor extends BaseExecutor {
51420
51423
  hasPendingRoutinePrompt() {
51421
51424
  return this.state.pendingRoutinePrompt !== null;
51422
51425
  }
51423
- async handleRoutinePromptResponse(postId, approved, username, ctx) {
51424
- if (!this.state.pendingRoutinePrompt)
51426
+ async completeCreationPrompt(pending, label, clear, emit, postId, approved, username, ctx) {
51427
+ if (!pending || pending.postId !== postId)
51425
51428
  return false;
51426
- if (this.state.pendingRoutinePrompt.postId !== postId)
51427
- return false;
51428
- const { parsed, requestedBy } = this.state.pendingRoutinePrompt;
51429
- 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)}`;
51429
+ const { parsed, requestedBy } = pending;
51430
+ const statusMessage = approved ? `✅ ${ctx.formatter.formatBold(`${label} "${parsed.name}" confirmed`)} by ${ctx.formatter.formatUserMention(username)} — saving...` : `❌ ${ctx.formatter.formatBold(`${label} "${parsed.name}" discarded`)} by ${ctx.formatter.formatUserMention(username)}`;
51430
51431
  try {
51431
51432
  await ctx.platform.updatePost(postId, statusMessage);
51432
51433
  } catch (err) {
51433
- ctx.logger.debug(`Failed to update routine prompt post: ${err}`);
51434
- }
51435
- this.state.pendingRoutinePrompt = null;
51436
- if (this.events) {
51437
- this.events.emit("routine-prompt:complete", { approved, parsed, requestedBy, postId });
51434
+ ctx.logger.debug(`Failed to update ${label.toLowerCase()} prompt post: ${err}`);
51438
51435
  }
51436
+ clear();
51437
+ emit({ approved, parsed, requestedBy, postId });
51439
51438
  return true;
51440
51439
  }
51440
+ handleRoutinePromptResponse(postId, approved, username, ctx) {
51441
+ return this.completeCreationPrompt(this.state.pendingRoutinePrompt, "Routine", () => {
51442
+ this.state.pendingRoutinePrompt = null;
51443
+ }, (payload) => this.events?.emit("routine-prompt:complete", payload), postId, approved, username, ctx);
51444
+ }
51445
+ setPendingWatchPrompt(prompt) {
51446
+ this.state.pendingWatchPrompt = prompt;
51447
+ }
51448
+ hasPendingWatchPrompt() {
51449
+ return this.state.pendingWatchPrompt !== null;
51450
+ }
51451
+ handleWatchPromptResponse(postId, approved, username, ctx) {
51452
+ return this.completeCreationPrompt(this.state.pendingWatchPrompt, "Watch", () => {
51453
+ this.state.pendingWatchPrompt = null;
51454
+ }, (payload) => this.events?.emit("watch-prompt:complete", payload), postId, approved, username, ctx);
51455
+ }
51441
51456
  async handleReaction(postId, emoji4, user, action, ctx) {
51442
51457
  ctx.logger.debug(`PromptExecutor.handleReaction: postId=${postId.substring(0, 8)}, emoji=${emoji4}, user=${user}, action=${action}`);
51443
51458
  if (action !== "added") {
@@ -51510,6 +51525,18 @@ class PromptExecutor extends BaseExecutor {
51510
51525
  ctx.logger.debug(`PromptExecutor: emoji ${emoji4} not valid for routine prompt, ignoring`);
51511
51526
  return false;
51512
51527
  }
51528
+ if (this.state.pendingWatchPrompt?.postId === postId) {
51529
+ if (isApprovalEmoji(emoji4)) {
51530
+ ctx.logger.debug(`Watch prompt reaction from @${user}: approve`);
51531
+ return this.handleWatchPromptResponse(postId, true, user, ctx);
51532
+ }
51533
+ if (isDenialEmoji(emoji4)) {
51534
+ ctx.logger.debug(`Watch prompt reaction from @${user}: discard`);
51535
+ return this.handleWatchPromptResponse(postId, false, user, ctx);
51536
+ }
51537
+ ctx.logger.debug(`PromptExecutor: emoji ${emoji4} not valid for watch prompt, ignoring`);
51538
+ return false;
51539
+ }
51513
51540
  ctx.logger.debug(`PromptExecutor: no pending prompt state matches postId=${postId.substring(0, 8)}`);
51514
51541
  return false;
51515
51542
  }
@@ -52057,6 +52084,9 @@ class MessageManager {
52057
52084
  setPendingRoutinePrompt(prompt) {
52058
52085
  this.promptExecutor.setPendingRoutinePrompt(prompt);
52059
52086
  }
52087
+ setPendingWatchPrompt(prompt) {
52088
+ this.promptExecutor.setPendingWatchPrompt(prompt);
52089
+ }
52060
52090
  setPendingBugReport(report) {
52061
52091
  this.bugReportExecutor.setPendingBugReport(report);
52062
52092
  }
@@ -56957,6 +56987,27 @@ var COMMAND_REGISTRY = [
56957
56987
  { name: "run", description: "Run a routine now, outside its schedule", args: "<n>" }
56958
56988
  ]
56959
56989
  },
56990
+ {
56991
+ command: "watch",
56992
+ description: "Create an event trigger from a natural-language request (confirmed with \uD83D\uDC4D before saving)",
56993
+ args: "<when ..., task>",
56994
+ category: "settings",
56995
+ audience: "user",
56996
+ claudeNotes: "User decisions, not yours"
56997
+ },
56998
+ {
56999
+ command: "watches",
57000
+ description: "List event triggers; pause/resume/delete manage them",
57001
+ args: "[pause|resume|delete <n>]",
57002
+ category: "settings",
57003
+ audience: "user",
57004
+ claudeNotes: "User decisions, not yours",
57005
+ subcommands: [
57006
+ { name: "pause", description: "Pause a watch", args: "<n>" },
57007
+ { name: "resume", description: "Resume a paused watch", args: "<n>" },
57008
+ { name: "delete", description: "Delete a watch", args: "<n>" }
57009
+ ]
57010
+ },
56960
57011
  {
56961
57012
  command: "update",
56962
57013
  description: "Show auto-update status",
@@ -57255,6 +57306,30 @@ var handleRoutines = async (ctx, args) => {
57255
57306
  await ctx.sessionManager.manageRoutines(ctx.threadId, args, ctx.username);
57256
57307
  return { handled: true };
57257
57308
  };
57309
+ var handleWatch = async (ctx, args) => {
57310
+ if (ctx.commandContext === "first-message") {
57311
+ return { handled: false };
57312
+ }
57313
+ if (!ctx.isAllowed) {
57314
+ return { handled: true };
57315
+ }
57316
+ if (!args?.trim()) {
57317
+ await ctx.client.createPost(`⚠️ Usage: ${ctx.formatter.formatCode("!watch when <something happens>, <task>")}`, ctx.threadId);
57318
+ return { handled: true };
57319
+ }
57320
+ await ctx.sessionManager.createWatch(ctx.threadId, args, ctx.username);
57321
+ return { handled: true };
57322
+ };
57323
+ var handleWatches = async (ctx, args) => {
57324
+ if (ctx.commandContext === "first-message") {
57325
+ return { handled: false };
57326
+ }
57327
+ if (!ctx.isAllowed) {
57328
+ return { handled: true };
57329
+ }
57330
+ await ctx.sessionManager.manageWatches(ctx.threadId, args, ctx.username);
57331
+ return { handled: true };
57332
+ };
57258
57333
  var handleCd = async (ctx, args) => {
57259
57334
  if (!args) {
57260
57335
  return { handled: false };
@@ -57448,6 +57523,8 @@ handlers.set("remember", handleRemember);
57448
57523
  handlers.set("memory", handleMemory);
57449
57524
  handlers.set("routine", handleRoutine);
57450
57525
  handlers.set("routines", handleRoutines);
57526
+ handlers.set("watch", handleWatch);
57527
+ handlers.set("watches", handleWatches);
57451
57528
  handlers.set("cd", handleCd);
57452
57529
  handlers.set("permissions", handlePermissions);
57453
57530
  handlers.set("mentions", handleMentions);
@@ -57589,16 +57666,35 @@ var contextPromptFiles = new Map;
57589
57666
  // src/memory/store.ts
57590
57667
  import { createHash } from "crypto";
57591
57668
  import {
57592
- chmodSync as chmodSync2,
57593
57669
  existsSync as existsSync5,
57594
57670
  mkdirSync as mkdirSync2,
57595
57671
  readFileSync as readFileSync4,
57596
- renameSync,
57597
- realpathSync,
57598
- writeFileSync as writeFileSync2
57672
+ realpathSync
57599
57673
  } from "fs";
57600
57674
  import { homedir as homedir5 } from "os";
57601
57675
  import { basename as basename3, dirname as dirname7, join as join7, sep as sep2 } from "path";
57676
+
57677
+ // src/persistence/atomic-file.ts
57678
+ import { chmodSync as chmodSync2, renameSync, writeFileSync as writeFileSync2 } from "fs";
57679
+
57680
+ class SerialQueue {
57681
+ tail = Promise.resolve();
57682
+ run(fn) {
57683
+ const next = this.tail.then(fn, fn);
57684
+ this.tail = next.catch(() => {
57685
+ return;
57686
+ });
57687
+ return next;
57688
+ }
57689
+ }
57690
+ function writeFileAtomic(file2, content) {
57691
+ const tempFile = `${file2}.tmp`;
57692
+ writeFileSync2(tempFile, content, { encoding: "utf-8", mode: 384 });
57693
+ renameSync(tempFile, file2);
57694
+ chmodSync2(file2, 384);
57695
+ }
57696
+
57697
+ // src/memory/store.ts
57602
57698
  var log18 = createLogger("memory");
57603
57699
  var DEFAULT_ROOT = join7(homedir5(), ".config", "claude-threads", "memory");
57604
57700
  var CHANNEL_BLOCK_MAX_LINES = 200;
@@ -57771,12 +57867,12 @@ class MemoryStore {
57771
57867
  _(older entries omitted — \`!memory\` shows all)_` : rendered;
57772
57868
  }
57773
57869
  runExclusive(platformId, fn) {
57774
- const tail = this.locks.get(platformId) ?? Promise.resolve();
57775
- const next = tail.then(fn, fn);
57776
- this.locks.set(platformId, next.catch(() => {
57777
- return;
57778
- }));
57779
- return next;
57870
+ let queue = this.locks.get(platformId);
57871
+ if (!queue) {
57872
+ queue = new SerialQueue;
57873
+ this.locks.set(platformId, queue);
57874
+ }
57875
+ return queue.run(fn);
57780
57876
  }
57781
57877
  loadLines(platformId) {
57782
57878
  const file2 = this.channelMemoryPath(platformId);
@@ -57819,10 +57915,7 @@ _(older entries omitted — \`!memory\` shows all)_` : rendered;
57819
57915
  const content = [FILE_HEADER, ...lines.map((l) => l.raw)].join(`
57820
57916
  `) + `
57821
57917
  `;
57822
- const tempFile = `${file2}.tmp`;
57823
- writeFileSync2(tempFile, content, { encoding: "utf-8", mode: 384 });
57824
- renameSync(tempFile, file2);
57825
- chmodSync2(file2, 384);
57918
+ writeFileAtomic(file2, content);
57826
57919
  }
57827
57920
  ensureDir(dir) {
57828
57921
  if (!existsSync5(dir)) {
@@ -57834,6 +57927,117 @@ _(older entries omitted — \`!memory\` shows all)_` : rendered;
57834
57927
  // src/memory/distiller.ts
57835
57928
  var log19 = createLogger("memory");
57836
57929
 
57930
+ // src/session/registry.ts
57931
+ function compositeSessionId(platformId, threadId) {
57932
+ return `${platformId}:${threadId}`;
57933
+ }
57934
+
57935
+ class SessionRegistry {
57936
+ sessions = new Map;
57937
+ postIndex = new Map;
57938
+ sessionStore;
57939
+ constructor(sessionStore) {
57940
+ this.sessionStore = sessionStore;
57941
+ }
57942
+ getSessionId(platformId, threadId) {
57943
+ return compositeSessionId(platformId, threadId);
57944
+ }
57945
+ parseSessionId(sessionId) {
57946
+ const colonIndex = sessionId.indexOf(":");
57947
+ if (colonIndex === -1)
57948
+ return null;
57949
+ return {
57950
+ platformId: sessionId.substring(0, colonIndex),
57951
+ threadId: sessionId.substring(colonIndex + 1)
57952
+ };
57953
+ }
57954
+ find(platformId, threadId) {
57955
+ return this.sessions.get(this.getSessionId(platformId, threadId));
57956
+ }
57957
+ findByThreadId(threadId) {
57958
+ for (const session of this.sessions.values()) {
57959
+ if (session.threadId === threadId) {
57960
+ return session;
57961
+ }
57962
+ }
57963
+ return;
57964
+ }
57965
+ findByPost(postId) {
57966
+ const threadId = this.postIndex.get(postId);
57967
+ if (!threadId)
57968
+ return;
57969
+ return this.findByThreadId(threadId);
57970
+ }
57971
+ get(sessionId) {
57972
+ return this.sessions.get(sessionId);
57973
+ }
57974
+ has(platformId, threadId) {
57975
+ return this.sessions.has(this.getSessionId(platformId, threadId));
57976
+ }
57977
+ isActiveThread(threadId) {
57978
+ return this.findByThreadId(threadId) !== undefined;
57979
+ }
57980
+ register(session) {
57981
+ this.sessions.set(session.sessionId, session);
57982
+ }
57983
+ unregister(sessionId) {
57984
+ this.sessions.delete(sessionId);
57985
+ }
57986
+ registerPost(postId, threadId) {
57987
+ this.postIndex.set(postId, threadId);
57988
+ }
57989
+ unregisterPost(postId) {
57990
+ this.postIndex.delete(postId);
57991
+ }
57992
+ clearPostsForThread(threadId) {
57993
+ for (const [postId, tid] of this.postIndex.entries()) {
57994
+ if (tid === threadId) {
57995
+ this.postIndex.delete(postId);
57996
+ }
57997
+ }
57998
+ }
57999
+ getAll() {
58000
+ return Array.from(this.sessions.values());
58001
+ }
58002
+ getActiveThreadIds() {
58003
+ return Array.from(this.sessions.values()).map((s) => s.threadId);
58004
+ }
58005
+ get size() {
58006
+ return this.sessions.size;
58007
+ }
58008
+ getForPlatform(platformId) {
58009
+ return Array.from(this.sessions.values()).filter((s) => s.sessionId.startsWith(`${platformId}:`));
58010
+ }
58011
+ hasPaused(platformId, threadId) {
58012
+ return this.sessionStore.findByThread(platformId, threadId) !== undefined;
58013
+ }
58014
+ getPersisted(platformId, threadId) {
58015
+ return this.sessionStore.findByThread(platformId, threadId);
58016
+ }
58017
+ getPersistedByThreadId(threadId) {
58018
+ return this.sessionStore.findByThreadIdAnyState(threadId);
58019
+ }
58020
+ getSessionStore() {
58021
+ return this.sessionStore;
58022
+ }
58023
+ hasById(sessionId) {
58024
+ return this.sessions.has(sessionId);
58025
+ }
58026
+ clear() {
58027
+ this.sessions.clear();
58028
+ this.postIndex.clear();
58029
+ }
58030
+ getThreadIdForPost(postId) {
58031
+ return this.postIndex.get(postId);
58032
+ }
58033
+ getSessions() {
58034
+ return this.sessions;
58035
+ }
58036
+ getPostIndex() {
58037
+ return this.postIndex;
58038
+ }
58039
+ }
58040
+
57837
58041
  // src/session/lifecycle.ts
57838
58042
  var log20 = createLogger("lifecycle");
57839
58043
  var sessionLog3 = createSessionLog(log20);
@@ -57853,199 +58057,174 @@ var DEFAULT_CONFIG_DIR = join8(homedir6(), ".config", "claude-threads");
57853
58057
  var DEFAULT_FILE = join8(DEFAULT_CONFIG_DIR, "github-emails.yaml");
57854
58058
 
57855
58059
  // src/persistence/routines-store.ts
57856
- import { existsSync as existsSync6, mkdirSync as mkdirSync3, readFileSync as readFileSync5 } from "fs";
58060
+ import { join as join10 } from "path";
58061
+
58062
+ // src/persistence/platform-list-store.ts
58063
+ import { existsSync as existsSync6, mkdirSync as mkdirSync3, readFileSync as readFileSync5, statSync as statSync2 } from "fs";
57857
58064
  import { homedir as homedir7 } from "os";
57858
58065
  import { join as join9 } from "path";
57859
- import { randomUUID as randomUUID2 } from "crypto";
57860
-
57861
- // src/persistence/atomic-file.ts
57862
- import { chmodSync as chmodSync3, renameSync as renameSync2, writeFileSync as writeFileSync3 } from "fs";
57863
-
57864
- class SerialQueue {
57865
- tail = Promise.resolve();
57866
- run(fn) {
57867
- const next = this.tail.then(fn, fn);
57868
- this.tail = next.catch(() => {
57869
- return;
57870
- });
57871
- return next;
57872
- }
57873
- }
57874
- function writeFileAtomic(file2, content) {
57875
- const tempFile = `${file2}.tmp`;
57876
- writeFileSync3(tempFile, content, { encoding: "utf-8", mode: 384 });
57877
- renameSync2(tempFile, file2);
57878
- chmodSync3(file2, 384);
57879
- }
57880
-
57881
- // src/persistence/routines-store.ts
57882
- var log22 = createLogger("routines");
57883
- var DEFAULT_CONFIG_DIR2 = join9(homedir7(), ".config", "claude-threads");
57884
- var DEFAULT_FILE2 = join9(DEFAULT_CONFIG_DIR2, "routines.yaml");
58066
+ var STORES_CONFIG_DIR = join9(homedir7(), ".config", "claude-threads");
57885
58067
  var STORE_VERSION = 1;
57886
- var DEFAULT_MAX_ROUTINES = 10;
57887
- var SCHEDULE_PRESETS = ["hourly", "daily", "weekdays", "weekly"];
57888
- var TIME_RE = /^([01]\d|2[0-3]):[0-5]\d$/;
57889
- function isValidTimezone(tz) {
57890
- if (typeof tz !== "string" || !tz)
57891
- return false;
57892
- try {
57893
- new Intl.DateTimeFormat("en-US", { timeZone: tz });
57894
- return true;
57895
- } catch {
57896
- return false;
57897
- }
57898
- }
57899
- function validateSchedule(schedule) {
57900
- if (!SCHEDULE_PRESETS.includes(schedule.preset)) {
57901
- return `unknown preset "${String(schedule.preset)}" (expected ${SCHEDULE_PRESETS.join("/")})`;
57902
- }
57903
- if (!isValidTimezone(schedule.timezone)) {
57904
- return `invalid timezone "${String(schedule.timezone)}"`;
57905
- }
57906
- if (schedule.preset === "hourly") {
57907
- return null;
57908
- }
57909
- if (!schedule.time || !TIME_RE.test(schedule.time)) {
57910
- return `invalid time "${String(schedule.time)}" (expected HH:MM, 24h)`;
57911
- }
57912
- if (schedule.preset === "weekly") {
57913
- const weekday = schedule.weekday;
57914
- if (typeof weekday !== "number" || !Number.isInteger(weekday) || weekday < 1 || weekday > 7) {
57915
- return `invalid weekday "${String(weekday)}" (expected 1=Mon … 7=Sun)`;
57916
- }
57917
- }
57918
- return null;
57919
- }
57920
- class RoutinesStore {
58068
+
58069
+ class PlatformListStore {
57921
58070
  file;
57922
58071
  configDir;
57923
58072
  queue = new SerialQueue;
57924
- constructor(filePath) {
57925
- const effective = filePath ?? process.env.CLAUDE_THREADS_ROUTINES_PATH;
57926
- if (effective) {
57927
- this.file = effective;
57928
- this.configDir = join9(effective, "..");
58073
+ collectionKey;
58074
+ cache = null;
58075
+ constructor(collectionKey, defaultFile, filePath) {
58076
+ this.collectionKey = collectionKey;
58077
+ if (filePath) {
58078
+ this.file = filePath;
58079
+ this.configDir = join9(filePath, "..");
57929
58080
  } else {
57930
- this.file = DEFAULT_FILE2;
57931
- this.configDir = DEFAULT_CONFIG_DIR2;
57932
- }
57933
- if (!existsSync6(this.configDir)) {
57934
- mkdirSync3(this.configDir, { recursive: true, mode: 448 });
58081
+ this.file = defaultFile;
58082
+ this.configDir = STORES_CONFIG_DIR;
57935
58083
  }
58084
+ mkdirSync3(this.configDir, { recursive: true, mode: 448 });
57936
58085
  }
57937
58086
  list(platformId) {
57938
- return this.loadRaw().routines[platformId] ?? [];
58087
+ return structuredClone(this.loadRaw().items[platformId] ?? []);
57939
58088
  }
57940
58089
  get(platformId, id) {
57941
- return this.list(platformId).find((r) => r.id === id);
58090
+ const item = (this.loadRaw().items[platformId] ?? []).find((i) => i.id === id);
58091
+ return item === undefined ? undefined : structuredClone(item);
57942
58092
  }
57943
- add(platformId, routine, maxRoutines = DEFAULT_MAX_ROUTINES) {
58093
+ addItem(platformId, max, capNoun, build) {
57944
58094
  return this.runExclusive(() => {
57945
- const scheduleError = validateSchedule(routine.schedule);
57946
- if (scheduleError)
57947
- return { ok: false, error: scheduleError };
57948
- const name = routine.name.trim().slice(0, 80);
57949
- const prompt = routine.prompt.trim().slice(0, 2000);
57950
- if (!name || !prompt)
57951
- return { ok: false, error: "name and prompt are required" };
57952
- const data = this.loadRaw();
57953
- const existing = data.routines[platformId] ?? [];
57954
- if (existing.length >= maxRoutines) {
57955
- return { ok: false, error: `routine limit reached (${maxRoutines}); delete one first` };
57956
- }
57957
- const full = {
57958
- ...routine,
57959
- name,
57960
- prompt,
57961
- id: randomUUID2().slice(0, 8),
57962
- createdAt: new Date().toISOString(),
57963
- enabled: true,
57964
- consecutiveFailures: 0
57965
- };
57966
- data.routines[platformId] = [...existing, full];
58095
+ const built = build();
58096
+ if (typeof built === "string")
58097
+ return { ok: false, error: built };
58098
+ const data = this.loadRaw(true);
58099
+ const existing = data.items[platformId] ?? [];
58100
+ if (existing.length >= max) {
58101
+ return { ok: false, error: `${capNoun} limit reached (${max}); delete one first` };
58102
+ }
58103
+ data.items[platformId] = [...existing, built];
57967
58104
  this.writeAtomic(data);
57968
- log22.info(`Routine "${full.name}" created on ${platformId} by @${full.createdBy}`);
57969
- return { ok: true, routine: full };
58105
+ return { ok: true, item: structuredClone(built) };
57970
58106
  });
57971
58107
  }
57972
58108
  update(platformId, id, patch) {
57973
58109
  return this.runExclusive(() => {
57974
- const data = this.loadRaw();
57975
- const routines = data.routines[platformId] ?? [];
57976
- const idx = routines.findIndex((r) => r.id === id);
58110
+ const data = this.loadRaw(true);
58111
+ const items = data.items[platformId] ?? [];
58112
+ const idx = items.findIndex((item) => item.id === id);
57977
58113
  if (idx < 0)
57978
58114
  return;
57979
- routines[idx] = { ...routines[idx], ...patch };
58115
+ items[idx] = { ...items[idx], ...patch };
57980
58116
  this.writeAtomic(data);
57981
- return routines[idx];
58117
+ return structuredClone(items[idx]);
57982
58118
  });
57983
58119
  }
57984
58120
  remove(platformId, id) {
57985
58121
  return this.runExclusive(() => {
57986
- const data = this.loadRaw();
57987
- const routines = data.routines[platformId] ?? [];
57988
- const idx = routines.findIndex((r) => r.id === id);
58122
+ const data = this.loadRaw(true);
58123
+ const items = data.items[platformId] ?? [];
58124
+ const idx = items.findIndex((item) => item.id === id);
57989
58125
  if (idx < 0)
57990
58126
  return;
57991
- const [removed] = routines.splice(idx, 1);
57992
- if (routines.length === 0)
57993
- delete data.routines[platformId];
58127
+ const [removed] = items.splice(idx, 1);
58128
+ if (items.length === 0)
58129
+ delete data.items[platformId];
57994
58130
  this.writeAtomic(data);
57995
- log22.info(`Routine "${removed.name}" removed from ${platformId}`);
58131
+ this.onRemoved(platformId, removed);
57996
58132
  return removed;
57997
58133
  });
57998
58134
  }
58135
+ onRemoved(_platformId, _item) {}
57999
58136
  runExclusive(fn) {
58000
58137
  return this.queue.run(fn);
58001
58138
  }
58002
- loadRaw() {
58139
+ loadRaw(forWrite = false) {
58003
58140
  if (!existsSync6(this.file)) {
58004
- return { version: STORE_VERSION, routines: {} };
58141
+ this.cache = null;
58142
+ return { version: STORE_VERSION, items: {} };
58005
58143
  }
58006
58144
  try {
58007
- const parsed = yaml.load(readFileSync5(this.file, "utf-8"));
58008
- if (!parsed || typeof parsed !== "object") {
58009
- return { version: STORE_VERSION, routines: {} };
58145
+ const stat = statSync2(this.file);
58146
+ if (this.cache && this.cache.mtimeMs === stat.mtimeMs && this.cache.size === stat.size) {
58147
+ return this.cache.data;
58010
58148
  }
58011
- const routines = parsed.routines && typeof parsed.routines === "object" ? parsed.routines : {};
58012
- for (const list of Object.values(routines)) {
58013
- for (const r of list) {
58014
- r.enabled = r.enabled ?? true;
58015
- r.consecutiveFailures = r.consecutiveFailures ?? 0;
58016
- }
58017
- }
58018
- return { version: parsed.version ?? STORE_VERSION, routines };
58149
+ const parsed = yaml.load(readFileSync5(this.file, "utf-8"));
58150
+ const rawItems = parsed && typeof parsed === "object" && !Array.isArray(parsed) ? parsed[this.collectionKey] : undefined;
58151
+ if (rawItems !== null && (rawItems === undefined || typeof rawItems !== "object" || Array.isArray(rawItems))) {
58152
+ this.cache = null;
58153
+ const problem = `unexpected shape (missing or non-map '${this.collectionKey}' key)`;
58154
+ if (forWrite) {
58155
+ throw new Error(`refusing to write over unreadable ${this.file}: ${problem}`);
58156
+ }
58157
+ this.warn(`Failed to read ${this.file}: ${problem} — starting empty`);
58158
+ return { version: STORE_VERSION, items: {} };
58159
+ }
58160
+ const items = rawItems ?? {};
58161
+ for (const list of Object.values(items)) {
58162
+ for (const item of list)
58163
+ this.applyItemDefaults(item);
58164
+ }
58165
+ const data = { version: parsed?.version ?? STORE_VERSION, items };
58166
+ this.cache = { mtimeMs: stat.mtimeMs, size: stat.size, data };
58167
+ return data;
58019
58168
  } catch (err) {
58020
- log22.warn(`Failed to read ${this.file}: ${err.message} — starting empty`);
58021
- return { version: STORE_VERSION, routines: {} };
58169
+ this.cache = null;
58170
+ if (forWrite) {
58171
+ throw new Error(`refusing to write over unreadable ${this.file}: ${err.message}`, { cause: err });
58172
+ }
58173
+ this.warn(`Failed to read ${this.file}: ${err.message} — starting empty`);
58174
+ return { version: STORE_VERSION, items: {} };
58022
58175
  }
58023
58176
  }
58024
58177
  writeAtomic(data) {
58025
- writeFileAtomic(this.file, yaml.dump(data, { sortKeys: true, lineWidth: -1 }));
58178
+ try {
58179
+ this.persistFile(yaml.dump({ version: data.version, [this.collectionKey]: data.items }, { sortKeys: true, lineWidth: -1 }));
58180
+ } catch (err) {
58181
+ this.cache = null;
58182
+ throw err;
58183
+ }
58184
+ try {
58185
+ const stat = statSync2(this.file);
58186
+ this.cache = { mtimeMs: stat.mtimeMs, size: stat.size, data };
58187
+ } catch {
58188
+ this.cache = null;
58189
+ }
58190
+ }
58191
+ persistFile(content) {
58192
+ writeFileAtomic(this.file, content);
58026
58193
  }
58027
58194
  }
58028
58195
 
58196
+ // src/persistence/routines-store.ts
58197
+ var log22 = createLogger("routines");
58198
+ var DEFAULT_FILE2 = join10(STORES_CONFIG_DIR, "routines.yaml");
58199
+
58029
58200
  // src/routines/parser.ts
58030
58201
  var log23 = createLogger("routines");
58031
58202
 
58203
+ // src/persistence/watches-store.ts
58204
+ import { join as join11 } from "path";
58205
+ var log24 = createLogger("watches");
58206
+ var DEFAULT_FILE3 = join11(STORES_CONFIG_DIR, "watches.yaml");
58207
+
58208
+ // src/watches/parser.ts
58209
+ var log25 = createLogger("watches");
58210
+
58032
58211
  // src/operations/commands/handler.ts
58033
- var log24 = createLogger("commands");
58034
- var sessionLog4 = createSessionLog(log24);
58212
+ var log26 = createLogger("commands");
58213
+ var sessionLog4 = createSessionLog(log26);
58035
58214
  // src/operations/suggestions/branch.ts
58036
58215
  import { exec as exec2 } from "child_process";
58037
58216
  import { promisify as promisify2 } from "util";
58038
58217
  var execAsync2 = promisify2(exec2);
58039
- var log25 = createLogger("branch");
58218
+ var log27 = createLogger("branch");
58040
58219
 
58041
58220
  // src/operations/worktree/handler.ts
58042
- var log26 = createLogger("worktree");
58043
- var sessionLog5 = createSessionLog(log26);
58221
+ var log28 = createLogger("worktree");
58222
+ var sessionLog5 = createSessionLog(log28);
58044
58223
  // src/operations/events/handler.ts
58045
- var log27 = createLogger("events");
58046
- var sessionLog6 = createSessionLog(log27);
58224
+ var log29 = createLogger("events");
58225
+ var sessionLog6 = createSessionLog(log29);
58047
58226
  // src/operations/monitor/handler.ts
58048
- var log28 = createLogger("monitor");
58227
+ var log30 = createLogger("monitor");
58049
58228
  var DEFAULT_INTERVAL_MS = 60 * 1000;
58050
58229
  // src/utils/websocket.ts
58051
58230
  var WS;
@@ -58127,7 +58306,7 @@ ${code}
58127
58306
 
58128
58307
  // src/platform/mattermost/upload.ts
58129
58308
  import { readFile } from "fs/promises";
58130
- var log29 = createLogger("mm-upload");
58309
+ var log31 = createLogger("mm-upload");
58131
58310
  async function uploadFileMattermost(args) {
58132
58311
  const { url: url2, token, channelId, threadId, filePath, filename, caption } = args;
58133
58312
  const buffer = await readFile(filePath);
@@ -58135,7 +58314,7 @@ async function uploadFileMattermost(args) {
58135
58314
  const arrayBuffer = buffer.buffer.slice(buffer.byteOffset, buffer.byteOffset + buffer.byteLength);
58136
58315
  const formData = new FormData;
58137
58316
  formData.append("files", new Blob([arrayBuffer]), filename);
58138
- log29.debug(`POST /files (${buffer.length} bytes, ${filename})`);
58317
+ log31.debug(`POST /files (${buffer.length} bytes, ${filename})`);
58139
58318
  const uploadResponse = await fetch(uploadUrl, {
58140
58319
  method: "POST",
58141
58320
  headers: {
@@ -58159,7 +58338,7 @@ async function uploadFileMattermost(args) {
58159
58338
  root_id: resolvePostThreadId(threadId),
58160
58339
  file_ids: [fileInfo.id]
58161
58340
  };
58162
- log29.debug(`POST /posts (file_ids=[${fileInfo.id}])`);
58341
+ log31.debug(`POST /posts (file_ids=[${fileInfo.id}])`);
58163
58342
  const postResponse = await fetch(postUrl, {
58164
58343
  method: "POST",
58165
58344
  headers: {
@@ -58653,7 +58832,7 @@ ${code}
58653
58832
 
58654
58833
  // src/platform/slack/upload.ts
58655
58834
  import { readFile as readFile2 } from "fs/promises";
58656
- var log30 = createLogger("slack-upload");
58835
+ var log32 = createLogger("slack-upload");
58657
58836
  var DEFAULT_API_URL = "https://slack.com/api";
58658
58837
  async function uploadFileSlack(args) {
58659
58838
  const { botToken, channelId, threadTs, filePath, filename, caption } = args;
@@ -58661,7 +58840,7 @@ async function uploadFileSlack(args) {
58661
58840
  const buffer = await readFile2(filePath);
58662
58841
  const params = new URLSearchParams({ filename, length: String(buffer.length) });
58663
58842
  const step1Url = `${apiUrl}/files.getUploadURLExternal?${params.toString()}`;
58664
- log30.debug(`GET files.getUploadURLExternal (${buffer.length} bytes, ${filename})`);
58843
+ log32.debug(`GET files.getUploadURLExternal (${buffer.length} bytes, ${filename})`);
58665
58844
  const step1Response = await fetch(step1Url, {
58666
58845
  method: "GET",
58667
58846
  headers: {
@@ -58679,7 +58858,7 @@ async function uploadFileSlack(args) {
58679
58858
  const uploadUrl = step1Data.upload_url;
58680
58859
  const fileId = step1Data.file_id;
58681
58860
  const arrayBuffer = buffer.buffer.slice(buffer.byteOffset, buffer.byteOffset + buffer.byteLength);
58682
- log30.debug(`POST <upload_url>`);
58861
+ log32.debug(`POST <upload_url>`);
58683
58862
  const step2Response = await fetch(uploadUrl, {
58684
58863
  method: "POST",
58685
58864
  headers: {
@@ -58699,7 +58878,7 @@ async function uploadFileSlack(args) {
58699
58878
  if (caption !== undefined) {
58700
58879
  step3Body.initial_comment = caption;
58701
58880
  }
58702
- log30.debug(`POST files.completeUploadExternal (file_id=${fileId}, thread_ts=${threadTs})`);
58881
+ log32.debug(`POST files.completeUploadExternal (file_id=${fileId}, thread_ts=${threadTs})`);
58703
58882
  const step3Response = await fetch(`${apiUrl}/files.completeUploadExternal`, {
58704
58883
  method: "POST",
58705
58884
  headers: {
@@ -58717,7 +58896,7 @@ async function uploadFileSlack(args) {
58717
58896
  throw new Error(`Slack completeUploadExternal error: ${step3Data.error || "unknown"}`);
58718
58897
  }
58719
58898
  if (!step3Data.ts) {
58720
- log30.warn(`Slack completeUploadExternal returned no ts; using fileId ${fileId} as postId. ` + `Do not use this id for updatePost/addReaction.`);
58899
+ log32.warn(`Slack completeUploadExternal returned no ts; using fileId ${fileId} as postId. ` + `Do not use this id for updatePost/addReaction.`);
58721
58900
  }
58722
58901
  return { fileId, postId: step3Data.ts ?? fileId };
58723
58902
  }