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.
- package/README.md +6 -5
- package/dist/commands/archive.js +2 -2
- package/dist/commands/archive.js.map +1 -1
- package/dist/commands/autopilot.d.ts +4 -0
- package/dist/commands/autopilot.d.ts.map +1 -1
- package/dist/commands/autopilot.js +6 -6
- package/dist/commands/autopilot.js.map +1 -1
- package/dist/commands/daily-report.d.ts +6 -0
- package/dist/commands/daily-report.d.ts.map +1 -1
- package/dist/commands/daily-report.js +13 -13
- package/dist/commands/daily-report.js.map +1 -1
- package/dist/commands/doctor-all.d.ts.map +1 -1
- package/dist/commands/doctor-all.js +67 -3
- package/dist/commands/doctor-all.js.map +1 -1
- package/dist/commands/doctor.js +1 -1
- package/dist/commands/doctor.js.map +1 -1
- package/dist/commands/init.d.ts.map +1 -1
- package/dist/commands/init.js +34 -6
- package/dist/commands/init.js.map +1 -1
- package/dist/commands/list.js +1 -1
- package/dist/commands/list.js.map +1 -1
- package/dist/commands/rename.js +2 -2
- package/dist/commands/rename.js.map +1 -1
- package/dist/commands/snooze.js +1 -1
- package/dist/commands/snooze.js.map +1 -1
- package/dist/commands/status.js +1 -1
- package/dist/commands/status.js.map +1 -1
- package/dist/commands/sync.js +1 -1
- package/dist/commands/sync.js.map +1 -1
- package/dist/commands/upgrade.d.ts.map +1 -1
- package/dist/commands/upgrade.js +6 -5
- package/dist/commands/upgrade.js.map +1 -1
- package/dist/index.js +2 -2
- package/dist/index.js.map +1 -1
- package/dist/lib/capsule.js +1 -1
- package/dist/lib/capsule.js.map +1 -1
- package/dist/lib/doctor-checks.js +1 -1
- package/dist/lib/doctor-checks.js.map +1 -1
- package/dist/lib/telegram.d.ts +2 -2
- package/dist/lib/telegram.d.ts.map +1 -1
- package/dist/lib/telegram.js +20 -31
- package/dist/lib/telegram.js.map +1 -1
- package/dist/plugin.js +450 -383
- package/dist/setup.js +46 -1
- package/dist/setup.js.map +1 -1
- package/package.json +1 -1
- package/skills/tm/SKILL.md +4 -4
package/dist/plugin.js
CHANGED
|
@@ -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(
|
|
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
|
|
9276
|
-
import * as
|
|
9275
|
+
import * as fs7 from "node:fs";
|
|
9276
|
+
import * as path7 from "node:path";
|
|
9277
9277
|
|
|
9278
9278
|
// src/lib/auth.ts
|
|
9279
9279
|
var AuthTier = {
|
|
@@ -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,
|
|
9409
|
+
function buildTopicCard(name, type) {
|
|
9410
9410
|
return [
|
|
9411
|
-
|
|
9412
|
-
|
|
9413
|
-
|
|
9411
|
+
`**${name}** is ready!`,
|
|
9412
|
+
"",
|
|
9413
|
+
`Type: ${type}`,
|
|
9414
9414
|
"",
|
|
9415
9415
|
"**How it works**",
|
|
9416
|
-
"
|
|
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
|
-
"
|
|
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
|
|
9423
|
+
"<b>Set up this topic</b>",
|
|
9430
9424
|
"",
|
|
9431
|
-
"
|
|
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
|
|
9435
|
-
"\u2022 <b>Research</b> \u2014
|
|
9436
|
-
"\u2022 <b>Marketing</b> \u2014
|
|
9437
|
-
"\u2022 <b>Custom</b> \u2014
|
|
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,
|
|
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
|
|
9460
|
-
|
|
9461
|
-
|
|
9454
|
+
`<b>\u2705 ${n}</b> is ready!`,
|
|
9455
|
+
"",
|
|
9456
|
+
`Type: ${t}`,
|
|
9462
9457
|
"",
|
|
9463
9458
|
"<b>How it works</b>",
|
|
9464
|
-
"
|
|
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
|
-
"
|
|
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: "
|
|
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 =
|
|
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 (
|
|
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 =
|
|
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,
|
|
10118
|
+
const topicCard = buildTopicCard(name, topicType);
|
|
9997
10119
|
if (ctx.postFn && groupId && threadId) {
|
|
9998
10120
|
try {
|
|
9999
|
-
const htmlCard = buildTopicCardHtml(name,
|
|
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: "
|
|
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: "
|
|
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
|
|
10113
|
-
import * as
|
|
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
|
|
10118
|
-
import * as
|
|
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 =
|
|
10125
|
-
if (!
|
|
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 =
|
|
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 =
|
|
10148
|
-
if (!
|
|
10149
|
-
if (!
|
|
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 (!
|
|
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 (!
|
|
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
|
-
`
|
|
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 &&
|
|
10425
|
+
if (cronJobsPath && fs8.existsSync(cronJobsPath)) {
|
|
10304
10426
|
try {
|
|
10305
|
-
const jobsRaw =
|
|
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 =
|
|
10529
|
+
const capsuleDir = path8.join(projectsBase, entry.slug);
|
|
10408
10530
|
results.push(...runRegistryChecks(entry, projectsBase));
|
|
10409
|
-
if (!
|
|
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 =
|
|
10440
|
-
const backupDir =
|
|
10441
|
-
if (!
|
|
10442
|
-
|
|
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 =
|
|
10446
|
-
const dst =
|
|
10447
|
-
if (
|
|
10448
|
-
|
|
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 =
|
|
10593
|
+
const filePath = path8.join(capsuleDir, name);
|
|
10472
10594
|
try {
|
|
10473
|
-
if (
|
|
10474
|
-
files.set(name,
|
|
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: "
|
|
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 =
|
|
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 =
|
|
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 (
|
|
10510
|
-
includeContent =
|
|
10631
|
+
if (fs9.existsSync(incPath)) {
|
|
10632
|
+
includeContent = fs9.readFileSync(incPath, "utf-8");
|
|
10511
10633
|
}
|
|
10512
10634
|
} catch {
|
|
10513
10635
|
}
|
|
10514
|
-
const cronJobsPath =
|
|
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
|
|
10544
|
-
import * as
|
|
10545
|
-
|
|
10546
|
-
|
|
10547
|
-
|
|
10548
|
-
|
|
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
|
|
10552
|
-
if (!
|
|
10553
|
-
return { text:
|
|
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 (
|
|
10556
|
-
const
|
|
10557
|
-
const
|
|
10558
|
-
if (
|
|
10559
|
-
|
|
10560
|
-
return {
|
|
10561
|
-
text: `Health checks were run ${Math.floor(elapsed / 6e4)} minutes ago. Try again in ${remainingMin} minute(s).`
|
|
10562
|
-
};
|
|
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
|
|
10566
|
-
const
|
|
10567
|
-
|
|
10568
|
-
|
|
10569
|
-
|
|
10570
|
-
|
|
10571
|
-
|
|
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
|
-
|
|
10576
|
-
|
|
10577
|
-
|
|
10578
|
-
const
|
|
10579
|
-
|
|
10580
|
-
|
|
10581
|
-
|
|
10582
|
-
|
|
10583
|
-
|
|
10584
|
-
|
|
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: "
|
|
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
|
|
10756
|
-
import * as
|
|
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
|
|
11126
|
+
async function handleStatus2(ctx) {
|
|
10813
11127
|
const { workspaceDir, userId, groupId, threadId } = ctx;
|
|
10814
11128
|
if (!userId || !groupId || !threadId) {
|
|
10815
|
-
return { text: "
|
|
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 =
|
|
10828
|
-
const capsuleDir =
|
|
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 =
|
|
10836
|
-
if (!
|
|
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 =
|
|
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: "
|
|
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: "
|
|
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:
|
|
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
|
|
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: "
|
|
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 =
|
|
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
|
|
10974
|
-
|
|
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.${
|
|
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: "
|
|
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: "
|
|
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:
|
|
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
|
|
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
|
|
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
|
|
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) {
|