openclaw-telegram-manager 2.4.0 → 2.5.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/README.md +6 -5
- package/dist/commands/autopilot.d.ts +4 -0
- package/dist/commands/autopilot.d.ts.map +1 -1
- package/dist/commands/autopilot.js +5 -5
- package/dist/commands/autopilot.js.map +1 -1
- package/dist/commands/daily-report.d.ts +6 -0
- package/dist/commands/daily-report.d.ts.map +1 -1
- package/dist/commands/daily-report.js +6 -6
- package/dist/commands/daily-report.js.map +1 -1
- package/dist/commands/doctor-all.d.ts.map +1 -1
- package/dist/commands/doctor-all.js +66 -2
- package/dist/commands/doctor-all.js.map +1 -1
- package/dist/commands/init.d.ts.map +1 -1
- package/dist/commands/init.js +29 -1
- package/dist/commands/init.js.map +1 -1
- package/dist/plugin.js +452 -375
- package/dist/setup.js +46 -1
- package/dist/setup.js.map +1 -1
- package/package.json +1 -1
- package/skills/tm/SKILL.md +4 -4
package/dist/plugin.js
CHANGED
|
@@ -9272,8 +9272,8 @@ function upgradeCapsule(projectsBase, slug, name, type, currentVersion) {
|
|
|
9272
9272
|
}
|
|
9273
9273
|
|
|
9274
9274
|
// src/commands/init.ts
|
|
9275
|
-
import * as
|
|
9276
|
-
import * as
|
|
9275
|
+
import * as fs7 from "node:fs";
|
|
9276
|
+
import * as path7 from "node:path";
|
|
9277
9277
|
|
|
9278
9278
|
// src/lib/auth.ts
|
|
9279
9279
|
var AuthTier = {
|
|
@@ -9871,6 +9871,119 @@ async function getConfigWrites(rpc) {
|
|
|
9871
9871
|
}
|
|
9872
9872
|
}
|
|
9873
9873
|
|
|
9874
|
+
// src/commands/autopilot.ts
|
|
9875
|
+
import * as fs6 from "node:fs";
|
|
9876
|
+
import * as path6 from "node:path";
|
|
9877
|
+
var MARKER_START = "<!-- TM_AUTOPILOT_START -->";
|
|
9878
|
+
var MARKER_END = "<!-- TM_AUTOPILOT_END -->";
|
|
9879
|
+
var HEARTBEAT_BLOCK = `${MARKER_START}
|
|
9880
|
+
## Topic Manager \u2014 Balanced Autopilot
|
|
9881
|
+
|
|
9882
|
+
Execute these steps IN ORDER. Do not skip steps.
|
|
9883
|
+
|
|
9884
|
+
1. CALL \`topic_manager\` with command "doctor --all" to health-check all active topics.
|
|
9885
|
+
This runs health checks AND posts daily progress reports for each eligible topic automatically.
|
|
9886
|
+
2. For each topic where STATUS.md "Last done (UTC)" is >3 days old:
|
|
9887
|
+
post a brief nudge in that topic asking the user for a status update.
|
|
9888
|
+
3. If no actions were taken in steps 1-2, do nothing (HEARTBEAT_OK).
|
|
9889
|
+
${MARKER_END}`;
|
|
9890
|
+
var HEARTBEAT_FILENAME = "HEARTBEAT.md";
|
|
9891
|
+
async function handleAutopilot(ctx, args) {
|
|
9892
|
+
const { workspaceDir, userId } = ctx;
|
|
9893
|
+
if (!userId) {
|
|
9894
|
+
return { text: "Missing context: userId not available." };
|
|
9895
|
+
}
|
|
9896
|
+
const registry = readRegistry(workspaceDir);
|
|
9897
|
+
const auth = checkAuthorization(userId, "autopilot", registry);
|
|
9898
|
+
if (!auth.authorized) {
|
|
9899
|
+
return { text: auth.message ?? "Not authorized." };
|
|
9900
|
+
}
|
|
9901
|
+
const subCommand = args.trim().toLowerCase() || "enable";
|
|
9902
|
+
switch (subCommand) {
|
|
9903
|
+
case "enable":
|
|
9904
|
+
return handleEnable(ctx);
|
|
9905
|
+
case "disable":
|
|
9906
|
+
return handleDisable(ctx);
|
|
9907
|
+
case "status":
|
|
9908
|
+
return handleStatus(ctx);
|
|
9909
|
+
default:
|
|
9910
|
+
return { text: `Unknown autopilot sub-command: "${subCommand}". Use enable, disable, or status.` };
|
|
9911
|
+
}
|
|
9912
|
+
}
|
|
9913
|
+
async function handleEnable(ctx) {
|
|
9914
|
+
const { workspaceDir } = ctx;
|
|
9915
|
+
const heartbeatPath = path6.join(workspaceDir, HEARTBEAT_FILENAME);
|
|
9916
|
+
let content = "";
|
|
9917
|
+
try {
|
|
9918
|
+
if (fs6.existsSync(heartbeatPath)) {
|
|
9919
|
+
content = fs6.readFileSync(heartbeatPath, "utf-8");
|
|
9920
|
+
}
|
|
9921
|
+
} catch {
|
|
9922
|
+
}
|
|
9923
|
+
if (content.includes(MARKER_START)) {
|
|
9924
|
+
await withRegistry(workspaceDir, (data) => {
|
|
9925
|
+
data.autopilotEnabled = true;
|
|
9926
|
+
});
|
|
9927
|
+
return { text: "Autopilot is already enabled." };
|
|
9928
|
+
}
|
|
9929
|
+
const newContent = content ? content.trimEnd() + "\n\n" + HEARTBEAT_BLOCK + "\n" : HEARTBEAT_BLOCK + "\n";
|
|
9930
|
+
fs6.writeFileSync(heartbeatPath, newContent, { mode: 416 });
|
|
9931
|
+
await withRegistry(workspaceDir, (data) => {
|
|
9932
|
+
data.autopilotEnabled = true;
|
|
9933
|
+
});
|
|
9934
|
+
return {
|
|
9935
|
+
text: "**Autopilot enabled.**\nHealth checks will run automatically every day."
|
|
9936
|
+
};
|
|
9937
|
+
}
|
|
9938
|
+
async function handleDisable(ctx) {
|
|
9939
|
+
const { workspaceDir } = ctx;
|
|
9940
|
+
const heartbeatPath = path6.join(workspaceDir, HEARTBEAT_FILENAME);
|
|
9941
|
+
if (!fs6.existsSync(heartbeatPath)) {
|
|
9942
|
+
await withRegistry(workspaceDir, (data) => {
|
|
9943
|
+
data.autopilotEnabled = false;
|
|
9944
|
+
});
|
|
9945
|
+
return { text: "Autopilot is already disabled." };
|
|
9946
|
+
}
|
|
9947
|
+
let content = fs6.readFileSync(heartbeatPath, "utf-8");
|
|
9948
|
+
if (!content.includes(MARKER_START)) {
|
|
9949
|
+
await withRegistry(workspaceDir, (data) => {
|
|
9950
|
+
data.autopilotEnabled = false;
|
|
9951
|
+
});
|
|
9952
|
+
return { text: "Autopilot is already disabled." };
|
|
9953
|
+
}
|
|
9954
|
+
const startIdx = content.indexOf(MARKER_START);
|
|
9955
|
+
const endIdx = content.indexOf(MARKER_END);
|
|
9956
|
+
if (startIdx >= 0 && endIdx >= 0) {
|
|
9957
|
+
const before = content.slice(0, startIdx);
|
|
9958
|
+
const after = content.slice(endIdx + MARKER_END.length);
|
|
9959
|
+
content = (before + after).replace(/\n{3,}/g, "\n\n").trim();
|
|
9960
|
+
if (content) {
|
|
9961
|
+
fs6.writeFileSync(heartbeatPath, content + "\n", { mode: 416 });
|
|
9962
|
+
} else {
|
|
9963
|
+
fs6.unlinkSync(heartbeatPath);
|
|
9964
|
+
}
|
|
9965
|
+
}
|
|
9966
|
+
await withRegistry(workspaceDir, (data) => {
|
|
9967
|
+
data.autopilotEnabled = false;
|
|
9968
|
+
});
|
|
9969
|
+
return {
|
|
9970
|
+
text: "**Autopilot disabled.**\nAutomatic health checks are now off."
|
|
9971
|
+
};
|
|
9972
|
+
}
|
|
9973
|
+
async function handleStatus(ctx) {
|
|
9974
|
+
const { workspaceDir } = ctx;
|
|
9975
|
+
const registry = readRegistry(workspaceDir);
|
|
9976
|
+
const enabled = registry.autopilotEnabled;
|
|
9977
|
+
const lastRun = registry.lastDoctorAllRunAt ? relativeTime(registry.lastDoctorAllRunAt) : "never";
|
|
9978
|
+
const lines = [
|
|
9979
|
+
`**Autopilot:** ${enabled ? "enabled" : "disabled"}`,
|
|
9980
|
+
`**Last health check run:** ${lastRun}`
|
|
9981
|
+
];
|
|
9982
|
+
return {
|
|
9983
|
+
text: lines.join("\n")
|
|
9984
|
+
};
|
|
9985
|
+
}
|
|
9986
|
+
|
|
9874
9987
|
// src/commands/init.ts
|
|
9875
9988
|
var VALID_TYPES = /* @__PURE__ */ new Set(["coding", "research", "marketing", "custom"]);
|
|
9876
9989
|
function deriveTopicName(nameArg, messageContext, threadId) {
|
|
@@ -9928,17 +10041,17 @@ async function handleInit(ctx, args) {
|
|
|
9928
10041
|
const name = deriveTopicName(nameArg, messageContext, threadId);
|
|
9929
10042
|
const existingSlugs = new Set(Object.values(registry.topics).map((t) => t.slug));
|
|
9930
10043
|
const finalSlug = generateSlug(threadId, groupId, existingSlugs);
|
|
9931
|
-
const projectsBase =
|
|
10044
|
+
const projectsBase = path7.join(workspaceDir, "projects");
|
|
9932
10045
|
if (!jailCheck(projectsBase, finalSlug)) {
|
|
9933
10046
|
return { text: "Setup failed \u2014 internal path validation error. Please try again." };
|
|
9934
10047
|
}
|
|
9935
10048
|
if (rejectSymlink(projectsBase)) {
|
|
9936
10049
|
return { text: "Setup failed \u2014 detected an unsafe file system configuration." };
|
|
9937
10050
|
}
|
|
9938
|
-
if (
|
|
10051
|
+
if (fs7.existsSync(path7.join(projectsBase, finalSlug))) {
|
|
9939
10052
|
return { text: "A folder for this topic already exists. Run /tm doctor to investigate." };
|
|
9940
10053
|
}
|
|
9941
|
-
const targetPath =
|
|
10054
|
+
const targetPath = path7.join(projectsBase, finalSlug);
|
|
9942
10055
|
if (rejectSymlink(targetPath)) {
|
|
9943
10056
|
return { text: "Setup failed \u2014 detected an unsafe file system configuration." };
|
|
9944
10057
|
}
|
|
@@ -9967,12 +10080,32 @@ async function handleInit(ctx, args) {
|
|
|
9967
10080
|
data.topics[key] = newEntry;
|
|
9968
10081
|
if (isFirstUser) {
|
|
9969
10082
|
data.topicManagerAdmins.push(userId);
|
|
10083
|
+
data.autopilotEnabled = true;
|
|
9970
10084
|
}
|
|
9971
10085
|
});
|
|
9972
10086
|
} catch (err) {
|
|
9973
10087
|
const msg = err instanceof Error ? err.message : String(err);
|
|
9974
10088
|
return { text: `Failed to initialize topic: ${msg}` };
|
|
9975
10089
|
}
|
|
10090
|
+
if (isFirstUser) {
|
|
10091
|
+
try {
|
|
10092
|
+
const heartbeatPath = path7.join(workspaceDir, HEARTBEAT_FILENAME);
|
|
10093
|
+
let hbContent = "";
|
|
10094
|
+
try {
|
|
10095
|
+
if (fs7.existsSync(heartbeatPath)) {
|
|
10096
|
+
hbContent = fs7.readFileSync(heartbeatPath, "utf-8");
|
|
10097
|
+
}
|
|
10098
|
+
} catch {
|
|
10099
|
+
}
|
|
10100
|
+
if (!hbContent.includes(MARKER_START)) {
|
|
10101
|
+
const newContent = hbContent ? hbContent.trimEnd() + "\n\n" + HEARTBEAT_BLOCK + "\n" : HEARTBEAT_BLOCK + "\n";
|
|
10102
|
+
const tmpPath = heartbeatPath + ".tmp";
|
|
10103
|
+
fs7.writeFileSync(tmpPath, newContent, { mode: 416 });
|
|
10104
|
+
fs7.renameSync(tmpPath, heartbeatPath);
|
|
10105
|
+
}
|
|
10106
|
+
} catch {
|
|
10107
|
+
}
|
|
10108
|
+
}
|
|
9976
10109
|
let restartMsg = "";
|
|
9977
10110
|
const configWritesEnabled = await getConfigWrites(ctx.rpc);
|
|
9978
10111
|
if (configWritesEnabled) {
|
|
@@ -10109,27 +10242,27 @@ function buildInitConfirmMessage(name, type) {
|
|
|
10109
10242
|
}
|
|
10110
10243
|
|
|
10111
10244
|
// src/commands/doctor.ts
|
|
10112
|
-
import * as
|
|
10113
|
-
import * as
|
|
10245
|
+
import * as fs9 from "node:fs";
|
|
10246
|
+
import * as path9 from "node:path";
|
|
10114
10247
|
|
|
10115
10248
|
// src/lib/doctor-checks.ts
|
|
10116
10249
|
var import_json52 = __toESM(require_lib(), 1);
|
|
10117
|
-
import * as
|
|
10118
|
-
import * as
|
|
10250
|
+
import * as fs8 from "node:fs";
|
|
10251
|
+
import * as path8 from "node:path";
|
|
10119
10252
|
function check(severity, checkId, message, fixable, remediation) {
|
|
10120
10253
|
return remediation ? { severity, checkId, message, fixable, remediation } : { severity, checkId, message, fixable };
|
|
10121
10254
|
}
|
|
10122
10255
|
function runRegistryChecks(entry, projectsBase) {
|
|
10123
10256
|
const results = [];
|
|
10124
|
-
const capsuleDir =
|
|
10125
|
-
if (!
|
|
10257
|
+
const capsuleDir = path8.join(projectsBase, entry.slug);
|
|
10258
|
+
if (!fs8.existsSync(capsuleDir)) {
|
|
10126
10259
|
results.push(
|
|
10127
10260
|
check(Severity.ERROR, "pathMissing", `Project folder is missing (projects/${entry.slug}/)`, false, "Run /tm init to recreate it")
|
|
10128
10261
|
);
|
|
10129
10262
|
return results;
|
|
10130
10263
|
}
|
|
10131
10264
|
try {
|
|
10132
|
-
const stat =
|
|
10265
|
+
const stat = fs8.statSync(capsuleDir);
|
|
10133
10266
|
if (!stat.isDirectory()) {
|
|
10134
10267
|
results.push(
|
|
10135
10268
|
check(Severity.ERROR, "pathNotDir", "Topic path exists but is not a folder", false)
|
|
@@ -10144,21 +10277,21 @@ function runRegistryChecks(entry, projectsBase) {
|
|
|
10144
10277
|
}
|
|
10145
10278
|
function runCapsuleChecks(entry, projectsBase) {
|
|
10146
10279
|
const results = [];
|
|
10147
|
-
const capsuleDir =
|
|
10148
|
-
if (!
|
|
10149
|
-
if (!
|
|
10280
|
+
const capsuleDir = path8.join(projectsBase, entry.slug);
|
|
10281
|
+
if (!fs8.existsSync(capsuleDir)) return results;
|
|
10282
|
+
if (!fs8.existsSync(path8.join(capsuleDir, "STATUS.md"))) {
|
|
10150
10283
|
results.push(
|
|
10151
10284
|
check(Severity.ERROR, "statusMissing", "Status file is missing", true, "Run /tm upgrade to recreate it")
|
|
10152
10285
|
);
|
|
10153
10286
|
}
|
|
10154
|
-
if (!
|
|
10287
|
+
if (!fs8.existsSync(path8.join(capsuleDir, "TODO.md"))) {
|
|
10155
10288
|
results.push(
|
|
10156
10289
|
check(Severity.WARN, "todoMissing", "TODO file is missing", true, "Run /tm upgrade to recreate it")
|
|
10157
10290
|
);
|
|
10158
10291
|
}
|
|
10159
10292
|
const overlays = OVERLAY_FILES[entry.type] ?? [];
|
|
10160
10293
|
for (const file of overlays) {
|
|
10161
|
-
if (!
|
|
10294
|
+
if (!fs8.existsSync(path8.join(capsuleDir, file))) {
|
|
10162
10295
|
results.push(
|
|
10163
10296
|
check(Severity.INFO, `overlayMissing:${file}`, `Optional overlay ${file} missing for type "${entry.type}"`, true)
|
|
10164
10297
|
);
|
|
@@ -10300,9 +10433,9 @@ function runCronChecks(cronContent, cronJobsPath) {
|
|
|
10300
10433
|
);
|
|
10301
10434
|
return results;
|
|
10302
10435
|
}
|
|
10303
|
-
if (cronJobsPath &&
|
|
10436
|
+
if (cronJobsPath && fs8.existsSync(cronJobsPath)) {
|
|
10304
10437
|
try {
|
|
10305
|
-
const jobsRaw =
|
|
10438
|
+
const jobsRaw = fs8.readFileSync(cronJobsPath, "utf-8");
|
|
10306
10439
|
const jobs = JSON.parse(jobsRaw);
|
|
10307
10440
|
const knownJobIds = new Set(Object.keys(jobs));
|
|
10308
10441
|
for (const line of lines) {
|
|
@@ -10404,9 +10537,9 @@ function runSpamControlCheck(entry) {
|
|
|
10404
10537
|
}
|
|
10405
10538
|
function runAllChecksForTopic(entry, projectsBase, includeContent, registry, cronJobsPath) {
|
|
10406
10539
|
const results = [];
|
|
10407
|
-
const capsuleDir =
|
|
10540
|
+
const capsuleDir = path8.join(projectsBase, entry.slug);
|
|
10408
10541
|
results.push(...runRegistryChecks(entry, projectsBase));
|
|
10409
|
-
if (!
|
|
10542
|
+
if (!fs8.existsSync(capsuleDir)) return results;
|
|
10410
10543
|
results.push(...runCapsuleChecks(entry, projectsBase));
|
|
10411
10544
|
const capsuleFiles = readCapsuleFiles(capsuleDir);
|
|
10412
10545
|
const statusContent = capsuleFiles.get("STATUS.md");
|
|
@@ -10436,16 +10569,16 @@ var BACKUP_FILES = ["STATUS.md", "TODO.md"];
|
|
|
10436
10569
|
function backupCapsuleIfHealthy(projectsBase, slug, results) {
|
|
10437
10570
|
const hasIssues = results.some((r) => r.severity === Severity.ERROR || r.severity === Severity.WARN);
|
|
10438
10571
|
if (hasIssues) return;
|
|
10439
|
-
const capsuleDir =
|
|
10440
|
-
const backupDir =
|
|
10441
|
-
if (!
|
|
10442
|
-
|
|
10572
|
+
const capsuleDir = path8.join(projectsBase, slug);
|
|
10573
|
+
const backupDir = path8.join(capsuleDir, BACKUP_DIR);
|
|
10574
|
+
if (!fs8.existsSync(backupDir)) {
|
|
10575
|
+
fs8.mkdirSync(backupDir, { recursive: true });
|
|
10443
10576
|
}
|
|
10444
10577
|
for (const file of BACKUP_FILES) {
|
|
10445
|
-
const src =
|
|
10446
|
-
const dst =
|
|
10447
|
-
if (
|
|
10448
|
-
|
|
10578
|
+
const src = path8.join(capsuleDir, file);
|
|
10579
|
+
const dst = path8.join(backupDir, file);
|
|
10580
|
+
if (fs8.existsSync(src)) {
|
|
10581
|
+
fs8.copyFileSync(src, dst);
|
|
10449
10582
|
}
|
|
10450
10583
|
}
|
|
10451
10584
|
}
|
|
@@ -10468,10 +10601,10 @@ function readCapsuleFiles(capsuleDir) {
|
|
|
10468
10601
|
"METRICS.md"
|
|
10469
10602
|
];
|
|
10470
10603
|
for (const name of filenames) {
|
|
10471
|
-
const filePath =
|
|
10604
|
+
const filePath = path8.join(capsuleDir, name);
|
|
10472
10605
|
try {
|
|
10473
|
-
if (
|
|
10474
|
-
files.set(name,
|
|
10606
|
+
if (fs8.existsSync(filePath)) {
|
|
10607
|
+
files.set(name, fs8.readFileSync(filePath, "utf-8"));
|
|
10475
10608
|
}
|
|
10476
10609
|
} catch {
|
|
10477
10610
|
}
|
|
@@ -10495,23 +10628,23 @@ async function handleDoctor(ctx) {
|
|
|
10495
10628
|
if (!entry) {
|
|
10496
10629
|
return { text: "This topic is not registered. Run /tm init first." };
|
|
10497
10630
|
}
|
|
10498
|
-
const projectsBase =
|
|
10631
|
+
const projectsBase = path9.join(workspaceDir, "projects");
|
|
10499
10632
|
if (!jailCheck(projectsBase, entry.slug)) {
|
|
10500
10633
|
return { text: "Something went wrong \u2014 path validation failed." };
|
|
10501
10634
|
}
|
|
10502
|
-
const capsuleDir =
|
|
10635
|
+
const capsuleDir = path9.join(projectsBase, entry.slug);
|
|
10503
10636
|
if (rejectSymlink(capsuleDir)) {
|
|
10504
10637
|
return { text: "Something went wrong \u2014 detected an unsafe file system configuration." };
|
|
10505
10638
|
}
|
|
10506
10639
|
let includeContent;
|
|
10507
10640
|
const incPath = includePath(configDir);
|
|
10508
10641
|
try {
|
|
10509
|
-
if (
|
|
10510
|
-
includeContent =
|
|
10642
|
+
if (fs9.existsSync(incPath)) {
|
|
10643
|
+
includeContent = fs9.readFileSync(incPath, "utf-8");
|
|
10511
10644
|
}
|
|
10512
10645
|
} catch {
|
|
10513
10646
|
}
|
|
10514
|
-
const cronJobsPath =
|
|
10647
|
+
const cronJobsPath = path9.join(configDir, "cron", "jobs.json");
|
|
10515
10648
|
const results = runAllChecksForTopic(
|
|
10516
10649
|
entry,
|
|
10517
10650
|
projectsBase,
|
|
@@ -10540,95 +10673,232 @@ async function handleDoctor(ctx) {
|
|
|
10540
10673
|
}
|
|
10541
10674
|
|
|
10542
10675
|
// src/commands/doctor-all.ts
|
|
10543
|
-
import * as
|
|
10544
|
-
import * as
|
|
10545
|
-
|
|
10546
|
-
|
|
10547
|
-
|
|
10548
|
-
|
|
10676
|
+
import * as fs11 from "node:fs";
|
|
10677
|
+
import * as path11 from "node:path";
|
|
10678
|
+
|
|
10679
|
+
// src/commands/daily-report.ts
|
|
10680
|
+
import * as fs10 from "node:fs";
|
|
10681
|
+
import * as path10 from "node:path";
|
|
10682
|
+
async function handleDailyReport(ctx) {
|
|
10683
|
+
const { workspaceDir, groupId, threadId, logger } = ctx;
|
|
10684
|
+
if (!groupId || !threadId) {
|
|
10685
|
+
return { text: "Missing context: must be called from a topic thread." };
|
|
10549
10686
|
}
|
|
10687
|
+
const key = topicKey(groupId, threadId);
|
|
10550
10688
|
const registry = readRegistry(workspaceDir);
|
|
10551
|
-
const
|
|
10552
|
-
if (!
|
|
10553
|
-
return { text:
|
|
10689
|
+
const entry = registry.topics[key];
|
|
10690
|
+
if (!entry) {
|
|
10691
|
+
return { text: "This topic is not registered. Run /tm init first." };
|
|
10554
10692
|
}
|
|
10555
|
-
if (
|
|
10556
|
-
const
|
|
10557
|
-
const
|
|
10558
|
-
if (
|
|
10559
|
-
|
|
10560
|
-
return {
|
|
10561
|
-
text: `Health checks were run ${Math.floor(elapsed / 6e4)} minutes ago. Try again in ${remainingMin} minute(s).`
|
|
10562
|
-
};
|
|
10693
|
+
if (entry.lastDailyReportAt) {
|
|
10694
|
+
const lastReport = new Date(entry.lastDailyReportAt);
|
|
10695
|
+
const now = /* @__PURE__ */ new Date();
|
|
10696
|
+
if (lastReport.getUTCFullYear() === now.getUTCFullYear() && lastReport.getUTCMonth() === now.getUTCMonth() && lastReport.getUTCDate() === now.getUTCDate()) {
|
|
10697
|
+
return { text: "Daily report already generated today. Try again tomorrow." };
|
|
10563
10698
|
}
|
|
10564
10699
|
}
|
|
10565
|
-
const
|
|
10566
|
-
const
|
|
10567
|
-
|
|
10568
|
-
|
|
10569
|
-
try {
|
|
10570
|
-
if (fs9.existsSync(incPath)) {
|
|
10571
|
-
includeContent = fs9.readFileSync(incPath, "utf-8");
|
|
10572
|
-
}
|
|
10573
|
-
} catch {
|
|
10700
|
+
const projectsBase = path10.join(workspaceDir, "projects");
|
|
10701
|
+
const capsuleDir = path10.join(projectsBase, entry.slug);
|
|
10702
|
+
if (!fs10.existsSync(capsuleDir)) {
|
|
10703
|
+
return { text: "Topic files not found. Run /tm init to set up this topic." };
|
|
10574
10704
|
}
|
|
10575
|
-
const
|
|
10576
|
-
const
|
|
10577
|
-
const
|
|
10578
|
-
const
|
|
10579
|
-
|
|
10580
|
-
|
|
10581
|
-
const
|
|
10582
|
-
|
|
10583
|
-
|
|
10584
|
-
|
|
10585
|
-
|
|
10586
|
-
|
|
10705
|
+
const statusContent = readFileOrNull(path10.join(capsuleDir, "STATUS.md"));
|
|
10706
|
+
const todoContent = readFileOrNull(path10.join(capsuleDir, "TODO.md"));
|
|
10707
|
+
const learningsContent = readFileOrNull(path10.join(capsuleDir, "LEARNINGS.md"));
|
|
10708
|
+
const doneContent = extractDoneSection(statusContent);
|
|
10709
|
+
const newLearnings = extractTodayLearnings(learningsContent);
|
|
10710
|
+
const blockers = extractBlockers(todoContent);
|
|
10711
|
+
const nextContent = extractNextActions(statusContent);
|
|
10712
|
+
const upcomingContent = extractUpcoming(statusContent);
|
|
10713
|
+
const health = computeHealth(entry.lastMessageAt, statusContent, blockers);
|
|
10714
|
+
const reportData = {
|
|
10715
|
+
name: entry.name,
|
|
10716
|
+
doneContent,
|
|
10717
|
+
learningsContent: newLearnings,
|
|
10718
|
+
blockersContent: blockers,
|
|
10719
|
+
nextContent,
|
|
10720
|
+
upcomingContent,
|
|
10721
|
+
health
|
|
10722
|
+
};
|
|
10723
|
+
if (ctx.postFn) {
|
|
10587
10724
|
try {
|
|
10588
|
-
const
|
|
10589
|
-
|
|
10590
|
-
|
|
10591
|
-
|
|
10592
|
-
|
|
10593
|
-
|
|
10594
|
-
|
|
10595
|
-
const isSpam = entry.consecutiveSilentDoctors >= SPAM_THRESHOLD;
|
|
10596
|
-
if (isSpam) {
|
|
10597
|
-
logger.info(`[doctor-all] Auto-snoozing ${entry.slug} (${entry.consecutiveSilentDoctors} silent runs)`);
|
|
10598
|
-
}
|
|
10599
|
-
backupCapsuleIfHealthy(projectsBase, entry.slug, results);
|
|
10600
|
-
const reportText = buildDoctorReport(entry.name, results, "html");
|
|
10601
|
-
const keyboard = buildDoctorButtons(
|
|
10602
|
-
entry.groupId,
|
|
10603
|
-
entry.threadId,
|
|
10604
|
-
registry.callbackSecret,
|
|
10605
|
-
userId
|
|
10606
|
-
);
|
|
10607
|
-
reports.push({
|
|
10608
|
-
slug: entry.slug,
|
|
10609
|
-
groupId: entry.groupId,
|
|
10610
|
-
threadId: entry.threadId,
|
|
10611
|
-
text: reportText,
|
|
10612
|
-
keyboard
|
|
10725
|
+
const htmlReport = buildDailyReport(reportData, "html");
|
|
10726
|
+
await ctx.postFn(groupId, threadId, htmlReport);
|
|
10727
|
+
await withRegistry(workspaceDir, (data) => {
|
|
10728
|
+
const e = data.topics[key];
|
|
10729
|
+
if (e) {
|
|
10730
|
+
e.lastDailyReportAt = (/* @__PURE__ */ new Date()).toISOString();
|
|
10731
|
+
}
|
|
10613
10732
|
});
|
|
10614
|
-
const gk = entry.groupId;
|
|
10615
|
-
if (!groupPostResults.has(gk)) {
|
|
10616
|
-
groupPostResults.set(gk, { total: 0, failed: 0 });
|
|
10617
|
-
}
|
|
10618
|
-
const group = groupPostResults.get(gk);
|
|
10619
|
-
group.total++;
|
|
10620
|
-
processed++;
|
|
10621
10733
|
} catch (err) {
|
|
10622
10734
|
const msg = err instanceof Error ? err.message : String(err);
|
|
10623
|
-
|
|
10624
|
-
|
|
10625
|
-
|
|
10626
|
-
|
|
10627
|
-
|
|
10628
|
-
|
|
10629
|
-
|
|
10630
|
-
|
|
10631
|
-
|
|
10735
|
+
logger.error(`[daily-report] Post failed: ${msg}`);
|
|
10736
|
+
return { text: `Daily report generated but post failed: ${msg}` };
|
|
10737
|
+
}
|
|
10738
|
+
} else {
|
|
10739
|
+
await withRegistry(workspaceDir, (data) => {
|
|
10740
|
+
const e = data.topics[key];
|
|
10741
|
+
if (e) {
|
|
10742
|
+
e.lastDailyReportAt = (/* @__PURE__ */ new Date()).toISOString();
|
|
10743
|
+
}
|
|
10744
|
+
});
|
|
10745
|
+
}
|
|
10746
|
+
return { text: buildDailyReport(reportData, "markdown") };
|
|
10747
|
+
}
|
|
10748
|
+
function readFileOrNull(filePath) {
|
|
10749
|
+
try {
|
|
10750
|
+
return fs10.readFileSync(filePath, "utf-8");
|
|
10751
|
+
} catch {
|
|
10752
|
+
return null;
|
|
10753
|
+
}
|
|
10754
|
+
}
|
|
10755
|
+
function extractDoneSection(statusContent) {
|
|
10756
|
+
if (!statusContent) return "_No STATUS.md found._";
|
|
10757
|
+
const match = statusContent.match(/^##\s*Last done\s*\(UTC\)\s*\n([\s\S]*?)(?=\n##\s|\n*$)/im);
|
|
10758
|
+
if (!match) return '_No "Last done" section found._';
|
|
10759
|
+
const text = match[1]?.trim();
|
|
10760
|
+
return text || "_Empty._";
|
|
10761
|
+
}
|
|
10762
|
+
function extractTodayLearnings(learningsContent) {
|
|
10763
|
+
if (!learningsContent) return "_No LEARNINGS.md found._";
|
|
10764
|
+
const today = (/* @__PURE__ */ new Date()).toISOString().slice(0, 10);
|
|
10765
|
+
const lines = learningsContent.split("\n");
|
|
10766
|
+
const todayLines = [];
|
|
10767
|
+
let inTodaySection = false;
|
|
10768
|
+
for (const line of lines) {
|
|
10769
|
+
if (line.startsWith("## ") && line.includes(today)) {
|
|
10770
|
+
inTodaySection = true;
|
|
10771
|
+
continue;
|
|
10772
|
+
}
|
|
10773
|
+
if (inTodaySection && line.startsWith("## ")) {
|
|
10774
|
+
break;
|
|
10775
|
+
}
|
|
10776
|
+
if (inTodaySection && line.trim()) {
|
|
10777
|
+
todayLines.push(line);
|
|
10778
|
+
}
|
|
10779
|
+
}
|
|
10780
|
+
return todayLines.length > 0 ? todayLines.join("\n") : "_None today._";
|
|
10781
|
+
}
|
|
10782
|
+
function extractBlockers(todoContent) {
|
|
10783
|
+
if (!todoContent) return "_No TODO.md found._";
|
|
10784
|
+
const lines = todoContent.split("\n");
|
|
10785
|
+
const blockerLines = lines.filter(
|
|
10786
|
+
(l) => /\[BLOCKED\]/i.test(l) || /\bblocked\b/i.test(l)
|
|
10787
|
+
);
|
|
10788
|
+
return blockerLines.length > 0 ? blockerLines.join("\n") : "_None._";
|
|
10789
|
+
}
|
|
10790
|
+
function extractNextActions(statusContent) {
|
|
10791
|
+
if (!statusContent) return "_No STATUS.md found._";
|
|
10792
|
+
const match = statusContent.match(/^##\s*Next (?:3 )?actions(?: \(now\))?\s*\n([\s\S]*?)(?=\n##\s|\n*$)/im);
|
|
10793
|
+
if (!match) return '_No "Next actions" section found._';
|
|
10794
|
+
const text = match[1]?.trim();
|
|
10795
|
+
return text || "_Empty._";
|
|
10796
|
+
}
|
|
10797
|
+
function extractUpcoming(statusContent) {
|
|
10798
|
+
if (!statusContent) return "_No STATUS.md found._";
|
|
10799
|
+
const match = statusContent.match(/^##\s*Upcoming actions\s*\n([\s\S]*?)(?=\n##\s|\n*$)/im);
|
|
10800
|
+
if (!match) return '_No "Upcoming actions" section found._';
|
|
10801
|
+
const text = match[1]?.trim();
|
|
10802
|
+
return text || "_Empty._";
|
|
10803
|
+
}
|
|
10804
|
+
function computeHealth(lastMessageAt, statusContent, blockers) {
|
|
10805
|
+
if (blockers && blockers !== "_None._" && blockers !== "_No TODO.md found._") {
|
|
10806
|
+
return "blocked";
|
|
10807
|
+
}
|
|
10808
|
+
if (!lastMessageAt) return "stale";
|
|
10809
|
+
const hoursSinceActivity = (Date.now() - new Date(lastMessageAt).getTime()) / 36e5;
|
|
10810
|
+
if (hoursSinceActivity > 72) return "stale";
|
|
10811
|
+
return "fresh";
|
|
10812
|
+
}
|
|
10813
|
+
|
|
10814
|
+
// src/commands/doctor-all.ts
|
|
10815
|
+
async function handleDoctorAll(ctx) {
|
|
10816
|
+
const { workspaceDir, configDir, userId, logger } = ctx;
|
|
10817
|
+
if (!userId) {
|
|
10818
|
+
return { text: "Missing context: userId not available." };
|
|
10819
|
+
}
|
|
10820
|
+
const registry = readRegistry(workspaceDir);
|
|
10821
|
+
const auth = checkAuthorization(userId, "doctor-all", registry);
|
|
10822
|
+
if (!auth.authorized) {
|
|
10823
|
+
return { text: auth.message ?? "Not authorized." };
|
|
10824
|
+
}
|
|
10825
|
+
if (registry.lastDoctorAllRunAt) {
|
|
10826
|
+
const lastRun = new Date(registry.lastDoctorAllRunAt).getTime();
|
|
10827
|
+
const elapsed = Date.now() - lastRun;
|
|
10828
|
+
if (elapsed < DOCTOR_ALL_COOLDOWN_MS) {
|
|
10829
|
+
const remainingMin = Math.ceil((DOCTOR_ALL_COOLDOWN_MS - elapsed) / 6e4);
|
|
10830
|
+
return {
|
|
10831
|
+
text: `Health checks were run ${Math.floor(elapsed / 6e4)} minutes ago. Try again in ${remainingMin} minute(s).`
|
|
10832
|
+
};
|
|
10833
|
+
}
|
|
10834
|
+
}
|
|
10835
|
+
const now = /* @__PURE__ */ new Date();
|
|
10836
|
+
const projectsBase = path11.join(workspaceDir, "projects");
|
|
10837
|
+
let includeContent;
|
|
10838
|
+
const incPath = includePath(configDir);
|
|
10839
|
+
try {
|
|
10840
|
+
if (fs11.existsSync(incPath)) {
|
|
10841
|
+
includeContent = fs11.readFileSync(incPath, "utf-8");
|
|
10842
|
+
}
|
|
10843
|
+
} catch {
|
|
10844
|
+
}
|
|
10845
|
+
const cronJobsPath = path11.join(configDir, "cron", "jobs.json");
|
|
10846
|
+
const allEntries = Object.entries(registry.topics);
|
|
10847
|
+
const reports = [];
|
|
10848
|
+
const errors = [];
|
|
10849
|
+
let processed = 0;
|
|
10850
|
+
let skipped = 0;
|
|
10851
|
+
const groupPostResults = /* @__PURE__ */ new Map();
|
|
10852
|
+
for (const [_key, entry] of allEntries) {
|
|
10853
|
+
if (!isEligible(entry, now)) {
|
|
10854
|
+
skipped++;
|
|
10855
|
+
continue;
|
|
10856
|
+
}
|
|
10857
|
+
try {
|
|
10858
|
+
const results = runAllChecksForTopic(
|
|
10859
|
+
entry,
|
|
10860
|
+
projectsBase,
|
|
10861
|
+
includeContent,
|
|
10862
|
+
registry,
|
|
10863
|
+
cronJobsPath
|
|
10864
|
+
);
|
|
10865
|
+
const isSpam = entry.consecutiveSilentDoctors >= SPAM_THRESHOLD;
|
|
10866
|
+
if (isSpam) {
|
|
10867
|
+
logger.info(`[doctor-all] Auto-snoozing ${entry.slug} (${entry.consecutiveSilentDoctors} silent runs)`);
|
|
10868
|
+
}
|
|
10869
|
+
backupCapsuleIfHealthy(projectsBase, entry.slug, results);
|
|
10870
|
+
const reportText = buildDoctorReport(entry.name, results, "html");
|
|
10871
|
+
const keyboard = buildDoctorButtons(
|
|
10872
|
+
entry.groupId,
|
|
10873
|
+
entry.threadId,
|
|
10874
|
+
registry.callbackSecret,
|
|
10875
|
+
userId
|
|
10876
|
+
);
|
|
10877
|
+
reports.push({
|
|
10878
|
+
slug: entry.slug,
|
|
10879
|
+
groupId: entry.groupId,
|
|
10880
|
+
threadId: entry.threadId,
|
|
10881
|
+
text: reportText,
|
|
10882
|
+
keyboard
|
|
10883
|
+
});
|
|
10884
|
+
const gk = entry.groupId;
|
|
10885
|
+
if (!groupPostResults.has(gk)) {
|
|
10886
|
+
groupPostResults.set(gk, { total: 0, failed: 0 });
|
|
10887
|
+
}
|
|
10888
|
+
const group = groupPostResults.get(gk);
|
|
10889
|
+
group.total++;
|
|
10890
|
+
processed++;
|
|
10891
|
+
} catch (err) {
|
|
10892
|
+
const msg = err instanceof Error ? err.message : String(err);
|
|
10893
|
+
errors.push(`${entry.slug}: ${msg}`);
|
|
10894
|
+
logger.error(`[doctor-all] Error processing ${entry.slug}: ${msg}`);
|
|
10895
|
+
const gk = entry.groupId;
|
|
10896
|
+
if (!groupPostResults.has(gk)) {
|
|
10897
|
+
groupPostResults.set(gk, { total: 0, failed: 0 });
|
|
10898
|
+
}
|
|
10899
|
+
const group = groupPostResults.get(gk);
|
|
10900
|
+
group.total++;
|
|
10901
|
+
group.failed++;
|
|
10632
10902
|
}
|
|
10633
10903
|
}
|
|
10634
10904
|
const migrationGroups = [];
|
|
@@ -10667,6 +10937,54 @@ async function handleDoctorAll(ctx) {
|
|
|
10667
10937
|
}
|
|
10668
10938
|
}
|
|
10669
10939
|
}
|
|
10940
|
+
let dailyReportSuccesses = 0;
|
|
10941
|
+
let dailyReportSkipped = 0;
|
|
10942
|
+
const dailyReportKeys = /* @__PURE__ */ new Set();
|
|
10943
|
+
if (ctx.postFn && reports.length > 0) {
|
|
10944
|
+
const rateLimitedPost = createRateLimitedPoster(ctx.postFn);
|
|
10945
|
+
const nowDate = now.toISOString().slice(0, 10);
|
|
10946
|
+
for (const report of reports) {
|
|
10947
|
+
const key = `${report.groupId}:${report.threadId}`;
|
|
10948
|
+
const entry = registry.topics[key];
|
|
10949
|
+
if (!entry) continue;
|
|
10950
|
+
if (entry.lastDailyReportAt) {
|
|
10951
|
+
const lastReport = new Date(entry.lastDailyReportAt);
|
|
10952
|
+
const lastDate = `${lastReport.getUTCFullYear()}-${String(lastReport.getUTCMonth() + 1).padStart(2, "0")}-${String(lastReport.getUTCDate()).padStart(2, "0")}`;
|
|
10953
|
+
if (lastDate === nowDate) {
|
|
10954
|
+
dailyReportSkipped++;
|
|
10955
|
+
continue;
|
|
10956
|
+
}
|
|
10957
|
+
}
|
|
10958
|
+
const capsuleDir = path11.join(projectsBase, entry.slug);
|
|
10959
|
+
const statusContent = readFileOrNull(path11.join(capsuleDir, "STATUS.md"));
|
|
10960
|
+
const todoContent = readFileOrNull(path11.join(capsuleDir, "TODO.md"));
|
|
10961
|
+
const learningsContent = readFileOrNull(path11.join(capsuleDir, "LEARNINGS.md"));
|
|
10962
|
+
const doneContent = extractDoneSection(statusContent);
|
|
10963
|
+
const newLearnings = extractTodayLearnings(learningsContent);
|
|
10964
|
+
const blockers = extractBlockers(todoContent);
|
|
10965
|
+
const nextContent = extractNextActions(statusContent);
|
|
10966
|
+
const upcomingContent = extractUpcoming(statusContent);
|
|
10967
|
+
const health = computeHealth(entry.lastMessageAt, statusContent, blockers);
|
|
10968
|
+
const reportData = {
|
|
10969
|
+
name: entry.name,
|
|
10970
|
+
doneContent,
|
|
10971
|
+
learningsContent: newLearnings,
|
|
10972
|
+
blockersContent: blockers,
|
|
10973
|
+
nextContent,
|
|
10974
|
+
upcomingContent,
|
|
10975
|
+
health
|
|
10976
|
+
};
|
|
10977
|
+
try {
|
|
10978
|
+
const htmlReport = buildDailyReport(reportData, "html");
|
|
10979
|
+
await rateLimitedPost(report.groupId, report.threadId, htmlReport);
|
|
10980
|
+
dailyReportSuccesses++;
|
|
10981
|
+
dailyReportKeys.add(key);
|
|
10982
|
+
} catch (err) {
|
|
10983
|
+
const msg = err instanceof Error ? err.message : String(err);
|
|
10984
|
+
logger.error(`[doctor-all] Daily report post failed for ${entry.slug}: ${msg}`);
|
|
10985
|
+
}
|
|
10986
|
+
}
|
|
10987
|
+
}
|
|
10670
10988
|
await withRegistry(workspaceDir, (data) => {
|
|
10671
10989
|
data.lastDoctorAllRunAt = now.toISOString();
|
|
10672
10990
|
for (const [_key, entry] of Object.entries(data.topics)) {
|
|
@@ -10689,6 +11007,12 @@ async function handleDoctorAll(ctx) {
|
|
|
10689
11007
|
entry.consecutiveSilentDoctors = 0;
|
|
10690
11008
|
}
|
|
10691
11009
|
}
|
|
11010
|
+
for (const key of dailyReportKeys) {
|
|
11011
|
+
const entry = data.topics[key];
|
|
11012
|
+
if (entry) {
|
|
11013
|
+
entry.lastDailyReportAt = now.toISOString();
|
|
11014
|
+
}
|
|
11015
|
+
}
|
|
10692
11016
|
});
|
|
10693
11017
|
const lines = [
|
|
10694
11018
|
`**Health Check Summary**`,
|
|
@@ -10699,6 +11023,7 @@ async function handleDoctorAll(ctx) {
|
|
|
10699
11023
|
];
|
|
10700
11024
|
if (ctx.postFn) {
|
|
10701
11025
|
lines.push(`Posted: ${postSuccesses}, Post failures: ${postErrors}`);
|
|
11026
|
+
lines.push(`Daily reports: ${dailyReportSuccesses} sent, ${dailyReportSkipped} skipped`);
|
|
10702
11027
|
}
|
|
10703
11028
|
if (errors.length > 0) {
|
|
10704
11029
|
lines.push("");
|
|
@@ -10752,8 +11077,8 @@ async function handleList(ctx) {
|
|
|
10752
11077
|
}
|
|
10753
11078
|
|
|
10754
11079
|
// src/commands/status.ts
|
|
10755
|
-
import * as
|
|
10756
|
-
import * as
|
|
11080
|
+
import * as fs12 from "node:fs";
|
|
11081
|
+
import * as path12 from "node:path";
|
|
10757
11082
|
var LAST_DONE_RE2 = /^##\s*Last done\s*\(UTC\)\s*\n([\s\S]*?)(?=\n##\s|\n*$)/im;
|
|
10758
11083
|
var NEXT_ACTIONS_RE2 = /^##\s*Next (?:3 )?actions(?: \(now\))?\s*\n([\s\S]*?)(?=\n##\s|\n*$)/im;
|
|
10759
11084
|
var UPCOMING_RE = /^##\s*Upcoming actions\s*\n([\s\S]*?)(?=\n##\s|\n*$)/im;
|
|
@@ -10809,7 +11134,7 @@ function formatStatus(name, content) {
|
|
|
10809
11134
|
}
|
|
10810
11135
|
return lines.join("\n");
|
|
10811
11136
|
}
|
|
10812
|
-
async function
|
|
11137
|
+
async function handleStatus2(ctx) {
|
|
10813
11138
|
const { workspaceDir, userId, groupId, threadId } = ctx;
|
|
10814
11139
|
if (!userId || !groupId || !threadId) {
|
|
10815
11140
|
return { text: "Missing context: userId, groupId, or threadId not available." };
|
|
@@ -10824,20 +11149,20 @@ async function handleStatus(ctx) {
|
|
|
10824
11149
|
if (!entry) {
|
|
10825
11150
|
return { text: "This topic is not registered. Run /tm init first." };
|
|
10826
11151
|
}
|
|
10827
|
-
const projectsBase =
|
|
10828
|
-
const capsuleDir =
|
|
11152
|
+
const projectsBase = path12.join(workspaceDir, "projects");
|
|
11153
|
+
const capsuleDir = path12.join(projectsBase, entry.slug);
|
|
10829
11154
|
if (!jailCheck(projectsBase, entry.slug)) {
|
|
10830
11155
|
return { text: "Something went wrong \u2014 path validation failed." };
|
|
10831
11156
|
}
|
|
10832
11157
|
if (rejectSymlink(capsuleDir)) {
|
|
10833
11158
|
return { text: "Something went wrong \u2014 detected an unsafe file system configuration." };
|
|
10834
11159
|
}
|
|
10835
|
-
const statusPath =
|
|
10836
|
-
if (!
|
|
11160
|
+
const statusPath = path12.join(capsuleDir, "STATUS.md");
|
|
11161
|
+
if (!fs12.existsSync(statusPath)) {
|
|
10837
11162
|
return { text: "No status available yet. Run /tm doctor to diagnose." };
|
|
10838
11163
|
}
|
|
10839
11164
|
try {
|
|
10840
|
-
const content =
|
|
11165
|
+
const content = fs12.readFileSync(statusPath, "utf-8");
|
|
10841
11166
|
return {
|
|
10842
11167
|
text: truncateMessage(formatStatus(entry.name, content))
|
|
10843
11168
|
};
|
|
@@ -10936,7 +11261,7 @@ Warning: include generation failed: ${msg}`;
|
|
|
10936
11261
|
}
|
|
10937
11262
|
|
|
10938
11263
|
// src/commands/upgrade.ts
|
|
10939
|
-
import * as
|
|
11264
|
+
import * as path13 from "node:path";
|
|
10940
11265
|
async function handleUpgrade(ctx) {
|
|
10941
11266
|
const { workspaceDir, userId, groupId, threadId } = ctx;
|
|
10942
11267
|
if (!userId || !groupId || !threadId) {
|
|
@@ -10957,7 +11282,7 @@ async function handleUpgrade(ctx) {
|
|
|
10957
11282
|
text: `Topic **${entry.name}** is already up to date. No upgrade needed.`
|
|
10958
11283
|
};
|
|
10959
11284
|
}
|
|
10960
|
-
const projectsBase =
|
|
11285
|
+
const projectsBase = path13.join(workspaceDir, "projects");
|
|
10961
11286
|
const result = upgradeCapsule(projectsBase, entry.slug, entry.name, entry.type, entry.capsuleVersion);
|
|
10962
11287
|
if (!result.upgraded) {
|
|
10963
11288
|
return {
|
|
@@ -11089,254 +11414,6 @@ Warning: include generation failed: ${msg}`;
|
|
|
11089
11414
|
};
|
|
11090
11415
|
}
|
|
11091
11416
|
|
|
11092
|
-
// src/commands/autopilot.ts
|
|
11093
|
-
import * as fs11 from "node:fs";
|
|
11094
|
-
import * as path12 from "node:path";
|
|
11095
|
-
var MARKER_START = "<!-- TM_AUTOPILOT_START -->";
|
|
11096
|
-
var MARKER_END = "<!-- TM_AUTOPILOT_END -->";
|
|
11097
|
-
var HEARTBEAT_BLOCK = `${MARKER_START}
|
|
11098
|
-
## Topic Manager \u2014 Balanced Autopilot
|
|
11099
|
-
|
|
11100
|
-
Execute these steps IN ORDER. Do not skip steps.
|
|
11101
|
-
|
|
11102
|
-
1. CALL \`topic_manager\` with command "doctor --all" to health-check all active topics.
|
|
11103
|
-
This handles stale timestamps, task ID mismatches, and posting errors automatically.
|
|
11104
|
-
2. For each topic where STATUS.md "Last done (UTC)" is >3 days old:
|
|
11105
|
-
post a brief nudge in that topic asking the user for a status update.
|
|
11106
|
-
3. If no actions were taken in steps 1-2, do nothing (HEARTBEAT_OK).
|
|
11107
|
-
${MARKER_END}`;
|
|
11108
|
-
var HEARTBEAT_FILENAME = "HEARTBEAT.md";
|
|
11109
|
-
async function handleAutopilot(ctx, args) {
|
|
11110
|
-
const { workspaceDir, userId } = ctx;
|
|
11111
|
-
if (!userId) {
|
|
11112
|
-
return { text: "Missing context: userId not available." };
|
|
11113
|
-
}
|
|
11114
|
-
const registry = readRegistry(workspaceDir);
|
|
11115
|
-
const auth = checkAuthorization(userId, "autopilot", registry);
|
|
11116
|
-
if (!auth.authorized) {
|
|
11117
|
-
return { text: auth.message ?? "Not authorized." };
|
|
11118
|
-
}
|
|
11119
|
-
const subCommand = args.trim().toLowerCase() || "enable";
|
|
11120
|
-
switch (subCommand) {
|
|
11121
|
-
case "enable":
|
|
11122
|
-
return handleEnable(ctx);
|
|
11123
|
-
case "disable":
|
|
11124
|
-
return handleDisable(ctx);
|
|
11125
|
-
case "status":
|
|
11126
|
-
return handleStatus2(ctx);
|
|
11127
|
-
default:
|
|
11128
|
-
return { text: `Unknown autopilot sub-command: "${subCommand}". Use enable, disable, or status.` };
|
|
11129
|
-
}
|
|
11130
|
-
}
|
|
11131
|
-
async function handleEnable(ctx) {
|
|
11132
|
-
const { workspaceDir } = ctx;
|
|
11133
|
-
const heartbeatPath = path12.join(workspaceDir, HEARTBEAT_FILENAME);
|
|
11134
|
-
let content = "";
|
|
11135
|
-
try {
|
|
11136
|
-
if (fs11.existsSync(heartbeatPath)) {
|
|
11137
|
-
content = fs11.readFileSync(heartbeatPath, "utf-8");
|
|
11138
|
-
}
|
|
11139
|
-
} catch {
|
|
11140
|
-
}
|
|
11141
|
-
if (content.includes(MARKER_START)) {
|
|
11142
|
-
await withRegistry(workspaceDir, (data) => {
|
|
11143
|
-
data.autopilotEnabled = true;
|
|
11144
|
-
});
|
|
11145
|
-
return { text: "Autopilot is already enabled." };
|
|
11146
|
-
}
|
|
11147
|
-
const newContent = content ? content.trimEnd() + "\n\n" + HEARTBEAT_BLOCK + "\n" : HEARTBEAT_BLOCK + "\n";
|
|
11148
|
-
fs11.writeFileSync(heartbeatPath, newContent, { mode: 416 });
|
|
11149
|
-
await withRegistry(workspaceDir, (data) => {
|
|
11150
|
-
data.autopilotEnabled = true;
|
|
11151
|
-
});
|
|
11152
|
-
return {
|
|
11153
|
-
text: "**Autopilot enabled.**\nHealth checks will run automatically every day."
|
|
11154
|
-
};
|
|
11155
|
-
}
|
|
11156
|
-
async function handleDisable(ctx) {
|
|
11157
|
-
const { workspaceDir } = ctx;
|
|
11158
|
-
const heartbeatPath = path12.join(workspaceDir, HEARTBEAT_FILENAME);
|
|
11159
|
-
if (!fs11.existsSync(heartbeatPath)) {
|
|
11160
|
-
await withRegistry(workspaceDir, (data) => {
|
|
11161
|
-
data.autopilotEnabled = false;
|
|
11162
|
-
});
|
|
11163
|
-
return { text: "Autopilot is already disabled." };
|
|
11164
|
-
}
|
|
11165
|
-
let content = fs11.readFileSync(heartbeatPath, "utf-8");
|
|
11166
|
-
if (!content.includes(MARKER_START)) {
|
|
11167
|
-
await withRegistry(workspaceDir, (data) => {
|
|
11168
|
-
data.autopilotEnabled = false;
|
|
11169
|
-
});
|
|
11170
|
-
return { text: "Autopilot is already disabled." };
|
|
11171
|
-
}
|
|
11172
|
-
const startIdx = content.indexOf(MARKER_START);
|
|
11173
|
-
const endIdx = content.indexOf(MARKER_END);
|
|
11174
|
-
if (startIdx >= 0 && endIdx >= 0) {
|
|
11175
|
-
const before = content.slice(0, startIdx);
|
|
11176
|
-
const after = content.slice(endIdx + MARKER_END.length);
|
|
11177
|
-
content = (before + after).replace(/\n{3,}/g, "\n\n").trim();
|
|
11178
|
-
if (content) {
|
|
11179
|
-
fs11.writeFileSync(heartbeatPath, content + "\n", { mode: 416 });
|
|
11180
|
-
} else {
|
|
11181
|
-
fs11.unlinkSync(heartbeatPath);
|
|
11182
|
-
}
|
|
11183
|
-
}
|
|
11184
|
-
await withRegistry(workspaceDir, (data) => {
|
|
11185
|
-
data.autopilotEnabled = false;
|
|
11186
|
-
});
|
|
11187
|
-
return {
|
|
11188
|
-
text: "**Autopilot disabled.**\nAutomatic health checks are now off."
|
|
11189
|
-
};
|
|
11190
|
-
}
|
|
11191
|
-
async function handleStatus2(ctx) {
|
|
11192
|
-
const { workspaceDir } = ctx;
|
|
11193
|
-
const registry = readRegistry(workspaceDir);
|
|
11194
|
-
const enabled = registry.autopilotEnabled;
|
|
11195
|
-
const lastRun = registry.lastDoctorAllRunAt ? relativeTime(registry.lastDoctorAllRunAt) : "never";
|
|
11196
|
-
const lines = [
|
|
11197
|
-
`**Autopilot:** ${enabled ? "enabled" : "disabled"}`,
|
|
11198
|
-
`**Last health check run:** ${lastRun}`
|
|
11199
|
-
];
|
|
11200
|
-
return {
|
|
11201
|
-
text: lines.join("\n")
|
|
11202
|
-
};
|
|
11203
|
-
}
|
|
11204
|
-
|
|
11205
|
-
// src/commands/daily-report.ts
|
|
11206
|
-
import * as fs12 from "node:fs";
|
|
11207
|
-
import * as path13 from "node:path";
|
|
11208
|
-
async function handleDailyReport(ctx) {
|
|
11209
|
-
const { workspaceDir, groupId, threadId, logger } = ctx;
|
|
11210
|
-
if (!groupId || !threadId) {
|
|
11211
|
-
return { text: "Missing context: must be called from a topic thread." };
|
|
11212
|
-
}
|
|
11213
|
-
const key = topicKey(groupId, threadId);
|
|
11214
|
-
const registry = readRegistry(workspaceDir);
|
|
11215
|
-
const entry = registry.topics[key];
|
|
11216
|
-
if (!entry) {
|
|
11217
|
-
return { text: "This topic is not registered. Run /tm init first." };
|
|
11218
|
-
}
|
|
11219
|
-
if (entry.lastDailyReportAt) {
|
|
11220
|
-
const lastReport = new Date(entry.lastDailyReportAt);
|
|
11221
|
-
const now = /* @__PURE__ */ new Date();
|
|
11222
|
-
if (lastReport.getUTCFullYear() === now.getUTCFullYear() && lastReport.getUTCMonth() === now.getUTCMonth() && lastReport.getUTCDate() === now.getUTCDate()) {
|
|
11223
|
-
return { text: "Daily report already generated today. Try again tomorrow." };
|
|
11224
|
-
}
|
|
11225
|
-
}
|
|
11226
|
-
const projectsBase = path13.join(workspaceDir, "projects");
|
|
11227
|
-
const capsuleDir = path13.join(projectsBase, entry.slug);
|
|
11228
|
-
if (!fs12.existsSync(capsuleDir)) {
|
|
11229
|
-
return { text: "Topic files not found. Run /tm init to set up this topic." };
|
|
11230
|
-
}
|
|
11231
|
-
const statusContent = readFileOrNull(path13.join(capsuleDir, "STATUS.md"));
|
|
11232
|
-
const todoContent = readFileOrNull(path13.join(capsuleDir, "TODO.md"));
|
|
11233
|
-
const learningsContent = readFileOrNull(path13.join(capsuleDir, "LEARNINGS.md"));
|
|
11234
|
-
const doneContent = extractDoneSection(statusContent);
|
|
11235
|
-
const newLearnings = extractTodayLearnings(learningsContent);
|
|
11236
|
-
const blockers = extractBlockers(todoContent);
|
|
11237
|
-
const nextContent = extractNextActions(statusContent);
|
|
11238
|
-
const upcomingContent = extractUpcoming(statusContent);
|
|
11239
|
-
const health = computeHealth(entry.lastMessageAt, statusContent, blockers);
|
|
11240
|
-
const reportData = {
|
|
11241
|
-
name: entry.name,
|
|
11242
|
-
doneContent,
|
|
11243
|
-
learningsContent: newLearnings,
|
|
11244
|
-
blockersContent: blockers,
|
|
11245
|
-
nextContent,
|
|
11246
|
-
upcomingContent,
|
|
11247
|
-
health
|
|
11248
|
-
};
|
|
11249
|
-
if (ctx.postFn) {
|
|
11250
|
-
try {
|
|
11251
|
-
const htmlReport = buildDailyReport(reportData, "html");
|
|
11252
|
-
await ctx.postFn(groupId, threadId, htmlReport);
|
|
11253
|
-
await withRegistry(workspaceDir, (data) => {
|
|
11254
|
-
const e = data.topics[key];
|
|
11255
|
-
if (e) {
|
|
11256
|
-
e.lastDailyReportAt = (/* @__PURE__ */ new Date()).toISOString();
|
|
11257
|
-
}
|
|
11258
|
-
});
|
|
11259
|
-
} catch (err) {
|
|
11260
|
-
const msg = err instanceof Error ? err.message : String(err);
|
|
11261
|
-
logger.error(`[daily-report] Post failed: ${msg}`);
|
|
11262
|
-
return { text: `Daily report generated but post failed: ${msg}` };
|
|
11263
|
-
}
|
|
11264
|
-
} else {
|
|
11265
|
-
await withRegistry(workspaceDir, (data) => {
|
|
11266
|
-
const e = data.topics[key];
|
|
11267
|
-
if (e) {
|
|
11268
|
-
e.lastDailyReportAt = (/* @__PURE__ */ new Date()).toISOString();
|
|
11269
|
-
}
|
|
11270
|
-
});
|
|
11271
|
-
}
|
|
11272
|
-
return { text: buildDailyReport(reportData, "markdown") };
|
|
11273
|
-
}
|
|
11274
|
-
function readFileOrNull(filePath) {
|
|
11275
|
-
try {
|
|
11276
|
-
return fs12.readFileSync(filePath, "utf-8");
|
|
11277
|
-
} catch {
|
|
11278
|
-
return null;
|
|
11279
|
-
}
|
|
11280
|
-
}
|
|
11281
|
-
function extractDoneSection(statusContent) {
|
|
11282
|
-
if (!statusContent) return "_No STATUS.md found._";
|
|
11283
|
-
const match = statusContent.match(/^##\s*Last done\s*\(UTC\)\s*\n([\s\S]*?)(?=\n##\s|\n*$)/im);
|
|
11284
|
-
if (!match) return '_No "Last done" section found._';
|
|
11285
|
-
const text = match[1]?.trim();
|
|
11286
|
-
return text || "_Empty._";
|
|
11287
|
-
}
|
|
11288
|
-
function extractTodayLearnings(learningsContent) {
|
|
11289
|
-
if (!learningsContent) return "_No LEARNINGS.md found._";
|
|
11290
|
-
const today = (/* @__PURE__ */ new Date()).toISOString().slice(0, 10);
|
|
11291
|
-
const lines = learningsContent.split("\n");
|
|
11292
|
-
const todayLines = [];
|
|
11293
|
-
let inTodaySection = false;
|
|
11294
|
-
for (const line of lines) {
|
|
11295
|
-
if (line.startsWith("## ") && line.includes(today)) {
|
|
11296
|
-
inTodaySection = true;
|
|
11297
|
-
continue;
|
|
11298
|
-
}
|
|
11299
|
-
if (inTodaySection && line.startsWith("## ")) {
|
|
11300
|
-
break;
|
|
11301
|
-
}
|
|
11302
|
-
if (inTodaySection && line.trim()) {
|
|
11303
|
-
todayLines.push(line);
|
|
11304
|
-
}
|
|
11305
|
-
}
|
|
11306
|
-
return todayLines.length > 0 ? todayLines.join("\n") : "_None today._";
|
|
11307
|
-
}
|
|
11308
|
-
function extractBlockers(todoContent) {
|
|
11309
|
-
if (!todoContent) return "_No TODO.md found._";
|
|
11310
|
-
const lines = todoContent.split("\n");
|
|
11311
|
-
const blockerLines = lines.filter(
|
|
11312
|
-
(l) => /\[BLOCKED\]/i.test(l) || /\bblocked\b/i.test(l)
|
|
11313
|
-
);
|
|
11314
|
-
return blockerLines.length > 0 ? blockerLines.join("\n") : "_None._";
|
|
11315
|
-
}
|
|
11316
|
-
function extractNextActions(statusContent) {
|
|
11317
|
-
if (!statusContent) return "_No STATUS.md found._";
|
|
11318
|
-
const match = statusContent.match(/^##\s*Next (?:3 )?actions(?: \(now\))?\s*\n([\s\S]*?)(?=\n##\s|\n*$)/im);
|
|
11319
|
-
if (!match) return '_No "Next actions" section found._';
|
|
11320
|
-
const text = match[1]?.trim();
|
|
11321
|
-
return text || "_Empty._";
|
|
11322
|
-
}
|
|
11323
|
-
function extractUpcoming(statusContent) {
|
|
11324
|
-
if (!statusContent) return "_No STATUS.md found._";
|
|
11325
|
-
const match = statusContent.match(/^##\s*Upcoming actions\s*\n([\s\S]*?)(?=\n##\s|\n*$)/im);
|
|
11326
|
-
if (!match) return '_No "Upcoming actions" section found._';
|
|
11327
|
-
const text = match[1]?.trim();
|
|
11328
|
-
return text || "_Empty._";
|
|
11329
|
-
}
|
|
11330
|
-
function computeHealth(lastMessageAt, statusContent, blockers) {
|
|
11331
|
-
if (blockers && blockers !== "_None._" && blockers !== "_No TODO.md found._") {
|
|
11332
|
-
return "blocked";
|
|
11333
|
-
}
|
|
11334
|
-
if (!lastMessageAt) return "stale";
|
|
11335
|
-
const hoursSinceActivity = (Date.now() - new Date(lastMessageAt).getTime()) / 36e5;
|
|
11336
|
-
if (hoursSinceActivity > 72) return "stale";
|
|
11337
|
-
return "fresh";
|
|
11338
|
-
}
|
|
11339
|
-
|
|
11340
11417
|
// src/commands/help.ts
|
|
11341
11418
|
function handleHelp(_ctx) {
|
|
11342
11419
|
return {
|
|
@@ -11395,7 +11472,7 @@ function createTopicManagerTool(deps) {
|
|
|
11395
11472
|
case "list":
|
|
11396
11473
|
return await handleList(ctx);
|
|
11397
11474
|
case "status":
|
|
11398
|
-
return await
|
|
11475
|
+
return await handleStatus2(ctx);
|
|
11399
11476
|
case "sync":
|
|
11400
11477
|
return await handleSync(ctx);
|
|
11401
11478
|
case "rename":
|