openclaw-telegram-manager 2.5.6 → 2.6.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
@@ -8832,7 +8832,7 @@ var TopicTypeSchema = Type.Union([
8832
8832
  Type.Literal("coding"),
8833
8833
  Type.Literal("research"),
8834
8834
  Type.Literal("marketing"),
8835
- Type.Literal("custom")
8835
+ Type.Literal("general")
8836
8836
  ]);
8837
8837
  var TopicStatusSchema = Type.Union([
8838
8838
  Type.Literal("active"),
@@ -8870,7 +8870,7 @@ var OVERLAY_FILES = {
8870
8870
  coding: ["ARCHITECTURE.md", "DEPLOY.md"],
8871
8871
  research: ["SOURCES.md", "FINDINGS.md"],
8872
8872
  marketing: ["CAMPAIGNS.md", "METRICS.md"],
8873
- custom: []
8873
+ general: []
8874
8874
  };
8875
8875
  var BASE_FILES = [
8876
8876
  "README.md",
@@ -9392,7 +9392,7 @@ function buildInitTypeButtons(groupId, threadId, secret, userId) {
9392
9392
  ],
9393
9393
  [
9394
9394
  { text: "Marketing", callback_data: cb("im") },
9395
- { text: "Custom", callback_data: cb("ix") }
9395
+ { text: "General", callback_data: cb("ig") }
9396
9396
  ]
9397
9397
  ]);
9398
9398
  }
@@ -9401,7 +9401,7 @@ function buildInitConfirmButton(groupId, threadId, secret, userId, type) {
9401
9401
  coding: "yc",
9402
9402
  research: "yr",
9403
9403
  marketing: "ym",
9404
- custom: "yx"
9404
+ general: "yg"
9405
9405
  };
9406
9406
  const cb = buildCallbackData(actionMap[type], groupId, threadId, secret, userId);
9407
9407
  return buildInlineKeyboard([[{ text: "Use this name", callback_data: cb }]]);
@@ -9428,7 +9428,7 @@ function buildInitWelcomeHtml() {
9428
9428
  "\u2022 <b>Coding</b> \u2014 tracks architecture decisions and deployment steps",
9429
9429
  "\u2022 <b>Research</b> \u2014 tracks sources and key findings",
9430
9430
  "\u2022 <b>Marketing</b> \u2014 tracks campaigns and metrics",
9431
- "\u2022 <b>Custom</b> \u2014 general-purpose tracking",
9431
+ "\u2022 <b>General</b> \u2014 general-purpose tracking",
9432
9432
  "",
9433
9433
  "<i>The AI may take a few seconds to respond \u2014 no need to tap twice.</i>"
9434
9434
  ].join("\n");
@@ -9526,9 +9526,7 @@ function buildHelpCard() {
9526
9526
  "/tm rename new-name \u2014 rename this topic",
9527
9527
  "/tm snooze 7d \u2014 pause health checks (e.g. 7d, 30d)",
9528
9528
  "/tm archive \u2014 archive this topic",
9529
- "/tm unarchive \u2014 bring back an archived topic",
9530
- "/tm sync \u2014 fix config if out of sync",
9531
- "/tm upgrade \u2014 update topic files to latest version"
9529
+ "/tm unarchive \u2014 bring back an archived topic"
9532
9530
  ].join("\n");
9533
9531
  }
9534
9532
  function buildListMessage(topics) {
@@ -9544,8 +9542,8 @@ function buildListMessage(topics) {
9544
9542
  let rendered = 0;
9545
9543
  for (const t of sorted) {
9546
9544
  const activity = t.lastMessageAt ? `active ${relativeTime(t.lastMessageAt)}` : "no activity yet";
9547
- const statusTag = t.status !== "active" ? ` \u2014 ${t.status}` : "";
9548
- const entry = `**${t.name}** \xB7 ${t.type}${statusTag}
9545
+ const icon = t.status === "snoozed" ? "\u{1F4A4} " : t.status === "archived" ? "\u{1F4E6} " : "";
9546
+ const entry = `${icon}**${t.name}** \xB7 ${t.type}
9549
9547
  ${activity}`;
9550
9548
  const tentative = [...lines, entry, ""].join("\n");
9551
9549
  if (tentative.length > TELEGRAM_MSG_LIMIT - 40) {
@@ -9628,12 +9626,22 @@ var FILE_MODE3 = 384;
9628
9626
  function getSystemPromptTemplate(name, slug, absoluteWorkspacePath) {
9629
9627
  return `You are the assistant for the Telegram topic: ${name}.
9630
9628
 
9629
+ Context hierarchy (strictly ordered):
9630
+ 1. TOPIC FILES (primary) \u2014 your project folder at: ${absoluteWorkspacePath}/projects/${slug}/
9631
+ These define WHAT this topic is working on: current tasks, status, goals, history.
9632
+ When asked "what are we working on?", answer ONLY from these files.
9633
+ 2. WORKSPACE MEMORY (secondary) \u2014 files like memory/, MEMORY.md at the workspace root.
9634
+ These contain general learnings, user preferences, and cross-topic knowledge.
9635
+ Use them for HOW to work (patterns, mistakes to avoid, coding style) \u2014 but
9636
+ never to determine what this topic is about or what its current tasks are.
9637
+ Workspace memory may reference projects belonging to OTHER topics \u2014 ignore those.
9638
+
9631
9639
  Determinism rules:
9632
- - Source of truth is the project folder at: ${absoluteWorkspacePath}/projects/${slug}/
9633
- - After /reset, /new, or context compaction: ALWAYS re-read STATUS.md,
9634
- then TODO.md, then LEARNINGS.md (last 20 entries), then COMMANDS.md
9635
- before continuing work. Do not rely on summarized memory for paths,
9636
- commands, or task state.
9640
+ - After /reset, /new, or context compaction: ALWAYS re-read your topic files
9641
+ first \u2014 STATUS.md, then TODO.md, then LEARNINGS.md (last 20 entries), then
9642
+ COMMANDS.md \u2014 before doing anything else. These are your ground truth.
9643
+ Do not rely on summarized memory or workspace-level files for this topic's
9644
+ tasks, status, or goals.
9637
9645
  - Before context compaction or when the conversation is long: proactively
9638
9646
  flush current progress to STATUS.md (update "Last done (UTC)" and
9639
9647
  "Next actions (now)") so compaction cannot erase critical state.
@@ -9656,8 +9664,11 @@ Learning capture:
9656
9664
  - If LEARNINGS.md exceeds ~200 lines, archive older entries to LEARNINGS-archive.md.
9657
9665
 
9658
9666
  Separation:
9659
- - Your workspace is strictly projects/${slug}/. Do not read, write, or reference
9660
- files in any other topic's project directory.
9667
+ - Your project folder is strictly projects/${slug}/. This is your identity.
9668
+ - Do not read, write, or reference files in any other topic's project directory.
9669
+ - Workspace-level files (memory/, MEMORY.md, projects-index, etc.) are shared
9670
+ context \u2014 you may read them for general knowledge and learnings, but they
9671
+ do not define this topic's current work, goals, or identity.
9661
9672
  - If the user mentions another topic by name or slug, ask for explicit
9662
9673
  confirmation before mixing work: "This references topic X \u2014 switch context?"
9663
9674
  - Never copy data between topic folders without explicit user instruction.
@@ -9973,7 +9984,7 @@ async function handleStatus(ctx) {
9973
9984
  }
9974
9985
 
9975
9986
  // src/commands/init.ts
9976
- var VALID_TYPES = /* @__PURE__ */ new Set(["coding", "research", "marketing", "custom"]);
9987
+ var VALID_TYPES = /* @__PURE__ */ new Set(["coding", "research", "marketing", "general"]);
9977
9988
  function deriveTopicName(nameArg, messageContext, threadId) {
9978
9989
  const topicTitle = messageContext?.["topicTitle"] ?? "";
9979
9990
  let name;
@@ -10120,13 +10131,12 @@ Warning: config sync failed: ${msg}`;
10120
10131
  try {
10121
10132
  const htmlCard = buildTopicCardHtml(name, topicType);
10122
10133
  await ctx.postFn(groupId, threadId, htmlCard);
10123
- return { text: "", pin: true };
10134
+ return { text: "" };
10124
10135
  } catch {
10125
10136
  }
10126
10137
  }
10127
10138
  return {
10128
- text: `${topicCard}${restartMsg}`,
10129
- pin: true
10139
+ text: `${topicCard}${restartMsg}`
10130
10140
  };
10131
10141
  }
10132
10142
  async function handleInitInteractive(ctx, args) {
@@ -10740,14 +10750,14 @@ function readFileOrNull(filePath) {
10740
10750
  }
10741
10751
  }
10742
10752
  function extractDoneSection(statusContent) {
10743
- if (!statusContent) return "_No status available yet._";
10753
+ if (!statusContent) return "No status available yet.";
10744
10754
  const match = statusContent.match(/^##\s*Last done\s*\(UTC\)\s*\n([\s\S]*?)(?=\n##\s|\n*$)/im);
10745
- if (!match) return '_No "Last done" section found._';
10755
+ if (!match) return "No recent activity found.";
10746
10756
  const text = match[1]?.trim();
10747
- return text || "_Empty._";
10757
+ return text || "Empty.";
10748
10758
  }
10749
10759
  function extractTodayLearnings(learningsContent) {
10750
- if (!learningsContent) return "_No learnings recorded yet._";
10760
+ if (!learningsContent) return "No learnings recorded yet.";
10751
10761
  const today = (/* @__PURE__ */ new Date()).toISOString().slice(0, 10);
10752
10762
  const lines = learningsContent.split("\n");
10753
10763
  const todayLines = [];
@@ -10764,32 +10774,32 @@ function extractTodayLearnings(learningsContent) {
10764
10774
  todayLines.push(line);
10765
10775
  }
10766
10776
  }
10767
- return todayLines.length > 0 ? todayLines.join("\n") : "_None today._";
10777
+ return todayLines.length > 0 ? todayLines.join("\n") : "None today.";
10768
10778
  }
10769
10779
  function extractBlockers(todoContent) {
10770
- if (!todoContent) return "_No tasks recorded yet._";
10780
+ if (!todoContent) return "No tasks recorded yet.";
10771
10781
  const lines = todoContent.split("\n");
10772
10782
  const blockerLines = lines.filter(
10773
10783
  (l) => /\[BLOCKED\]/i.test(l) || /\bblocked\b/i.test(l)
10774
10784
  );
10775
- return blockerLines.length > 0 ? blockerLines.join("\n") : "_None._";
10785
+ return blockerLines.length > 0 ? blockerLines.join("\n") : "None.";
10776
10786
  }
10777
10787
  function extractNextActions(statusContent) {
10778
- if (!statusContent) return "_No status available yet._";
10788
+ if (!statusContent) return "No status available yet.";
10779
10789
  const match = statusContent.match(/^##\s*Next (?:3 )?actions(?: \(now\))?\s*\n([\s\S]*?)(?=\n##\s|\n*$)/im);
10780
- if (!match) return '_No "Next actions" section found._';
10790
+ if (!match) return "None yet.";
10781
10791
  const text = match[1]?.trim();
10782
- return text || "_Empty._";
10792
+ return text || "None yet.";
10783
10793
  }
10784
10794
  function extractUpcoming(statusContent) {
10785
- if (!statusContent) return "_No status available yet._";
10795
+ if (!statusContent) return "No status available yet.";
10786
10796
  const match = statusContent.match(/^##\s*Upcoming actions\s*\n([\s\S]*?)(?=\n##\s|\n*$)/im);
10787
- if (!match) return '_No "Upcoming actions" section found._';
10797
+ if (!match) return "None yet.";
10788
10798
  const text = match[1]?.trim();
10789
- return text || "_Empty._";
10799
+ return text || "None yet.";
10790
10800
  }
10791
10801
  function computeHealth(lastMessageAt, statusContent, blockers) {
10792
- if (blockers && blockers !== "_None._" && blockers !== "_No tasks recorded yet._") {
10802
+ if (blockers && blockers !== "None." && blockers !== "No tasks recorded yet.") {
10793
10803
  return "blocked";
10794
10804
  }
10795
10805
  if (!lastMessageAt) return "stale";
@@ -10799,12 +10809,139 @@ function computeHealth(lastMessageAt, statusContent, blockers) {
10799
10809
  }
10800
10810
 
10801
10811
  // src/commands/doctor-all.ts
10812
+ function getEligibility(entry, now, statusTimestamp) {
10813
+ if (entry.status === "archived") return { eligible: false, skipReason: "archived" };
10814
+ if (entry.snoozeUntil && new Date(entry.snoozeUntil).getTime() > now.getTime()) {
10815
+ return { eligible: false, skipReason: "snoozed" };
10816
+ }
10817
+ const lastActive = mostRecent(entry.lastMessageAt, statusTimestamp);
10818
+ if (lastActive) {
10819
+ const lastActiveMs = new Date(lastActive).getTime();
10820
+ const inactiveMs = INACTIVE_AFTER_DAYS * 24 * 60 * 60 * 1e3;
10821
+ if (now.getTime() - lastActiveMs > inactiveMs) {
10822
+ return { eligible: false, skipReason: "inactive" };
10823
+ }
10824
+ }
10825
+ if (entry.lastDoctorReportAt) {
10826
+ const lastReport = new Date(entry.lastDoctorReportAt).getTime();
10827
+ if (now.getTime() - lastReport < DOCTOR_PER_TOPIC_CAP_MS) {
10828
+ return { eligible: false, skipReason: "recently-checked" };
10829
+ }
10830
+ }
10831
+ return { eligible: true };
10832
+ }
10833
+ var SKIP_ICONS = {
10834
+ archived: "\u{1F4E6}",
10835
+ // 📦
10836
+ snoozed: "\u{1F4A4}",
10837
+ // 💤
10838
+ inactive: "\u{1F507}",
10839
+ // 🔇
10840
+ "recently-checked": "\u23F0"
10841
+ // ⏰
10842
+ };
10843
+ var SKIP_LABELS = {
10844
+ archived: "archived",
10845
+ snoozed: "snoozed",
10846
+ inactive: "inactive",
10847
+ "recently-checked": "recently checked"
10848
+ };
10849
+ var SUMMARY_SOFT_LIMIT = 3800;
10850
+ function buildDoctorAllSummary(data) {
10851
+ const {
10852
+ checkedTopics,
10853
+ skippedTopics,
10854
+ postFailures,
10855
+ dailyReportsSent,
10856
+ dailyReportsSkipped,
10857
+ hasPostFn,
10858
+ migrationGroups,
10859
+ errors
10860
+ } = data;
10861
+ if (checkedTopics.length === 0 && skippedTopics.length === 0) {
10862
+ return "**Health Check Summary**\n\nNo topics registered yet.";
10863
+ }
10864
+ const lines = ["**Health Check Summary**", ""];
10865
+ let checkedRendered = 0;
10866
+ for (const t of checkedTopics) {
10867
+ let icon;
10868
+ let label;
10869
+ switch (t.status) {
10870
+ case "checked":
10871
+ icon = "\u2705";
10872
+ label = "checked";
10873
+ break;
10874
+ case "check-failed":
10875
+ icon = "\u274C";
10876
+ label = "check failed";
10877
+ break;
10878
+ case "post-failed":
10879
+ icon = "\u26A0\uFE0F";
10880
+ label = "failed to post";
10881
+ break;
10882
+ }
10883
+ const line = `${icon} ${t.name} \u2014 ${label}`;
10884
+ if (lines.join("\n").length + line.length > SUMMARY_SOFT_LIMIT) {
10885
+ const remaining = checkedTopics.length - checkedRendered;
10886
+ lines.push(`... and ${remaining} more`);
10887
+ break;
10888
+ }
10889
+ lines.push(line);
10890
+ checkedRendered++;
10891
+ }
10892
+ if (skippedTopics.length > 0) {
10893
+ lines.push("");
10894
+ lines.push("\u23ED\uFE0F Skipped:");
10895
+ let skippedRendered = 0;
10896
+ for (const t of skippedTopics) {
10897
+ const icon = SKIP_ICONS[t.reason];
10898
+ const label = SKIP_LABELS[t.reason];
10899
+ const line = `${icon} ${t.name} \u2014 ${label}`;
10900
+ if (lines.join("\n").length + line.length > SUMMARY_SOFT_LIMIT) {
10901
+ const remaining = skippedTopics.length - skippedRendered;
10902
+ lines.push(`... and ${remaining} more`);
10903
+ break;
10904
+ }
10905
+ lines.push(line);
10906
+ skippedRendered++;
10907
+ }
10908
+ }
10909
+ if (postFailures > 0) {
10910
+ lines.push("");
10911
+ lines.push(`\u26A0\uFE0F ${postFailures} topic(s) failed to post`);
10912
+ }
10913
+ if (hasPostFn) {
10914
+ const parts = [];
10915
+ if (dailyReportsSent > 0) parts.push(`${dailyReportsSent} sent`);
10916
+ if (dailyReportsSkipped > 0) parts.push(`${dailyReportsSkipped} skipped`);
10917
+ if (parts.length > 0) {
10918
+ lines.push("");
10919
+ lines.push(`Daily reports: ${parts.join(", ")}`);
10920
+ }
10921
+ }
10922
+ if (migrationGroups > 0) {
10923
+ lines.push("");
10924
+ lines.push(`**Warning:** ${migrationGroups} group(s) had all topics fail. The group may have been migrated or deleted.`);
10925
+ }
10926
+ if (errors.length > 0) {
10927
+ lines.push("");
10928
+ lines.push(`**Errors (${errors.length}):**`);
10929
+ for (const e of errors.slice(0, 10)) {
10930
+ lines.push(`- ${e}`);
10931
+ }
10932
+ if (errors.length > 10) {
10933
+ lines.push(`... and ${errors.length - 10} more`);
10934
+ }
10935
+ }
10936
+ return truncateMessage(lines.join("\n"));
10937
+ }
10802
10938
  async function handleDoctorAll(ctx) {
10803
- const { workspaceDir, configDir, userId, logger } = ctx;
10939
+ const { workspaceDir, configDir, logger } = ctx;
10940
+ const registry = readRegistry(workspaceDir);
10941
+ const userId = ctx.userId ?? registry.topicManagerAdmins[0];
10804
10942
  if (!userId) {
10805
10943
  return { text: "Something went wrong \u2014 could not identify your user account." };
10806
10944
  }
10807
- const registry = readRegistry(workspaceDir);
10808
10945
  const auth = checkAuthorization(userId, "doctor-all", registry);
10809
10946
  if (!auth.authorized) {
10810
10947
  return { text: auth.message ?? "Not authorized." };
@@ -10833,12 +10970,16 @@ async function handleDoctorAll(ctx) {
10833
10970
  const allEntries = Object.entries(registry.topics);
10834
10971
  const reports = [];
10835
10972
  const errors = [];
10836
- let processed = 0;
10837
- let skipped = 0;
10973
+ const checkedTopics = [];
10974
+ const skippedTopics = [];
10838
10975
  const groupPostResults = /* @__PURE__ */ new Map();
10839
10976
  for (const [_key, entry] of allEntries) {
10840
- if (!isEligible(entry, now)) {
10841
- skipped++;
10977
+ const capsuleDir = path11.join(projectsBase, entry.slug);
10978
+ const statusForEligibility = readFileOrNull(path11.join(capsuleDir, "STATUS.md"));
10979
+ const statusTs = statusForEligibility ? extractStatusTimestamp(statusForEligibility) : null;
10980
+ const eligibility = getEligibility(entry, now, statusTs);
10981
+ if (!eligibility.eligible) {
10982
+ skippedTopics.push({ name: entry.name, reason: eligibility.skipReason });
10842
10983
  continue;
10843
10984
  }
10844
10985
  try {
@@ -10874,11 +11015,12 @@ async function handleDoctorAll(ctx) {
10874
11015
  }
10875
11016
  const group = groupPostResults.get(gk);
10876
11017
  group.total++;
10877
- processed++;
11018
+ checkedTopics.push({ name: entry.name, slug: entry.slug, status: "checked" });
10878
11019
  } catch (err) {
10879
11020
  const msg = err instanceof Error ? err.message : String(err);
10880
11021
  errors.push(`${entry.slug}: ${msg}`);
10881
11022
  logger.error(`[doctor-all] Error processing ${entry.slug}: ${msg}`);
11023
+ checkedTopics.push({ name: entry.name, slug: entry.slug, status: "check-failed" });
10882
11024
  const gk = entry.groupId;
10883
11025
  if (!groupPostResults.has(gk)) {
10884
11026
  groupPostResults.set(gk, { total: 0, failed: 0 });
@@ -10894,14 +11036,13 @@ async function handleDoctorAll(ctx) {
10894
11036
  migrationGroups.push(gid);
10895
11037
  }
10896
11038
  }
10897
- let postErrors = 0;
10898
- let postSuccesses = 0;
11039
+ let postFailures = 0;
11040
+ const postFailedSlugs = /* @__PURE__ */ new Set();
10899
11041
  if (ctx.postFn && reports.length > 0) {
10900
11042
  const rateLimitedPost = createRateLimitedPoster(ctx.postFn);
10901
11043
  for (const report of reports) {
10902
11044
  try {
10903
11045
  await rateLimitedPost(report.groupId, report.threadId, report.text, report.keyboard);
10904
- postSuccesses++;
10905
11046
  await withRegistry(workspaceDir, (data) => {
10906
11047
  const key = `${report.groupId}:${report.threadId}`;
10907
11048
  const entry = data.topics[key];
@@ -10911,7 +11052,8 @@ async function handleDoctorAll(ctx) {
10911
11052
  }
10912
11053
  });
10913
11054
  } catch (err) {
10914
- postErrors++;
11055
+ postFailures++;
11056
+ postFailedSlugs.add(report.slug);
10915
11057
  const msg = err instanceof Error ? err.message : String(err);
10916
11058
  logger.error(`[doctor-all] Post failed for ${report.slug}: ${msg}`);
10917
11059
  await withRegistry(workspaceDir, (data) => {
@@ -10923,6 +11065,11 @@ async function handleDoctorAll(ctx) {
10923
11065
  });
10924
11066
  }
10925
11067
  }
11068
+ for (const outcome of checkedTopics) {
11069
+ if (postFailedSlugs.has(outcome.slug) && outcome.status === "checked") {
11070
+ outcome.status = "post-failed";
11071
+ }
11072
+ }
10926
11073
  }
10927
11074
  let dailyReportSuccesses = 0;
10928
11075
  let dailyReportSkipped = 0;
@@ -10975,7 +11122,10 @@ async function handleDoctorAll(ctx) {
10975
11122
  await withRegistry(workspaceDir, (data) => {
10976
11123
  data.lastDoctorAllRunAt = now.toISOString();
10977
11124
  for (const [_key, entry] of Object.entries(data.topics)) {
10978
- if (!isEligible(entry, now)) continue;
11125
+ const dir = path11.join(projectsBase, entry.slug);
11126
+ const statusFile = readFileOrNull(path11.join(dir, "STATUS.md"));
11127
+ const ts = statusFile ? extractStatusTimestamp(statusFile) : null;
11128
+ if (!isEligible(entry, now, ts)) continue;
10979
11129
  if (entry.lastMessageAt) {
10980
11130
  const lastMsg = new Date(entry.lastMessageAt).getTime();
10981
11131
  const lastDoctor = entry.lastDoctorRunAt ? new Date(entry.lastDoctorRunAt).getTime() : 0;
@@ -11001,42 +11151,44 @@ async function handleDoctorAll(ctx) {
11001
11151
  }
11002
11152
  }
11003
11153
  });
11004
- const lines = [
11005
- `**Health Check Summary**`,
11006
- "",
11007
- `Checked: ${processed}`,
11008
- `Skipped: ${skipped}`,
11009
- `Total topics: ${allEntries.length}`
11010
- ];
11011
- if (ctx.postFn) {
11012
- lines.push(`Posted: ${postSuccesses}, Post failures: ${postErrors}`);
11013
- lines.push(`Daily reports: ${dailyReportSuccesses} sent, ${dailyReportSkipped} skipped`);
11014
- }
11015
- if (errors.length > 0) {
11016
- lines.push("");
11017
- lines.push(`**Errors (${errors.length}):**`);
11018
- for (const e of errors.slice(0, 10)) {
11019
- lines.push(`- ${e}`);
11020
- }
11021
- if (errors.length > 10) {
11022
- lines.push(`... and ${errors.length - 10} more`);
11023
- }
11024
- }
11025
- if (migrationGroups.length > 0) {
11026
- lines.push("");
11027
- lines.push(`**Warning:** ${migrationGroups.length} group(s) had all topics fail. The group may have been migrated or deleted.`);
11028
- }
11029
11154
  return {
11030
- text: lines.join("\n")
11155
+ text: buildDoctorAllSummary({
11156
+ checkedTopics,
11157
+ skippedTopics,
11158
+ postFailures,
11159
+ dailyReportsSent: dailyReportSuccesses,
11160
+ dailyReportsSkipped: dailyReportSkipped,
11161
+ hasPostFn: !!ctx.postFn,
11162
+ migrationGroups: migrationGroups.length,
11163
+ errors
11164
+ })
11031
11165
  };
11032
11166
  }
11033
- function isEligible(entry, now) {
11167
+ var STATUS_TIMESTAMP_RE = /^##\s*Last done\s*\(UTC\)/im;
11168
+ var ISO_TIMESTAMP_RE = /\d{4}-\d{2}-\d{2}T\d{2}:\d{2}/;
11169
+ function extractStatusTimestamp(content) {
11170
+ if (!STATUS_TIMESTAMP_RE.test(content)) return null;
11171
+ const idx = content.search(STATUS_TIMESTAMP_RE);
11172
+ const sectionAfter = content.slice(idx);
11173
+ const nextSection = sectionAfter.indexOf("\n## ", 1);
11174
+ const section = nextSection > 0 ? sectionAfter.slice(0, nextSection) : sectionAfter;
11175
+ const match = section.match(ISO_TIMESTAMP_RE);
11176
+ return match ? match[0] : null;
11177
+ }
11178
+ function mostRecent(a, b) {
11179
+ if (!a && !b) return null;
11180
+ if (!a) return b;
11181
+ if (!b) return a;
11182
+ return a > b ? a : b;
11183
+ }
11184
+ function isEligible(entry, now, statusTimestamp) {
11034
11185
  if (entry.status === "archived") return false;
11035
11186
  if (entry.snoozeUntil && new Date(entry.snoozeUntil).getTime() > now.getTime()) return false;
11036
- if (entry.lastMessageAt) {
11037
- const lastActive = new Date(entry.lastMessageAt).getTime();
11187
+ const lastActive = mostRecent(entry.lastMessageAt, statusTimestamp);
11188
+ if (lastActive) {
11189
+ const lastActiveMs = new Date(lastActive).getTime();
11038
11190
  const inactiveMs = INACTIVE_AFTER_DAYS * 24 * 60 * 60 * 1e3;
11039
- if (now.getTime() - lastActive > inactiveMs) return false;
11191
+ if (now.getTime() - lastActiveMs > inactiveMs) return false;
11040
11192
  }
11041
11193
  if (entry.lastDoctorReportAt) {
11042
11194
  const lastReport = new Date(entry.lastDoctorReportAt).getTime();
@@ -11105,18 +11257,18 @@ function formatStatus(name, content) {
11105
11257
  ""
11106
11258
  ];
11107
11259
  if (timestamp) {
11108
- lines.push(`**Last activity:** ${relativeTime(timestamp)}`);
11260
+ lines.push(`\u{1F552} **Last activity:** ${relativeTime(timestamp)}`);
11109
11261
  }
11110
11262
  if (lastDoneBody && !isPlaceholder(lastDoneBody)) {
11111
11263
  lines.push(lastDoneBody);
11112
11264
  }
11113
11265
  lines.push("");
11114
- lines.push("**Next actions**");
11266
+ lines.push("\u{1F3AF} **Next actions**");
11115
11267
  lines.push(formatSection(nextRaw));
11116
11268
  const upcomingFormatted = formatSection(upcomingRaw);
11117
11269
  if (upcomingFormatted !== "_None yet._") {
11118
11270
  lines.push("");
11119
- lines.push("**Upcoming**");
11271
+ lines.push("\u{1F4C5} **Upcoming**");
11120
11272
  lines.push(upcomingFormatted);
11121
11273
  }
11122
11274
  return lines.join("\n");
@@ -11560,13 +11712,13 @@ async function handleCallback(data, ctx) {
11560
11712
  ic: "coding",
11561
11713
  ir: "research",
11562
11714
  im: "marketing",
11563
- ix: "custom"
11715
+ ig: "general"
11564
11716
  };
11565
11717
  const initConfirmMap = {
11566
11718
  yc: "coding",
11567
11719
  yr: "research",
11568
11720
  ym: "marketing",
11569
- yx: "custom"
11721
+ yg: "general"
11570
11722
  };
11571
11723
  const cbCtx = { ...ctx, groupId: cbGroupId, threadId: cbThreadId, userId: cbUserId };
11572
11724
  if (action in initTypeMap) {
package/dist/setup.js CHANGED
@@ -1145,6 +1145,7 @@ import * as crypto from "node:crypto";
1145
1145
  import { execSync } from "node:child_process";
1146
1146
  import * as readline from "node:readline";
1147
1147
  var PLUGIN_NAME = "openclaw-telegram-manager";
1148
+ var PLUGIN_DISPLAY_NAME = "OpenClaw Telegram Manager";
1148
1149
  var PLUGIN_VERSION = JSON.parse(
1149
1150
  fs.readFileSync(new URL("../package.json", import.meta.url), "utf-8")
1150
1151
  ).version;
@@ -1254,7 +1255,7 @@ if (command === "setup") {
1254
1255
  process.exit(1);
1255
1256
  }
1256
1257
  async function runSetup() {
1257
- banner(PLUGIN_NAME, `v${PLUGIN_VERSION}`);
1258
+ banner(PLUGIN_DISPLAY_NAME, `v${PLUGIN_VERSION}`);
1258
1259
  startSpinner("Checking OpenClaw version\u2026");
1259
1260
  const version = checkOpenClawVersion();
1260
1261
  ok(`OpenClaw ${c.dim}${version}${c.reset}`);
@@ -1269,8 +1270,10 @@ async function runSetup() {
1269
1270
  startSpinner("Patching memory flush\u2026");
1270
1271
  patchMemoryFlush(configDir);
1271
1272
  startSpinner("Preparing workspace\u2026");
1272
- const projectsDir = path.join(configDir, "workspace", "projects");
1273
+ const workspaceDir = path.join(configDir, "workspace");
1274
+ const projectsDir = path.join(workspaceDir, "projects");
1273
1275
  ensureDir(projectsDir);
1276
+ ensureGitignore(workspaceDir);
1274
1277
  initRegistry(projectsDir);
1275
1278
  createEmptyInclude(configDir, existingGroups);
1276
1279
  ok("Workspace ready");
@@ -1290,7 +1293,7 @@ async function runSetup() {
1290
1293
  console.log("");
1291
1294
  }
1292
1295
  async function runUninstall() {
1293
- banner(PLUGIN_NAME, "uninstall");
1296
+ banner(PLUGIN_DISPLAY_NAME, "uninstall");
1294
1297
  startSpinner("Locating config\u2026");
1295
1298
  const configDir = locateConfigDir();
1296
1299
  ok(`Config ${c.dim}${configDir}${c.reset}`);
@@ -1497,6 +1500,29 @@ function createEmptyInclude(configDir, seedGroups) {
1497
1500
  ].join("\n");
1498
1501
  fs.writeFileSync(includePath, content, { mode: 384 });
1499
1502
  }
1503
+ var GITIGNORE_ENTRIES = [
1504
+ "projects/topics.json",
1505
+ "projects/audit.jsonl",
1506
+ "projects/*/.tm-backup/"
1507
+ ];
1508
+ function ensureGitignore(workspaceDir) {
1509
+ const gitignorePath = path.join(workspaceDir, ".gitignore");
1510
+ let content = "";
1511
+ try {
1512
+ if (fs.existsSync(gitignorePath)) {
1513
+ content = fs.readFileSync(gitignorePath, "utf-8");
1514
+ }
1515
+ } catch {
1516
+ }
1517
+ const lines = content.split("\n");
1518
+ const missing = GITIGNORE_ENTRIES.filter(
1519
+ (entry) => !lines.some((line) => line.trim() === entry)
1520
+ );
1521
+ if (missing.length === 0) return;
1522
+ const block = "\n# telegram-manager (operational files)\n" + missing.join("\n") + "\n";
1523
+ const newContent = content ? content.trimEnd() + "\n" + block : block.trimStart();
1524
+ fs.writeFileSync(gitignorePath, newContent);
1525
+ }
1500
1526
  function writeHeartbeat(configDir) {
1501
1527
  const heartbeatPath = path.join(configDir, "workspace", "HEARTBEAT.md");
1502
1528
  let content = "";