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.
- package/CHANGELOG.md +16 -0
- package/README.md +5 -0
- package/dist/index.js +1814 -1046
- package/dist/mcp/mcp-server.js +344 -48
- package/docs/CONFIGURATION.md +59 -0
- package/package.json +1 -1
package/dist/mcp/mcp-server.js
CHANGED
|
@@ -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
|
-
|
|
55476
|
-
|
|
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
|
-
|
|
55497
|
-
|
|
55498
|
-
|
|
55499
|
-
|
|
55500
|
-
|
|
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
|
|
57632
|
-
var sessionLog4 = createSessionLog(
|
|
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
|
|
57933
|
+
var log24 = createLogger("branch");
|
|
57638
57934
|
|
|
57639
57935
|
// src/operations/worktree/handler.ts
|
|
57640
|
-
var
|
|
57641
|
-
var sessionLog5 = createSessionLog(
|
|
57936
|
+
var log25 = createLogger("worktree");
|
|
57937
|
+
var sessionLog5 = createSessionLog(log25);
|
|
57642
57938
|
// src/operations/events/handler.ts
|
|
57643
|
-
var
|
|
57644
|
-
var sessionLog6 = createSessionLog(
|
|
57939
|
+
var log26 = createLogger("events");
|
|
57940
|
+
var sessionLog6 = createSessionLog(log26);
|
|
57645
57941
|
// src/operations/monitor/handler.ts
|
|
57646
|
-
var
|
|
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
|
|
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
|
-
|
|
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
|
-
|
|
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
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
}
|
package/docs/CONFIGURATION.md
CHANGED
|
@@ -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