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/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 fs6 from "node:fs";
9276
- import * as path6 from "node:path";
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 = path6.join(workspaceDir, "projects");
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 (fs6.existsSync(path6.join(projectsBase, finalSlug))) {
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 = path6.join(projectsBase, finalSlug);
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 fs8 from "node:fs";
10113
- import * as path8 from "node:path";
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 fs7 from "node:fs";
10118
- import * as path7 from "node:path";
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 = path7.join(projectsBase, entry.slug);
10125
- if (!fs7.existsSync(capsuleDir)) {
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 = fs7.statSync(capsuleDir);
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 = path7.join(projectsBase, entry.slug);
10148
- if (!fs7.existsSync(capsuleDir)) return results;
10149
- if (!fs7.existsSync(path7.join(capsuleDir, "STATUS.md"))) {
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 (!fs7.existsSync(path7.join(capsuleDir, "TODO.md"))) {
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 (!fs7.existsSync(path7.join(capsuleDir, file))) {
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 && fs7.existsSync(cronJobsPath)) {
10436
+ if (cronJobsPath && fs8.existsSync(cronJobsPath)) {
10304
10437
  try {
10305
- const jobsRaw = fs7.readFileSync(cronJobsPath, "utf-8");
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 = path7.join(projectsBase, entry.slug);
10540
+ const capsuleDir = path8.join(projectsBase, entry.slug);
10408
10541
  results.push(...runRegistryChecks(entry, projectsBase));
10409
- if (!fs7.existsSync(capsuleDir)) return results;
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 = path7.join(projectsBase, slug);
10440
- const backupDir = path7.join(capsuleDir, BACKUP_DIR);
10441
- if (!fs7.existsSync(backupDir)) {
10442
- fs7.mkdirSync(backupDir, { recursive: true });
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 = path7.join(capsuleDir, file);
10446
- const dst = path7.join(backupDir, file);
10447
- if (fs7.existsSync(src)) {
10448
- fs7.copyFileSync(src, dst);
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 = path7.join(capsuleDir, name);
10604
+ const filePath = path8.join(capsuleDir, name);
10472
10605
  try {
10473
- if (fs7.existsSync(filePath)) {
10474
- files.set(name, fs7.readFileSync(filePath, "utf-8"));
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 = path8.join(workspaceDir, "projects");
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 = path8.join(projectsBase, entry.slug);
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 (fs8.existsSync(incPath)) {
10510
- includeContent = fs8.readFileSync(incPath, "utf-8");
10642
+ if (fs9.existsSync(incPath)) {
10643
+ includeContent = fs9.readFileSync(incPath, "utf-8");
10511
10644
  }
10512
10645
  } catch {
10513
10646
  }
10514
- const cronJobsPath = path8.join(configDir, "cron", "jobs.json");
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 fs9 from "node:fs";
10544
- import * as path9 from "node:path";
10545
- async function handleDoctorAll(ctx) {
10546
- const { workspaceDir, configDir, userId, logger } = ctx;
10547
- if (!userId) {
10548
- return { text: "Missing context: userId not available." };
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 auth = checkAuthorization(userId, "doctor-all", registry);
10552
- if (!auth.authorized) {
10553
- return { text: auth.message ?? "Not authorized." };
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 (registry.lastDoctorAllRunAt) {
10556
- const lastRun = new Date(registry.lastDoctorAllRunAt).getTime();
10557
- const elapsed = Date.now() - lastRun;
10558
- if (elapsed < DOCTOR_ALL_COOLDOWN_MS) {
10559
- const remainingMin = Math.ceil((DOCTOR_ALL_COOLDOWN_MS - elapsed) / 6e4);
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 now = /* @__PURE__ */ new Date();
10566
- const projectsBase = path9.join(workspaceDir, "projects");
10567
- let includeContent;
10568
- const incPath = includePath(configDir);
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 cronJobsPath = path9.join(configDir, "cron", "jobs.json");
10576
- const allEntries = Object.entries(registry.topics);
10577
- const reports = [];
10578
- const errors = [];
10579
- let processed = 0;
10580
- let skipped = 0;
10581
- const groupPostResults = /* @__PURE__ */ new Map();
10582
- for (const [_key, entry] of allEntries) {
10583
- if (!isEligible(entry, now)) {
10584
- skipped++;
10585
- continue;
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 results = runAllChecksForTopic(
10589
- entry,
10590
- projectsBase,
10591
- includeContent,
10592
- registry,
10593
- cronJobsPath
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
- errors.push(`${entry.slug}: ${msg}`);
10624
- logger.error(`[doctor-all] Error processing ${entry.slug}: ${msg}`);
10625
- const gk = entry.groupId;
10626
- if (!groupPostResults.has(gk)) {
10627
- groupPostResults.set(gk, { total: 0, failed: 0 });
10628
- }
10629
- const group = groupPostResults.get(gk);
10630
- group.total++;
10631
- group.failed++;
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 fs10 from "node:fs";
10756
- import * as path10 from "node:path";
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 handleStatus(ctx) {
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 = path10.join(workspaceDir, "projects");
10828
- const capsuleDir = path10.join(projectsBase, entry.slug);
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 = path10.join(capsuleDir, "STATUS.md");
10836
- if (!fs10.existsSync(statusPath)) {
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 = fs10.readFileSync(statusPath, "utf-8");
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 path11 from "node:path";
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 = path11.join(workspaceDir, "projects");
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 handleStatus(ctx);
11475
+ return await handleStatus2(ctx);
11399
11476
  case "sync":
11400
11477
  return await handleSync(ctx);
11401
11478
  case "rename":