openclaw-telegram-manager 2.5.7 → 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,6 +10809,132 @@ 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
10939
  const { workspaceDir, configDir, logger } = ctx;
10804
10940
  const registry = readRegistry(workspaceDir);
@@ -10834,12 +10970,16 @@ async function handleDoctorAll(ctx) {
10834
10970
  const allEntries = Object.entries(registry.topics);
10835
10971
  const reports = [];
10836
10972
  const errors = [];
10837
- let processed = 0;
10838
- let skipped = 0;
10973
+ const checkedTopics = [];
10974
+ const skippedTopics = [];
10839
10975
  const groupPostResults = /* @__PURE__ */ new Map();
10840
10976
  for (const [_key, entry] of allEntries) {
10841
- if (!isEligible(entry, now)) {
10842
- 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 });
10843
10983
  continue;
10844
10984
  }
10845
10985
  try {
@@ -10875,11 +11015,12 @@ async function handleDoctorAll(ctx) {
10875
11015
  }
10876
11016
  const group = groupPostResults.get(gk);
10877
11017
  group.total++;
10878
- processed++;
11018
+ checkedTopics.push({ name: entry.name, slug: entry.slug, status: "checked" });
10879
11019
  } catch (err) {
10880
11020
  const msg = err instanceof Error ? err.message : String(err);
10881
11021
  errors.push(`${entry.slug}: ${msg}`);
10882
11022
  logger.error(`[doctor-all] Error processing ${entry.slug}: ${msg}`);
11023
+ checkedTopics.push({ name: entry.name, slug: entry.slug, status: "check-failed" });
10883
11024
  const gk = entry.groupId;
10884
11025
  if (!groupPostResults.has(gk)) {
10885
11026
  groupPostResults.set(gk, { total: 0, failed: 0 });
@@ -10895,14 +11036,13 @@ async function handleDoctorAll(ctx) {
10895
11036
  migrationGroups.push(gid);
10896
11037
  }
10897
11038
  }
10898
- let postErrors = 0;
10899
- let postSuccesses = 0;
11039
+ let postFailures = 0;
11040
+ const postFailedSlugs = /* @__PURE__ */ new Set();
10900
11041
  if (ctx.postFn && reports.length > 0) {
10901
11042
  const rateLimitedPost = createRateLimitedPoster(ctx.postFn);
10902
11043
  for (const report of reports) {
10903
11044
  try {
10904
11045
  await rateLimitedPost(report.groupId, report.threadId, report.text, report.keyboard);
10905
- postSuccesses++;
10906
11046
  await withRegistry(workspaceDir, (data) => {
10907
11047
  const key = `${report.groupId}:${report.threadId}`;
10908
11048
  const entry = data.topics[key];
@@ -10912,7 +11052,8 @@ async function handleDoctorAll(ctx) {
10912
11052
  }
10913
11053
  });
10914
11054
  } catch (err) {
10915
- postErrors++;
11055
+ postFailures++;
11056
+ postFailedSlugs.add(report.slug);
10916
11057
  const msg = err instanceof Error ? err.message : String(err);
10917
11058
  logger.error(`[doctor-all] Post failed for ${report.slug}: ${msg}`);
10918
11059
  await withRegistry(workspaceDir, (data) => {
@@ -10924,6 +11065,11 @@ async function handleDoctorAll(ctx) {
10924
11065
  });
10925
11066
  }
10926
11067
  }
11068
+ for (const outcome of checkedTopics) {
11069
+ if (postFailedSlugs.has(outcome.slug) && outcome.status === "checked") {
11070
+ outcome.status = "post-failed";
11071
+ }
11072
+ }
10927
11073
  }
10928
11074
  let dailyReportSuccesses = 0;
10929
11075
  let dailyReportSkipped = 0;
@@ -10976,7 +11122,10 @@ async function handleDoctorAll(ctx) {
10976
11122
  await withRegistry(workspaceDir, (data) => {
10977
11123
  data.lastDoctorAllRunAt = now.toISOString();
10978
11124
  for (const [_key, entry] of Object.entries(data.topics)) {
10979
- 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;
10980
11129
  if (entry.lastMessageAt) {
10981
11130
  const lastMsg = new Date(entry.lastMessageAt).getTime();
10982
11131
  const lastDoctor = entry.lastDoctorRunAt ? new Date(entry.lastDoctorRunAt).getTime() : 0;
@@ -11002,42 +11151,44 @@ async function handleDoctorAll(ctx) {
11002
11151
  }
11003
11152
  }
11004
11153
  });
11005
- const lines = [
11006
- `**Health Check Summary**`,
11007
- "",
11008
- `Checked: ${processed}`,
11009
- `Skipped: ${skipped}`,
11010
- `Total topics: ${allEntries.length}`
11011
- ];
11012
- if (ctx.postFn) {
11013
- lines.push(`Posted: ${postSuccesses}, Post failures: ${postErrors}`);
11014
- lines.push(`Daily reports: ${dailyReportSuccesses} sent, ${dailyReportSkipped} skipped`);
11015
- }
11016
- if (errors.length > 0) {
11017
- lines.push("");
11018
- lines.push(`**Errors (${errors.length}):**`);
11019
- for (const e of errors.slice(0, 10)) {
11020
- lines.push(`- ${e}`);
11021
- }
11022
- if (errors.length > 10) {
11023
- lines.push(`... and ${errors.length - 10} more`);
11024
- }
11025
- }
11026
- if (migrationGroups.length > 0) {
11027
- lines.push("");
11028
- lines.push(`**Warning:** ${migrationGroups.length} group(s) had all topics fail. The group may have been migrated or deleted.`);
11029
- }
11030
11154
  return {
11031
- 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
+ })
11032
11165
  };
11033
11166
  }
11034
- 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) {
11035
11185
  if (entry.status === "archived") return false;
11036
11186
  if (entry.snoozeUntil && new Date(entry.snoozeUntil).getTime() > now.getTime()) return false;
11037
- if (entry.lastMessageAt) {
11038
- const lastActive = new Date(entry.lastMessageAt).getTime();
11187
+ const lastActive = mostRecent(entry.lastMessageAt, statusTimestamp);
11188
+ if (lastActive) {
11189
+ const lastActiveMs = new Date(lastActive).getTime();
11039
11190
  const inactiveMs = INACTIVE_AFTER_DAYS * 24 * 60 * 60 * 1e3;
11040
- if (now.getTime() - lastActive > inactiveMs) return false;
11191
+ if (now.getTime() - lastActiveMs > inactiveMs) return false;
11041
11192
  }
11042
11193
  if (entry.lastDoctorReportAt) {
11043
11194
  const lastReport = new Date(entry.lastDoctorReportAt).getTime();
@@ -11106,18 +11257,18 @@ function formatStatus(name, content) {
11106
11257
  ""
11107
11258
  ];
11108
11259
  if (timestamp) {
11109
- lines.push(`**Last activity:** ${relativeTime(timestamp)}`);
11260
+ lines.push(`\u{1F552} **Last activity:** ${relativeTime(timestamp)}`);
11110
11261
  }
11111
11262
  if (lastDoneBody && !isPlaceholder(lastDoneBody)) {
11112
11263
  lines.push(lastDoneBody);
11113
11264
  }
11114
11265
  lines.push("");
11115
- lines.push("**Next actions**");
11266
+ lines.push("\u{1F3AF} **Next actions**");
11116
11267
  lines.push(formatSection(nextRaw));
11117
11268
  const upcomingFormatted = formatSection(upcomingRaw);
11118
11269
  if (upcomingFormatted !== "_None yet._") {
11119
11270
  lines.push("");
11120
- lines.push("**Upcoming**");
11271
+ lines.push("\u{1F4C5} **Upcoming**");
11121
11272
  lines.push(upcomingFormatted);
11122
11273
  }
11123
11274
  return lines.join("\n");
@@ -11561,13 +11712,13 @@ async function handleCallback(data, ctx) {
11561
11712
  ic: "coding",
11562
11713
  ir: "research",
11563
11714
  im: "marketing",
11564
- ix: "custom"
11715
+ ig: "general"
11565
11716
  };
11566
11717
  const initConfirmMap = {
11567
11718
  yc: "coding",
11568
11719
  yr: "research",
11569
11720
  ym: "marketing",
11570
- yx: "custom"
11721
+ yg: "general"
11571
11722
  };
11572
11723
  const cbCtx = { ...ctx, groupId: cbGroupId, threadId: cbThreadId, userId: cbUserId };
11573
11724
  if (action in initTypeMap) {
package/dist/setup.js CHANGED
@@ -1270,8 +1270,10 @@ async function runSetup() {
1270
1270
  startSpinner("Patching memory flush\u2026");
1271
1271
  patchMemoryFlush(configDir);
1272
1272
  startSpinner("Preparing workspace\u2026");
1273
- const projectsDir = path.join(configDir, "workspace", "projects");
1273
+ const workspaceDir = path.join(configDir, "workspace");
1274
+ const projectsDir = path.join(workspaceDir, "projects");
1274
1275
  ensureDir(projectsDir);
1276
+ ensureGitignore(workspaceDir);
1275
1277
  initRegistry(projectsDir);
1276
1278
  createEmptyInclude(configDir, existingGroups);
1277
1279
  ok("Workspace ready");
@@ -1498,6 +1500,29 @@ function createEmptyInclude(configDir, seedGroups) {
1498
1500
  ].join("\n");
1499
1501
  fs.writeFileSync(includePath, content, { mode: 384 });
1500
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
+ }
1501
1526
  function writeHeartbeat(configDir) {
1502
1527
  const heartbeatPath = path.join(configDir, "workspace", "HEARTBEAT.md");
1503
1528
  let content = "";