openclaw-telegram-manager 2.4.0 → 2.5.1

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.
Files changed (47) hide show
  1. package/README.md +6 -5
  2. package/dist/commands/archive.js +2 -2
  3. package/dist/commands/archive.js.map +1 -1
  4. package/dist/commands/autopilot.d.ts +4 -0
  5. package/dist/commands/autopilot.d.ts.map +1 -1
  6. package/dist/commands/autopilot.js +6 -6
  7. package/dist/commands/autopilot.js.map +1 -1
  8. package/dist/commands/daily-report.d.ts +6 -0
  9. package/dist/commands/daily-report.d.ts.map +1 -1
  10. package/dist/commands/daily-report.js +13 -13
  11. package/dist/commands/daily-report.js.map +1 -1
  12. package/dist/commands/doctor-all.d.ts.map +1 -1
  13. package/dist/commands/doctor-all.js +67 -3
  14. package/dist/commands/doctor-all.js.map +1 -1
  15. package/dist/commands/doctor.js +1 -1
  16. package/dist/commands/doctor.js.map +1 -1
  17. package/dist/commands/init.d.ts.map +1 -1
  18. package/dist/commands/init.js +34 -6
  19. package/dist/commands/init.js.map +1 -1
  20. package/dist/commands/list.js +1 -1
  21. package/dist/commands/list.js.map +1 -1
  22. package/dist/commands/rename.js +2 -2
  23. package/dist/commands/rename.js.map +1 -1
  24. package/dist/commands/snooze.js +1 -1
  25. package/dist/commands/snooze.js.map +1 -1
  26. package/dist/commands/status.js +1 -1
  27. package/dist/commands/status.js.map +1 -1
  28. package/dist/commands/sync.js +1 -1
  29. package/dist/commands/sync.js.map +1 -1
  30. package/dist/commands/upgrade.d.ts.map +1 -1
  31. package/dist/commands/upgrade.js +6 -5
  32. package/dist/commands/upgrade.js.map +1 -1
  33. package/dist/index.js +2 -2
  34. package/dist/index.js.map +1 -1
  35. package/dist/lib/capsule.js +1 -1
  36. package/dist/lib/capsule.js.map +1 -1
  37. package/dist/lib/doctor-checks.js +1 -1
  38. package/dist/lib/doctor-checks.js.map +1 -1
  39. package/dist/lib/telegram.d.ts +2 -2
  40. package/dist/lib/telegram.d.ts.map +1 -1
  41. package/dist/lib/telegram.js +20 -31
  42. package/dist/lib/telegram.js.map +1 -1
  43. package/dist/plugin.js +450 -383
  44. package/dist/setup.js +46 -1
  45. package/dist/setup.js.map +1 -1
  46. package/package.json +1 -1
  47. package/skills/tm/SKILL.md +4 -4
package/dist/plugin.js CHANGED
@@ -9240,7 +9240,7 @@ function upgradeCapsule(projectsBase, slug, name, type, currentVersion) {
9240
9240
  throw new Error(`Path escapes projects directory: ${slug}`);
9241
9241
  }
9242
9242
  if (rejectSymlink(capsuleDir)) {
9243
- throw new Error(`Topic directory is a symlink: ${capsuleDir}`);
9243
+ throw new Error("Topic directory is a symlink \u2014 this is not allowed for security reasons.");
9244
9244
  }
9245
9245
  const addedFiles = [];
9246
9246
  for (const file of BASE_FILES) {
@@ -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 = {
@@ -9406,35 +9406,31 @@ function buildInitConfirmButton(groupId, threadId, secret, userId, type) {
9406
9406
  const cb = buildCallbackData(actionMap[type], groupId, threadId, secret, userId);
9407
9407
  return buildInlineKeyboard([[{ text: "Use this name", callback_data: cb }]]);
9408
9408
  }
9409
- function buildTopicCard(name, slug, type) {
9409
+ function buildTopicCard(name, type) {
9410
9410
  return [
9411
- `**Topic: ${name}**`,
9412
- `**Type:** ${type}`,
9413
- `**Stored in:** projects/${slug}/`,
9411
+ `**${name}** is ready!`,
9412
+ "",
9413
+ `Type: ${type}`,
9414
9414
  "",
9415
9415
  "**How it works**",
9416
- "Talk to the AI in this topic like you normally would \u2014 describe what you need, ask questions, or give instructions. Progress, TODOs, and decisions are tracked automatically so nothing is lost between sessions.",
9416
+ "Just talk to the AI in this topic like you normally would. Progress, TODOs, and decisions are tracked automatically so nothing is lost between sessions.",
9417
9417
  "",
9418
- "**Available commands:**",
9419
- "/tm status \u2014 see current progress",
9420
- "/tm doctor \u2014 run health checks",
9421
- "/tm rename new-name \u2014 rename this topic",
9422
- "/tm list \u2014 all topics",
9423
- "/tm archive \u2014 archive this topic",
9424
- "/tm help \u2014 full command reference"
9418
+ "Type /tm help if you ever need it."
9425
9419
  ].join("\n");
9426
9420
  }
9427
9421
  function buildInitWelcomeHtml() {
9428
9422
  return [
9429
- "<b>Set up a new topic workcell</b>",
9423
+ "<b>Set up this topic</b>",
9430
9424
  "",
9431
- "A workcell gives this topic a persistent memory \u2014 The AI writes status, TODOs, and notes to disk so context survives between sessions.",
9425
+ "The AI will remember everything across sessions \u2014 progress, decisions, TODOs, and notes are saved automatically.",
9432
9426
  "",
9433
9427
  "<b>Pick a type:</b>",
9434
- "\u2022 <b>Coding</b> \u2014 adds ARCHITECTURE.md + DEPLOY.md",
9435
- "\u2022 <b>Research</b> \u2014 adds SOURCES.md + FINDINGS.md",
9436
- "\u2022 <b>Marketing</b> \u2014 adds CAMPAIGNS.md + METRICS.md",
9437
- "\u2022 <b>Custom</b> \u2014 base files only"
9428
+ "\u2022 <b>Coding</b> \u2014 tracks architecture decisions and deployment steps",
9429
+ "\u2022 <b>Research</b> \u2014 tracks sources and key findings",
9430
+ "\u2022 <b>Marketing</b> \u2014 tracks campaigns and metrics",
9431
+ "\u2022 <b>Custom</b> \u2014 general-purpose tracking",
9432
+ "",
9433
+ "<i>The AI may take a few seconds to respond \u2014 no need to tap twice.</i>"
9438
9434
  ].join("\n");
9439
9435
  }
9440
9436
  function buildInitNameConfirmHtml(name, type) {
@@ -9451,25 +9447,18 @@ function buildInitNameConfirmHtml(name, type) {
9451
9447
  `For a custom name: <code>/tm init your-name ${t}</code>`
9452
9448
  ].join("\n");
9453
9449
  }
9454
- function buildTopicCardHtml(name, slug, type) {
9450
+ function buildTopicCardHtml(name, type) {
9455
9451
  const n = htmlEscape(name);
9456
- const s = htmlEscape(slug);
9457
9452
  const t = htmlEscape(type);
9458
9453
  return [
9459
- `<b>Topic: ${n}</b>`,
9460
- `<b>Type:</b> ${t}`,
9461
- `<b>Stored in:</b> <code>projects/${s}/</code>`,
9454
+ `<b>\u2705 ${n}</b> is ready!`,
9455
+ "",
9456
+ `Type: ${t}`,
9462
9457
  "",
9463
9458
  "<b>How it works</b>",
9464
- "Talk to the AI in this topic like you normally would \u2014 describe what you need, ask questions, or give instructions. Progress, TODOs, and decisions are tracked automatically so nothing is lost between sessions.",
9459
+ "Just talk to the AI in this topic like you normally would. Progress, TODOs, and decisions are tracked automatically so nothing is lost between sessions.",
9465
9460
  "",
9466
- "<b>Available commands:</b>",
9467
- "/tm status \u2014 see current progress",
9468
- "/tm doctor \u2014 run health checks",
9469
- "/tm rename new-name \u2014 rename this topic",
9470
- "/tm list \u2014 all topics",
9471
- "/tm archive \u2014 archive this topic",
9472
- "/tm help \u2014 full command reference"
9461
+ "Type /tm help if you ever need it."
9473
9462
  ].join("\n");
9474
9463
  }
9475
9464
  function formatCommands(text, isHtml) {
@@ -9871,6 +9860,119 @@ async function getConfigWrites(rpc) {
9871
9860
  }
9872
9861
  }
9873
9862
 
9863
+ // src/commands/autopilot.ts
9864
+ import * as fs6 from "node:fs";
9865
+ import * as path6 from "node:path";
9866
+ var MARKER_START = "<!-- TM_AUTOPILOT_START -->";
9867
+ var MARKER_END = "<!-- TM_AUTOPILOT_END -->";
9868
+ var HEARTBEAT_BLOCK = `${MARKER_START}
9869
+ ## Topic Manager \u2014 Balanced Autopilot
9870
+
9871
+ Execute these steps IN ORDER. Do not skip steps.
9872
+
9873
+ 1. CALL \`topic_manager\` with command "doctor --all" to health-check all active topics.
9874
+ This runs health checks AND posts daily progress reports for each eligible topic automatically.
9875
+ 2. For each topic where STATUS.md "Last done (UTC)" is >3 days old:
9876
+ post a brief nudge in that topic asking the user for a status update.
9877
+ 3. If no actions were taken in steps 1-2, do nothing (HEARTBEAT_OK).
9878
+ ${MARKER_END}`;
9879
+ var HEARTBEAT_FILENAME = "HEARTBEAT.md";
9880
+ async function handleAutopilot(ctx, args) {
9881
+ const { workspaceDir, userId } = ctx;
9882
+ if (!userId) {
9883
+ return { text: "Something went wrong \u2014 could not identify your user account." };
9884
+ }
9885
+ const registry = readRegistry(workspaceDir);
9886
+ const auth = checkAuthorization(userId, "autopilot", registry);
9887
+ if (!auth.authorized) {
9888
+ return { text: auth.message ?? "Not authorized." };
9889
+ }
9890
+ const subCommand = args.trim().toLowerCase() || "enable";
9891
+ switch (subCommand) {
9892
+ case "enable":
9893
+ return handleEnable(ctx);
9894
+ case "disable":
9895
+ return handleDisable(ctx);
9896
+ case "status":
9897
+ return handleStatus(ctx);
9898
+ default:
9899
+ return { text: `Unknown autopilot sub-command: "${subCommand}". Use enable, disable, or status.` };
9900
+ }
9901
+ }
9902
+ async function handleEnable(ctx) {
9903
+ const { workspaceDir } = ctx;
9904
+ const heartbeatPath = path6.join(workspaceDir, HEARTBEAT_FILENAME);
9905
+ let content = "";
9906
+ try {
9907
+ if (fs6.existsSync(heartbeatPath)) {
9908
+ content = fs6.readFileSync(heartbeatPath, "utf-8");
9909
+ }
9910
+ } catch {
9911
+ }
9912
+ if (content.includes(MARKER_START)) {
9913
+ await withRegistry(workspaceDir, (data) => {
9914
+ data.autopilotEnabled = true;
9915
+ });
9916
+ return { text: "Autopilot is already enabled." };
9917
+ }
9918
+ const newContent = content ? content.trimEnd() + "\n\n" + HEARTBEAT_BLOCK + "\n" : HEARTBEAT_BLOCK + "\n";
9919
+ fs6.writeFileSync(heartbeatPath, newContent, { mode: 416 });
9920
+ await withRegistry(workspaceDir, (data) => {
9921
+ data.autopilotEnabled = true;
9922
+ });
9923
+ return {
9924
+ text: "**Autopilot enabled.**\nHealth checks will run automatically every day."
9925
+ };
9926
+ }
9927
+ async function handleDisable(ctx) {
9928
+ const { workspaceDir } = ctx;
9929
+ const heartbeatPath = path6.join(workspaceDir, HEARTBEAT_FILENAME);
9930
+ if (!fs6.existsSync(heartbeatPath)) {
9931
+ await withRegistry(workspaceDir, (data) => {
9932
+ data.autopilotEnabled = false;
9933
+ });
9934
+ return { text: "Autopilot is already disabled." };
9935
+ }
9936
+ let content = fs6.readFileSync(heartbeatPath, "utf-8");
9937
+ if (!content.includes(MARKER_START)) {
9938
+ await withRegistry(workspaceDir, (data) => {
9939
+ data.autopilotEnabled = false;
9940
+ });
9941
+ return { text: "Autopilot is already disabled." };
9942
+ }
9943
+ const startIdx = content.indexOf(MARKER_START);
9944
+ const endIdx = content.indexOf(MARKER_END);
9945
+ if (startIdx >= 0 && endIdx >= 0) {
9946
+ const before = content.slice(0, startIdx);
9947
+ const after = content.slice(endIdx + MARKER_END.length);
9948
+ content = (before + after).replace(/\n{3,}/g, "\n\n").trim();
9949
+ if (content) {
9950
+ fs6.writeFileSync(heartbeatPath, content + "\n", { mode: 416 });
9951
+ } else {
9952
+ fs6.unlinkSync(heartbeatPath);
9953
+ }
9954
+ }
9955
+ await withRegistry(workspaceDir, (data) => {
9956
+ data.autopilotEnabled = false;
9957
+ });
9958
+ return {
9959
+ text: "**Autopilot disabled.**\nAutomatic health checks are now off."
9960
+ };
9961
+ }
9962
+ async function handleStatus(ctx) {
9963
+ const { workspaceDir } = ctx;
9964
+ const registry = readRegistry(workspaceDir);
9965
+ const enabled = registry.autopilotEnabled;
9966
+ const lastRun = registry.lastDoctorAllRunAt ? relativeTime(registry.lastDoctorAllRunAt) : "never";
9967
+ const lines = [
9968
+ `**Autopilot:** ${enabled ? "enabled" : "disabled"}`,
9969
+ `**Last health check run:** ${lastRun}`
9970
+ ];
9971
+ return {
9972
+ text: lines.join("\n")
9973
+ };
9974
+ }
9975
+
9874
9976
  // src/commands/init.ts
9875
9977
  var VALID_TYPES = /* @__PURE__ */ new Set(["coding", "research", "marketing", "custom"]);
9876
9978
  function deriveTopicName(nameArg, messageContext, threadId) {
@@ -9891,7 +9993,7 @@ function deriveTopicName(nameArg, messageContext, threadId) {
9891
9993
  async function handleInit(ctx, args) {
9892
9994
  const { workspaceDir, configDir, userId, groupId, threadId, rpc, logger, messageContext } = ctx;
9893
9995
  if (!userId || !groupId || !threadId) {
9894
- return { text: "Missing context: groupId, threadId, or userId not available. Run this command inside a Telegram forum topic." };
9996
+ return { text: "Something went wrong \u2014 this command must be run inside a Telegram forum topic." };
9895
9997
  }
9896
9998
  if (!validateGroupId(groupId)) {
9897
9999
  return { text: "Something went wrong \u2014 this doesn't look like a valid forum topic." };
@@ -9928,17 +10030,17 @@ async function handleInit(ctx, args) {
9928
10030
  const name = deriveTopicName(nameArg, messageContext, threadId);
9929
10031
  const existingSlugs = new Set(Object.values(registry.topics).map((t) => t.slug));
9930
10032
  const finalSlug = generateSlug(threadId, groupId, existingSlugs);
9931
- const projectsBase = path6.join(workspaceDir, "projects");
10033
+ const projectsBase = path7.join(workspaceDir, "projects");
9932
10034
  if (!jailCheck(projectsBase, finalSlug)) {
9933
10035
  return { text: "Setup failed \u2014 internal path validation error. Please try again." };
9934
10036
  }
9935
10037
  if (rejectSymlink(projectsBase)) {
9936
10038
  return { text: "Setup failed \u2014 detected an unsafe file system configuration." };
9937
10039
  }
9938
- if (fs6.existsSync(path6.join(projectsBase, finalSlug))) {
10040
+ if (fs7.existsSync(path7.join(projectsBase, finalSlug))) {
9939
10041
  return { text: "A folder for this topic already exists. Run /tm doctor to investigate." };
9940
10042
  }
9941
- const targetPath = path6.join(projectsBase, finalSlug);
10043
+ const targetPath = path7.join(projectsBase, finalSlug);
9942
10044
  if (rejectSymlink(targetPath)) {
9943
10045
  return { text: "Setup failed \u2014 detected an unsafe file system configuration." };
9944
10046
  }
@@ -9967,12 +10069,32 @@ async function handleInit(ctx, args) {
9967
10069
  data.topics[key] = newEntry;
9968
10070
  if (isFirstUser) {
9969
10071
  data.topicManagerAdmins.push(userId);
10072
+ data.autopilotEnabled = true;
9970
10073
  }
9971
10074
  });
9972
10075
  } catch (err) {
9973
10076
  const msg = err instanceof Error ? err.message : String(err);
9974
10077
  return { text: `Failed to initialize topic: ${msg}` };
9975
10078
  }
10079
+ if (isFirstUser) {
10080
+ try {
10081
+ const heartbeatPath = path7.join(workspaceDir, HEARTBEAT_FILENAME);
10082
+ let hbContent = "";
10083
+ try {
10084
+ if (fs7.existsSync(heartbeatPath)) {
10085
+ hbContent = fs7.readFileSync(heartbeatPath, "utf-8");
10086
+ }
10087
+ } catch {
10088
+ }
10089
+ if (!hbContent.includes(MARKER_START)) {
10090
+ const newContent = hbContent ? hbContent.trimEnd() + "\n\n" + HEARTBEAT_BLOCK + "\n" : HEARTBEAT_BLOCK + "\n";
10091
+ const tmpPath = heartbeatPath + ".tmp";
10092
+ fs7.writeFileSync(tmpPath, newContent, { mode: 416 });
10093
+ fs7.renameSync(tmpPath, heartbeatPath);
10094
+ }
10095
+ } catch {
10096
+ }
10097
+ }
9976
10098
  let restartMsg = "";
9977
10099
  const configWritesEnabled = await getConfigWrites(ctx.rpc);
9978
10100
  if (configWritesEnabled) {
@@ -9993,10 +10115,10 @@ Warning: config sync failed: ${msg}`;
9993
10115
  workspaceDir,
9994
10116
  buildAuditEntry(userId, "init", finalSlug, `Initialized topic name="${name}" type=${topicType} group=${groupId} thread=${threadId}`)
9995
10117
  );
9996
- const topicCard = buildTopicCard(name, finalSlug, topicType);
10118
+ const topicCard = buildTopicCard(name, topicType);
9997
10119
  if (ctx.postFn && groupId && threadId) {
9998
10120
  try {
9999
- const htmlCard = buildTopicCardHtml(name, finalSlug, topicType);
10121
+ const htmlCard = buildTopicCardHtml(name, topicType);
10000
10122
  await ctx.postFn(groupId, threadId, htmlCard);
10001
10123
  return { text: "", pin: true };
10002
10124
  } catch {
@@ -10016,7 +10138,7 @@ async function handleInitInteractive(ctx, args) {
10016
10138
  async function buildTypePicker(ctx) {
10017
10139
  const { workspaceDir, userId, groupId, threadId } = ctx;
10018
10140
  if (!userId || !groupId || !threadId) {
10019
- return { text: "Missing context: groupId, threadId, or userId not available. Run this command inside a Telegram forum topic." };
10141
+ return { text: "Something went wrong \u2014 this command must be run inside a Telegram forum topic." };
10020
10142
  }
10021
10143
  if (!validateGroupId(groupId)) {
10022
10144
  return { text: "Something went wrong \u2014 this doesn't look like a valid forum topic." };
@@ -10055,7 +10177,7 @@ async function buildTypePicker(ctx) {
10055
10177
  async function handleInitTypeSelect(ctx, type) {
10056
10178
  const { workspaceDir, userId, groupId, threadId, messageContext } = ctx;
10057
10179
  if (!userId || !groupId || !threadId) {
10058
- return { text: "Missing context: groupId, threadId, or userId not available. Run this command inside a Telegram forum topic." };
10180
+ return { text: "Something went wrong \u2014 this command must be run inside a Telegram forum topic." };
10059
10181
  }
10060
10182
  if (!validateGroupId(groupId)) {
10061
10183
  return { text: "Something went wrong \u2014 this doesn't look like a valid forum topic." };
@@ -10109,27 +10231,27 @@ function buildInitConfirmMessage(name, type) {
10109
10231
  }
10110
10232
 
10111
10233
  // src/commands/doctor.ts
10112
- import * as fs8 from "node:fs";
10113
- import * as path8 from "node:path";
10234
+ import * as fs9 from "node:fs";
10235
+ import * as path9 from "node:path";
10114
10236
 
10115
10237
  // src/lib/doctor-checks.ts
10116
10238
  var import_json52 = __toESM(require_lib(), 1);
10117
- import * as fs7 from "node:fs";
10118
- import * as path7 from "node:path";
10239
+ import * as fs8 from "node:fs";
10240
+ import * as path8 from "node:path";
10119
10241
  function check(severity, checkId, message, fixable, remediation) {
10120
10242
  return remediation ? { severity, checkId, message, fixable, remediation } : { severity, checkId, message, fixable };
10121
10243
  }
10122
10244
  function runRegistryChecks(entry, projectsBase) {
10123
10245
  const results = [];
10124
- const capsuleDir = path7.join(projectsBase, entry.slug);
10125
- if (!fs7.existsSync(capsuleDir)) {
10246
+ const capsuleDir = path8.join(projectsBase, entry.slug);
10247
+ if (!fs8.existsSync(capsuleDir)) {
10126
10248
  results.push(
10127
10249
  check(Severity.ERROR, "pathMissing", `Project folder is missing (projects/${entry.slug}/)`, false, "Run /tm init to recreate it")
10128
10250
  );
10129
10251
  return results;
10130
10252
  }
10131
10253
  try {
10132
- const stat = fs7.statSync(capsuleDir);
10254
+ const stat = fs8.statSync(capsuleDir);
10133
10255
  if (!stat.isDirectory()) {
10134
10256
  results.push(
10135
10257
  check(Severity.ERROR, "pathNotDir", "Topic path exists but is not a folder", false)
@@ -10144,21 +10266,21 @@ function runRegistryChecks(entry, projectsBase) {
10144
10266
  }
10145
10267
  function runCapsuleChecks(entry, projectsBase) {
10146
10268
  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"))) {
10269
+ const capsuleDir = path8.join(projectsBase, entry.slug);
10270
+ if (!fs8.existsSync(capsuleDir)) return results;
10271
+ if (!fs8.existsSync(path8.join(capsuleDir, "STATUS.md"))) {
10150
10272
  results.push(
10151
10273
  check(Severity.ERROR, "statusMissing", "Status file is missing", true, "Run /tm upgrade to recreate it")
10152
10274
  );
10153
10275
  }
10154
- if (!fs7.existsSync(path7.join(capsuleDir, "TODO.md"))) {
10276
+ if (!fs8.existsSync(path8.join(capsuleDir, "TODO.md"))) {
10155
10277
  results.push(
10156
10278
  check(Severity.WARN, "todoMissing", "TODO file is missing", true, "Run /tm upgrade to recreate it")
10157
10279
  );
10158
10280
  }
10159
10281
  const overlays = OVERLAY_FILES[entry.type] ?? [];
10160
10282
  for (const file of overlays) {
10161
- if (!fs7.existsSync(path7.join(capsuleDir, file))) {
10283
+ if (!fs8.existsSync(path8.join(capsuleDir, file))) {
10162
10284
  results.push(
10163
10285
  check(Severity.INFO, `overlayMissing:${file}`, `Optional overlay ${file} missing for type "${entry.type}"`, true)
10164
10286
  );
@@ -10169,7 +10291,7 @@ function runCapsuleChecks(entry, projectsBase) {
10169
10291
  check(
10170
10292
  Severity.INFO,
10171
10293
  "capsuleVersionBehind",
10172
- `Capsule version ${entry.capsuleVersion} is behind current ${CAPSULE_VERSION}. Will auto-upgrade on next command.`,
10294
+ `Topic files are outdated (v${entry.capsuleVersion} \u2192 v${CAPSULE_VERSION}). Will auto-upgrade on next command.`,
10173
10295
  false
10174
10296
  )
10175
10297
  );
@@ -10300,9 +10422,9 @@ function runCronChecks(cronContent, cronJobsPath) {
10300
10422
  );
10301
10423
  return results;
10302
10424
  }
10303
- if (cronJobsPath && fs7.existsSync(cronJobsPath)) {
10425
+ if (cronJobsPath && fs8.existsSync(cronJobsPath)) {
10304
10426
  try {
10305
- const jobsRaw = fs7.readFileSync(cronJobsPath, "utf-8");
10427
+ const jobsRaw = fs8.readFileSync(cronJobsPath, "utf-8");
10306
10428
  const jobs = JSON.parse(jobsRaw);
10307
10429
  const knownJobIds = new Set(Object.keys(jobs));
10308
10430
  for (const line of lines) {
@@ -10404,9 +10526,9 @@ function runSpamControlCheck(entry) {
10404
10526
  }
10405
10527
  function runAllChecksForTopic(entry, projectsBase, includeContent, registry, cronJobsPath) {
10406
10528
  const results = [];
10407
- const capsuleDir = path7.join(projectsBase, entry.slug);
10529
+ const capsuleDir = path8.join(projectsBase, entry.slug);
10408
10530
  results.push(...runRegistryChecks(entry, projectsBase));
10409
- if (!fs7.existsSync(capsuleDir)) return results;
10531
+ if (!fs8.existsSync(capsuleDir)) return results;
10410
10532
  results.push(...runCapsuleChecks(entry, projectsBase));
10411
10533
  const capsuleFiles = readCapsuleFiles(capsuleDir);
10412
10534
  const statusContent = capsuleFiles.get("STATUS.md");
@@ -10436,16 +10558,16 @@ var BACKUP_FILES = ["STATUS.md", "TODO.md"];
10436
10558
  function backupCapsuleIfHealthy(projectsBase, slug, results) {
10437
10559
  const hasIssues = results.some((r) => r.severity === Severity.ERROR || r.severity === Severity.WARN);
10438
10560
  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 });
10561
+ const capsuleDir = path8.join(projectsBase, slug);
10562
+ const backupDir = path8.join(capsuleDir, BACKUP_DIR);
10563
+ if (!fs8.existsSync(backupDir)) {
10564
+ fs8.mkdirSync(backupDir, { recursive: true });
10443
10565
  }
10444
10566
  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);
10567
+ const src = path8.join(capsuleDir, file);
10568
+ const dst = path8.join(backupDir, file);
10569
+ if (fs8.existsSync(src)) {
10570
+ fs8.copyFileSync(src, dst);
10449
10571
  }
10450
10572
  }
10451
10573
  }
@@ -10468,10 +10590,10 @@ function readCapsuleFiles(capsuleDir) {
10468
10590
  "METRICS.md"
10469
10591
  ];
10470
10592
  for (const name of filenames) {
10471
- const filePath = path7.join(capsuleDir, name);
10593
+ const filePath = path8.join(capsuleDir, name);
10472
10594
  try {
10473
- if (fs7.existsSync(filePath)) {
10474
- files.set(name, fs7.readFileSync(filePath, "utf-8"));
10595
+ if (fs8.existsSync(filePath)) {
10596
+ files.set(name, fs8.readFileSync(filePath, "utf-8"));
10475
10597
  }
10476
10598
  } catch {
10477
10599
  }
@@ -10483,7 +10605,7 @@ function readCapsuleFiles(capsuleDir) {
10483
10605
  async function handleDoctor(ctx) {
10484
10606
  const { workspaceDir, configDir, userId, groupId, threadId } = ctx;
10485
10607
  if (!userId || !groupId || !threadId) {
10486
- return { text: "Missing context: userId, groupId, or threadId not available." };
10608
+ return { text: "Something went wrong \u2014 this command must be run inside a Telegram forum topic." };
10487
10609
  }
10488
10610
  const registry = readRegistry(workspaceDir);
10489
10611
  const auth = checkAuthorization(userId, "doctor", registry);
@@ -10495,23 +10617,23 @@ async function handleDoctor(ctx) {
10495
10617
  if (!entry) {
10496
10618
  return { text: "This topic is not registered. Run /tm init first." };
10497
10619
  }
10498
- const projectsBase = path8.join(workspaceDir, "projects");
10620
+ const projectsBase = path9.join(workspaceDir, "projects");
10499
10621
  if (!jailCheck(projectsBase, entry.slug)) {
10500
10622
  return { text: "Something went wrong \u2014 path validation failed." };
10501
10623
  }
10502
- const capsuleDir = path8.join(projectsBase, entry.slug);
10624
+ const capsuleDir = path9.join(projectsBase, entry.slug);
10503
10625
  if (rejectSymlink(capsuleDir)) {
10504
10626
  return { text: "Something went wrong \u2014 detected an unsafe file system configuration." };
10505
10627
  }
10506
10628
  let includeContent;
10507
10629
  const incPath = includePath(configDir);
10508
10630
  try {
10509
- if (fs8.existsSync(incPath)) {
10510
- includeContent = fs8.readFileSync(incPath, "utf-8");
10631
+ if (fs9.existsSync(incPath)) {
10632
+ includeContent = fs9.readFileSync(incPath, "utf-8");
10511
10633
  }
10512
10634
  } catch {
10513
10635
  }
10514
- const cronJobsPath = path8.join(configDir, "cron", "jobs.json");
10636
+ const cronJobsPath = path9.join(configDir, "cron", "jobs.json");
10515
10637
  const results = runAllChecksForTopic(
10516
10638
  entry,
10517
10639
  projectsBase,
@@ -10540,48 +10662,185 @@ async function handleDoctor(ctx) {
10540
10662
  }
10541
10663
 
10542
10664
  // 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." };
10665
+ import * as fs11 from "node:fs";
10666
+ import * as path11 from "node:path";
10667
+
10668
+ // src/commands/daily-report.ts
10669
+ import * as fs10 from "node:fs";
10670
+ import * as path10 from "node:path";
10671
+ async function handleDailyReport(ctx) {
10672
+ const { workspaceDir, groupId, threadId, logger } = ctx;
10673
+ if (!groupId || !threadId) {
10674
+ return { text: "Something went wrong \u2014 this command must be run inside a Telegram forum topic." };
10549
10675
  }
10676
+ const key = topicKey(groupId, threadId);
10550
10677
  const registry = readRegistry(workspaceDir);
10551
- const auth = checkAuthorization(userId, "doctor-all", registry);
10552
- if (!auth.authorized) {
10553
- return { text: auth.message ?? "Not authorized." };
10678
+ const entry = registry.topics[key];
10679
+ if (!entry) {
10680
+ return { text: "This topic is not registered. Run /tm init first." };
10554
10681
  }
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
- };
10682
+ if (entry.lastDailyReportAt) {
10683
+ const lastReport = new Date(entry.lastDailyReportAt);
10684
+ const now = /* @__PURE__ */ new Date();
10685
+ if (lastReport.getUTCFullYear() === now.getUTCFullYear() && lastReport.getUTCMonth() === now.getUTCMonth() && lastReport.getUTCDate() === now.getUTCDate()) {
10686
+ return { text: "Daily report already generated today. Try again tomorrow." };
10563
10687
  }
10564
10688
  }
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");
10689
+ const projectsBase = path10.join(workspaceDir, "projects");
10690
+ const capsuleDir = path10.join(projectsBase, entry.slug);
10691
+ if (!fs10.existsSync(capsuleDir)) {
10692
+ return { text: "Topic files not found. Run /tm init to set up this topic." };
10693
+ }
10694
+ const statusContent = readFileOrNull(path10.join(capsuleDir, "STATUS.md"));
10695
+ const todoContent = readFileOrNull(path10.join(capsuleDir, "TODO.md"));
10696
+ const learningsContent = readFileOrNull(path10.join(capsuleDir, "LEARNINGS.md"));
10697
+ const doneContent = extractDoneSection(statusContent);
10698
+ const newLearnings = extractTodayLearnings(learningsContent);
10699
+ const blockers = extractBlockers(todoContent);
10700
+ const nextContent = extractNextActions(statusContent);
10701
+ const upcomingContent = extractUpcoming(statusContent);
10702
+ const health = computeHealth(entry.lastMessageAt, statusContent, blockers);
10703
+ const reportData = {
10704
+ name: entry.name,
10705
+ doneContent,
10706
+ learningsContent: newLearnings,
10707
+ blockersContent: blockers,
10708
+ nextContent,
10709
+ upcomingContent,
10710
+ health
10711
+ };
10712
+ if (ctx.postFn) {
10713
+ try {
10714
+ const htmlReport = buildDailyReport(reportData, "html");
10715
+ await ctx.postFn(groupId, threadId, htmlReport);
10716
+ await withRegistry(workspaceDir, (data) => {
10717
+ const e = data.topics[key];
10718
+ if (e) {
10719
+ e.lastDailyReportAt = (/* @__PURE__ */ new Date()).toISOString();
10720
+ }
10721
+ });
10722
+ } catch (err) {
10723
+ const msg = err instanceof Error ? err.message : String(err);
10724
+ logger.error(`[daily-report] Post failed: ${msg}`);
10725
+ return { text: `Daily report generated but post failed: ${msg}` };
10572
10726
  }
10727
+ } else {
10728
+ await withRegistry(workspaceDir, (data) => {
10729
+ const e = data.topics[key];
10730
+ if (e) {
10731
+ e.lastDailyReportAt = (/* @__PURE__ */ new Date()).toISOString();
10732
+ }
10733
+ });
10734
+ }
10735
+ return { text: buildDailyReport(reportData, "markdown") };
10736
+ }
10737
+ function readFileOrNull(filePath) {
10738
+ try {
10739
+ return fs10.readFileSync(filePath, "utf-8");
10573
10740
  } catch {
10741
+ return null;
10574
10742
  }
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++;
10743
+ }
10744
+ function extractDoneSection(statusContent) {
10745
+ if (!statusContent) return "_No status available yet._";
10746
+ const match = statusContent.match(/^##\s*Last done\s*\(UTC\)\s*\n([\s\S]*?)(?=\n##\s|\n*$)/im);
10747
+ if (!match) return '_No "Last done" section found._';
10748
+ const text = match[1]?.trim();
10749
+ return text || "_Empty._";
10750
+ }
10751
+ function extractTodayLearnings(learningsContent) {
10752
+ if (!learningsContent) return "_No learnings recorded yet._";
10753
+ const today = (/* @__PURE__ */ new Date()).toISOString().slice(0, 10);
10754
+ const lines = learningsContent.split("\n");
10755
+ const todayLines = [];
10756
+ let inTodaySection = false;
10757
+ for (const line of lines) {
10758
+ if (line.startsWith("## ") && line.includes(today)) {
10759
+ inTodaySection = true;
10760
+ continue;
10761
+ }
10762
+ if (inTodaySection && line.startsWith("## ")) {
10763
+ break;
10764
+ }
10765
+ if (inTodaySection && line.trim()) {
10766
+ todayLines.push(line);
10767
+ }
10768
+ }
10769
+ return todayLines.length > 0 ? todayLines.join("\n") : "_None today._";
10770
+ }
10771
+ function extractBlockers(todoContent) {
10772
+ if (!todoContent) return "_No tasks recorded yet._";
10773
+ const lines = todoContent.split("\n");
10774
+ const blockerLines = lines.filter(
10775
+ (l) => /\[BLOCKED\]/i.test(l) || /\bblocked\b/i.test(l)
10776
+ );
10777
+ return blockerLines.length > 0 ? blockerLines.join("\n") : "_None._";
10778
+ }
10779
+ function extractNextActions(statusContent) {
10780
+ if (!statusContent) return "_No status available yet._";
10781
+ const match = statusContent.match(/^##\s*Next (?:3 )?actions(?: \(now\))?\s*\n([\s\S]*?)(?=\n##\s|\n*$)/im);
10782
+ if (!match) return '_No "Next actions" section found._';
10783
+ const text = match[1]?.trim();
10784
+ return text || "_Empty._";
10785
+ }
10786
+ function extractUpcoming(statusContent) {
10787
+ if (!statusContent) return "_No status available yet._";
10788
+ const match = statusContent.match(/^##\s*Upcoming actions\s*\n([\s\S]*?)(?=\n##\s|\n*$)/im);
10789
+ if (!match) return '_No "Upcoming actions" section found._';
10790
+ const text = match[1]?.trim();
10791
+ return text || "_Empty._";
10792
+ }
10793
+ function computeHealth(lastMessageAt, statusContent, blockers) {
10794
+ if (blockers && blockers !== "_None._" && blockers !== "_No tasks recorded yet._") {
10795
+ return "blocked";
10796
+ }
10797
+ if (!lastMessageAt) return "stale";
10798
+ const hoursSinceActivity = (Date.now() - new Date(lastMessageAt).getTime()) / 36e5;
10799
+ if (hoursSinceActivity > 72) return "stale";
10800
+ return "fresh";
10801
+ }
10802
+
10803
+ // src/commands/doctor-all.ts
10804
+ async function handleDoctorAll(ctx) {
10805
+ const { workspaceDir, configDir, userId, logger } = ctx;
10806
+ if (!userId) {
10807
+ return { text: "Something went wrong \u2014 could not identify your user account." };
10808
+ }
10809
+ const registry = readRegistry(workspaceDir);
10810
+ const auth = checkAuthorization(userId, "doctor-all", registry);
10811
+ if (!auth.authorized) {
10812
+ return { text: auth.message ?? "Not authorized." };
10813
+ }
10814
+ if (registry.lastDoctorAllRunAt) {
10815
+ const lastRun = new Date(registry.lastDoctorAllRunAt).getTime();
10816
+ const elapsed = Date.now() - lastRun;
10817
+ if (elapsed < DOCTOR_ALL_COOLDOWN_MS) {
10818
+ const remainingMin = Math.ceil((DOCTOR_ALL_COOLDOWN_MS - elapsed) / 6e4);
10819
+ return {
10820
+ text: `Health checks were run ${Math.floor(elapsed / 6e4)} minutes ago. Try again in ${remainingMin} minute(s).`
10821
+ };
10822
+ }
10823
+ }
10824
+ const now = /* @__PURE__ */ new Date();
10825
+ const projectsBase = path11.join(workspaceDir, "projects");
10826
+ let includeContent;
10827
+ const incPath = includePath(configDir);
10828
+ try {
10829
+ if (fs11.existsSync(incPath)) {
10830
+ includeContent = fs11.readFileSync(incPath, "utf-8");
10831
+ }
10832
+ } catch {
10833
+ }
10834
+ const cronJobsPath = path11.join(configDir, "cron", "jobs.json");
10835
+ const allEntries = Object.entries(registry.topics);
10836
+ const reports = [];
10837
+ const errors = [];
10838
+ let processed = 0;
10839
+ let skipped = 0;
10840
+ const groupPostResults = /* @__PURE__ */ new Map();
10841
+ for (const [_key, entry] of allEntries) {
10842
+ if (!isEligible(entry, now)) {
10843
+ skipped++;
10585
10844
  continue;
10586
10845
  }
10587
10846
  try {
@@ -10667,6 +10926,54 @@ async function handleDoctorAll(ctx) {
10667
10926
  }
10668
10927
  }
10669
10928
  }
10929
+ let dailyReportSuccesses = 0;
10930
+ let dailyReportSkipped = 0;
10931
+ const dailyReportKeys = /* @__PURE__ */ new Set();
10932
+ if (ctx.postFn && reports.length > 0) {
10933
+ const rateLimitedPost = createRateLimitedPoster(ctx.postFn);
10934
+ const nowDate = now.toISOString().slice(0, 10);
10935
+ for (const report of reports) {
10936
+ const key = `${report.groupId}:${report.threadId}`;
10937
+ const entry = registry.topics[key];
10938
+ if (!entry) continue;
10939
+ if (entry.lastDailyReportAt) {
10940
+ const lastReport = new Date(entry.lastDailyReportAt);
10941
+ const lastDate = `${lastReport.getUTCFullYear()}-${String(lastReport.getUTCMonth() + 1).padStart(2, "0")}-${String(lastReport.getUTCDate()).padStart(2, "0")}`;
10942
+ if (lastDate === nowDate) {
10943
+ dailyReportSkipped++;
10944
+ continue;
10945
+ }
10946
+ }
10947
+ const capsuleDir = path11.join(projectsBase, entry.slug);
10948
+ const statusContent = readFileOrNull(path11.join(capsuleDir, "STATUS.md"));
10949
+ const todoContent = readFileOrNull(path11.join(capsuleDir, "TODO.md"));
10950
+ const learningsContent = readFileOrNull(path11.join(capsuleDir, "LEARNINGS.md"));
10951
+ const doneContent = extractDoneSection(statusContent);
10952
+ const newLearnings = extractTodayLearnings(learningsContent);
10953
+ const blockers = extractBlockers(todoContent);
10954
+ const nextContent = extractNextActions(statusContent);
10955
+ const upcomingContent = extractUpcoming(statusContent);
10956
+ const health = computeHealth(entry.lastMessageAt, statusContent, blockers);
10957
+ const reportData = {
10958
+ name: entry.name,
10959
+ doneContent,
10960
+ learningsContent: newLearnings,
10961
+ blockersContent: blockers,
10962
+ nextContent,
10963
+ upcomingContent,
10964
+ health
10965
+ };
10966
+ try {
10967
+ const htmlReport = buildDailyReport(reportData, "html");
10968
+ await rateLimitedPost(report.groupId, report.threadId, htmlReport);
10969
+ dailyReportSuccesses++;
10970
+ dailyReportKeys.add(key);
10971
+ } catch (err) {
10972
+ const msg = err instanceof Error ? err.message : String(err);
10973
+ logger.error(`[doctor-all] Daily report post failed for ${entry.slug}: ${msg}`);
10974
+ }
10975
+ }
10976
+ }
10670
10977
  await withRegistry(workspaceDir, (data) => {
10671
10978
  data.lastDoctorAllRunAt = now.toISOString();
10672
10979
  for (const [_key, entry] of Object.entries(data.topics)) {
@@ -10689,6 +10996,12 @@ async function handleDoctorAll(ctx) {
10689
10996
  entry.consecutiveSilentDoctors = 0;
10690
10997
  }
10691
10998
  }
10999
+ for (const key of dailyReportKeys) {
11000
+ const entry = data.topics[key];
11001
+ if (entry) {
11002
+ entry.lastDailyReportAt = now.toISOString();
11003
+ }
11004
+ }
10692
11005
  });
10693
11006
  const lines = [
10694
11007
  `**Health Check Summary**`,
@@ -10699,6 +11012,7 @@ async function handleDoctorAll(ctx) {
10699
11012
  ];
10700
11013
  if (ctx.postFn) {
10701
11014
  lines.push(`Posted: ${postSuccesses}, Post failures: ${postErrors}`);
11015
+ lines.push(`Daily reports: ${dailyReportSuccesses} sent, ${dailyReportSkipped} skipped`);
10702
11016
  }
10703
11017
  if (errors.length > 0) {
10704
11018
  lines.push("");
@@ -10737,7 +11051,7 @@ function isEligible(entry, now) {
10737
11051
  async function handleList(ctx) {
10738
11052
  const { workspaceDir, userId } = ctx;
10739
11053
  if (!userId) {
10740
- return { text: "Missing context: userId not available." };
11054
+ return { text: "Something went wrong \u2014 could not identify your user account." };
10741
11055
  }
10742
11056
  const registry = readRegistry(workspaceDir);
10743
11057
  const auth = checkAuthorization(userId, "list", registry);
@@ -10752,8 +11066,8 @@ async function handleList(ctx) {
10752
11066
  }
10753
11067
 
10754
11068
  // src/commands/status.ts
10755
- import * as fs10 from "node:fs";
10756
- import * as path10 from "node:path";
11069
+ import * as fs12 from "node:fs";
11070
+ import * as path12 from "node:path";
10757
11071
  var LAST_DONE_RE2 = /^##\s*Last done\s*\(UTC\)\s*\n([\s\S]*?)(?=\n##\s|\n*$)/im;
10758
11072
  var NEXT_ACTIONS_RE2 = /^##\s*Next (?:3 )?actions(?: \(now\))?\s*\n([\s\S]*?)(?=\n##\s|\n*$)/im;
10759
11073
  var UPCOMING_RE = /^##\s*Upcoming actions\s*\n([\s\S]*?)(?=\n##\s|\n*$)/im;
@@ -10809,10 +11123,10 @@ function formatStatus(name, content) {
10809
11123
  }
10810
11124
  return lines.join("\n");
10811
11125
  }
10812
- async function handleStatus(ctx) {
11126
+ async function handleStatus2(ctx) {
10813
11127
  const { workspaceDir, userId, groupId, threadId } = ctx;
10814
11128
  if (!userId || !groupId || !threadId) {
10815
- return { text: "Missing context: userId, groupId, or threadId not available." };
11129
+ return { text: "Something went wrong \u2014 this command must be run inside a Telegram forum topic." };
10816
11130
  }
10817
11131
  const registry = readRegistry(workspaceDir);
10818
11132
  const auth = checkAuthorization(userId, "status", registry);
@@ -10824,20 +11138,20 @@ async function handleStatus(ctx) {
10824
11138
  if (!entry) {
10825
11139
  return { text: "This topic is not registered. Run /tm init first." };
10826
11140
  }
10827
- const projectsBase = path10.join(workspaceDir, "projects");
10828
- const capsuleDir = path10.join(projectsBase, entry.slug);
11141
+ const projectsBase = path12.join(workspaceDir, "projects");
11142
+ const capsuleDir = path12.join(projectsBase, entry.slug);
10829
11143
  if (!jailCheck(projectsBase, entry.slug)) {
10830
11144
  return { text: "Something went wrong \u2014 path validation failed." };
10831
11145
  }
10832
11146
  if (rejectSymlink(capsuleDir)) {
10833
11147
  return { text: "Something went wrong \u2014 detected an unsafe file system configuration." };
10834
11148
  }
10835
- const statusPath = path10.join(capsuleDir, "STATUS.md");
10836
- if (!fs10.existsSync(statusPath)) {
11149
+ const statusPath = path12.join(capsuleDir, "STATUS.md");
11150
+ if (!fs12.existsSync(statusPath)) {
10837
11151
  return { text: "No status available yet. Run /tm doctor to diagnose." };
10838
11152
  }
10839
11153
  try {
10840
- const content = fs10.readFileSync(statusPath, "utf-8");
11154
+ const content = fs12.readFileSync(statusPath, "utf-8");
10841
11155
  return {
10842
11156
  text: truncateMessage(formatStatus(entry.name, content))
10843
11157
  };
@@ -10851,7 +11165,7 @@ async function handleStatus(ctx) {
10851
11165
  async function handleSync(ctx) {
10852
11166
  const { workspaceDir, configDir, userId, rpc, logger } = ctx;
10853
11167
  if (!userId) {
10854
- return { text: "Missing context: userId not available." };
11168
+ return { text: "Something went wrong \u2014 could not identify your user account." };
10855
11169
  }
10856
11170
  const registry = readRegistry(workspaceDir);
10857
11171
  const auth = checkAuthorization(userId, "sync", registry);
@@ -10881,7 +11195,7 @@ async function handleSync(ctx) {
10881
11195
  async function handleRename(ctx, newName) {
10882
11196
  const { workspaceDir, configDir, userId, groupId, threadId, rpc, logger } = ctx;
10883
11197
  if (!userId || !groupId || !threadId) {
10884
- return { text: "Missing context: userId, groupId, or threadId not available." };
11198
+ return { text: "Something went wrong \u2014 this command must be run inside a Telegram forum topic." };
10885
11199
  }
10886
11200
  const trimmedName = newName.trim();
10887
11201
  if (!trimmedName) {
@@ -10919,7 +11233,7 @@ async function handleRename(ctx, newName) {
10919
11233
  } catch (err) {
10920
11234
  const msg = err instanceof Error ? err.message : String(err);
10921
11235
  restartMsg = `
10922
- Warning: include generation failed: ${msg}`;
11236
+ Warning: config sync failed: ${msg}`;
10923
11237
  }
10924
11238
  const result = await triggerRestart(rpc, logger);
10925
11239
  if (!result.success && result.fallbackMessage) {
@@ -10936,11 +11250,11 @@ Warning: include generation failed: ${msg}`;
10936
11250
  }
10937
11251
 
10938
11252
  // src/commands/upgrade.ts
10939
- import * as path11 from "node:path";
11253
+ import * as path13 from "node:path";
10940
11254
  async function handleUpgrade(ctx) {
10941
11255
  const { workspaceDir, userId, groupId, threadId } = ctx;
10942
11256
  if (!userId || !groupId || !threadId) {
10943
- return { text: "Missing context: userId, groupId, or threadId not available." };
11257
+ return { text: "Something went wrong \u2014 this command must be run inside a Telegram forum topic." };
10944
11258
  }
10945
11259
  const registry = readRegistry(workspaceDir);
10946
11260
  const auth = checkAuthorization(userId, "upgrade", registry);
@@ -10957,7 +11271,7 @@ async function handleUpgrade(ctx) {
10957
11271
  text: `Topic **${entry.name}** is already up to date. No upgrade needed.`
10958
11272
  };
10959
11273
  }
10960
- const projectsBase = path11.join(workspaceDir, "projects");
11274
+ const projectsBase = path13.join(workspaceDir, "projects");
10961
11275
  const result = upgradeCapsule(projectsBase, entry.slug, entry.name, entry.type, entry.capsuleVersion);
10962
11276
  if (!result.upgraded) {
10963
11277
  return {
@@ -10970,10 +11284,11 @@ async function handleUpgrade(ctx) {
10970
11284
  topic.capsuleVersion = result.newVersion;
10971
11285
  }
10972
11286
  });
10973
- const addedList = result.addedFiles.length > 0 ? `
10974
- Added files: ${result.addedFiles.join(", ")}` : "\nNo new files added.";
11287
+ const addedCount = result.addedFiles.length;
11288
+ const addedNote = addedCount > 0 ? `
11289
+ ${addedCount} new file${addedCount === 1 ? "" : "s"} added.` : "";
10975
11290
  return {
10976
- text: `Topic **${entry.name}** upgraded.${addedList}`
11291
+ text: `Topic **${entry.name}** upgraded to the latest version.${addedNote}`
10977
11292
  };
10978
11293
  }
10979
11294
 
@@ -10982,7 +11297,7 @@ var DURATION_RE = /^(\d+)d$/;
10982
11297
  async function handleSnooze(ctx, args) {
10983
11298
  const { workspaceDir, userId, groupId, threadId } = ctx;
10984
11299
  if (!userId || !groupId || !threadId) {
10985
- return { text: "Missing context: userId, groupId, or threadId not available." };
11300
+ return { text: "Something went wrong \u2014 this command must be run inside a Telegram forum topic." };
10986
11301
  }
10987
11302
  const trimmed = args.trim();
10988
11303
  if (!trimmed) {
@@ -11035,7 +11350,7 @@ async function handleArchiveToggle(ctx, archive) {
11035
11350
  const { workspaceDir, configDir, userId, groupId, threadId, rpc, logger } = ctx;
11036
11351
  const command = archive ? "archive" : "unarchive";
11037
11352
  if (!userId || !groupId || !threadId) {
11038
- return { text: "Missing context: userId, groupId, or threadId not available." };
11353
+ return { text: "Something went wrong \u2014 this command must be run inside a Telegram forum topic." };
11039
11354
  }
11040
11355
  const registry = readRegistry(workspaceDir);
11041
11356
  const auth = checkAuthorization(userId, command, registry);
@@ -11072,7 +11387,7 @@ async function handleArchiveToggle(ctx, archive) {
11072
11387
  } catch (err) {
11073
11388
  const msg = err instanceof Error ? err.message : String(err);
11074
11389
  restartMsg = `
11075
- Warning: include generation failed: ${msg}`;
11390
+ Warning: config sync failed: ${msg}`;
11076
11391
  }
11077
11392
  const result = await triggerRestart(rpc, logger);
11078
11393
  if (!result.success && result.fallbackMessage) {
@@ -11089,254 +11404,6 @@ Warning: include generation failed: ${msg}`;
11089
11404
  };
11090
11405
  }
11091
11406
 
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
11407
  // src/commands/help.ts
11341
11408
  function handleHelp(_ctx) {
11342
11409
  return {
@@ -11395,7 +11462,7 @@ function createTopicManagerTool(deps) {
11395
11462
  case "list":
11396
11463
  return await handleList(ctx);
11397
11464
  case "status":
11398
- return await handleStatus(ctx);
11465
+ return await handleStatus2(ctx);
11399
11466
  case "sync":
11400
11467
  return await handleSync(ctx);
11401
11468
  case "rename":
@@ -11586,7 +11653,7 @@ function register(api) {
11586
11653
  });
11587
11654
  api.registerTool({
11588
11655
  name: "topic_manager",
11589
- description: "Manage Telegram forum topics as deterministic workcells. Sub-commands: init, doctor, list, status, sync, rename, upgrade, snooze, archive, unarchive, autopilot, help.",
11656
+ description: "Manage Telegram forum topics with persistent memory. Sub-commands: init, doctor, list, status, sync, rename, upgrade, snooze, archive, unarchive, autopilot, help.",
11590
11657
  parameters: Type.Object({
11591
11658
  command: Type.String({
11592
11659
  description: "Sub-command and arguments (e.g., 'init', 'doctor --all', 'rename new-name')"
@@ -11608,7 +11675,7 @@ function register(api) {
11608
11675
  if (api.registerCommand) {
11609
11676
  api.registerCommand({
11610
11677
  name: "tm",
11611
- description: "Manage Telegram forum topics as deterministic workcells. Sub-commands: init, doctor, list, status, sync, rename, upgrade, snooze, archive, unarchive, autopilot, help.",
11678
+ description: "Manage Telegram forum topics with persistent memory. Sub-commands: init, doctor, list, status, sync, rename, upgrade, snooze, archive, unarchive, autopilot, help.",
11612
11679
  acceptsArgs: true,
11613
11680
  requireAuth: false,
11614
11681
  async handler(ctx) {