openclaw-telegram-manager 2.3.2 → 2.4.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/README.md +74 -58
- package/dist/commands/autopilot.d.ts.map +1 -1
- package/dist/commands/autopilot.js +9 -6
- package/dist/commands/autopilot.js.map +1 -1
- package/dist/commands/daily-report.d.ts.map +1 -1
- package/dist/commands/daily-report.js +11 -10
- 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 +6 -9
- package/dist/commands/doctor-all.js.map +1 -1
- package/dist/commands/doctor.d.ts.map +1 -1
- package/dist/commands/doctor.js +3 -11
- package/dist/commands/doctor.js.map +1 -1
- package/dist/commands/init.d.ts.map +1 -1
- package/dist/commands/init.js +22 -32
- package/dist/commands/init.js.map +1 -1
- package/dist/commands/rename.d.ts.map +1 -1
- package/dist/commands/rename.js +2 -4
- package/dist/commands/rename.js.map +1 -1
- package/dist/commands/snooze.js +2 -2
- package/dist/commands/snooze.js.map +1 -1
- package/dist/commands/status.d.ts.map +1 -1
- package/dist/commands/status.js +74 -6
- 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.js +2 -2
- package/dist/commands/upgrade.js.map +1 -1
- package/dist/lib/capsule.js +3 -3
- package/dist/lib/capsule.js.map +1 -1
- package/dist/lib/doctor-checks.d.ts.map +1 -1
- package/dist/lib/doctor-checks.js +26 -59
- package/dist/lib/doctor-checks.js.map +1 -1
- package/dist/lib/include-generator.d.ts +1 -1
- package/dist/lib/include-generator.js +4 -4
- package/dist/lib/include-generator.js.map +1 -1
- package/dist/lib/registry.d.ts.map +1 -1
- package/dist/lib/registry.js +3 -0
- package/dist/lib/registry.js.map +1 -1
- package/dist/lib/telegram.d.ts +9 -4
- package/dist/lib/telegram.d.ts.map +1 -1
- package/dist/lib/telegram.js +87 -76
- package/dist/lib/telegram.js.map +1 -1
- package/dist/lib/types.d.ts +2 -2
- package/dist/lib/types.js +1 -1
- package/dist/lib/types.js.map +1 -1
- package/dist/plugin.js +288 -291
- package/dist/setup.js +3 -3
- package/dist/setup.js.map +1 -1
- package/dist/tool.js +0 -18
- package/dist/tool.js.map +1 -1
- package/package.json +10 -1
- package/skills/tm/SKILL.md +4 -4
package/dist/plugin.js
CHANGED
|
@@ -8850,9 +8850,9 @@ var TopicEntrySchema = Type.Object({
|
|
|
8850
8850
|
lastMessageAt: Type.Union([Type.String(), Type.Null()]),
|
|
8851
8851
|
lastDoctorReportAt: Type.Union([Type.String(), Type.Null()]),
|
|
8852
8852
|
lastDoctorRunAt: Type.Union([Type.String(), Type.Null()]),
|
|
8853
|
+
lastDailyReportAt: Type.Union([Type.String(), Type.Null()]),
|
|
8853
8854
|
lastCapsuleWriteAt: Type.Union([Type.String(), Type.Null()]),
|
|
8854
8855
|
snoozeUntil: Type.Union([Type.String(), Type.Null()]),
|
|
8855
|
-
ignoreChecks: Type.Array(Type.String()),
|
|
8856
8856
|
consecutiveSilentDoctors: Type.Integer({ minimum: 0 }),
|
|
8857
8857
|
lastPostError: Type.Union([Type.String({ maxLength: MAX_POST_ERROR_LENGTH }), Type.Null()]),
|
|
8858
8858
|
extras: Type.Record(Type.String(), Type.Unknown())
|
|
@@ -8925,6 +8925,9 @@ var migrations = {
|
|
|
8925
8925
|
if (entry["lastCapsuleWriteAt"] === void 0) {
|
|
8926
8926
|
entry["lastCapsuleWriteAt"] = null;
|
|
8927
8927
|
}
|
|
8928
|
+
if (entry["lastDailyReportAt"] === void 0) {
|
|
8929
|
+
entry["lastDailyReportAt"] = null;
|
|
8930
|
+
}
|
|
8928
8931
|
}
|
|
8929
8932
|
}
|
|
8930
8933
|
return data;
|
|
@@ -9128,21 +9131,19 @@ _Describe what this topic is about._
|
|
|
9128
9131
|
`,
|
|
9129
9132
|
"STATUS.md": (name) => `# Status: ${name}
|
|
9130
9133
|
|
|
9131
|
-
> This file is maintained by the agent \u2014 just send messages in the chat.
|
|
9132
|
-
|
|
9133
9134
|
## Last done (UTC)
|
|
9134
9135
|
|
|
9135
9136
|
${(/* @__PURE__ */ new Date()).toISOString()}
|
|
9136
9137
|
|
|
9137
|
-
|
|
9138
|
+
Topic created. Waiting for first instructions.
|
|
9138
9139
|
|
|
9139
9140
|
## Next actions (now)
|
|
9140
9141
|
|
|
9141
|
-
|
|
9142
|
+
_None yet._
|
|
9142
9143
|
|
|
9143
9144
|
## Upcoming actions
|
|
9144
9145
|
|
|
9145
|
-
|
|
9146
|
+
_None yet._
|
|
9146
9147
|
`,
|
|
9147
9148
|
"TODO.md": (name) => `# TODO: ${name}
|
|
9148
9149
|
|
|
@@ -9211,7 +9212,7 @@ function scaffoldCapsule(projectsBase, slug, name, type) {
|
|
|
9211
9212
|
throw new Error(`Path escapes projects directory: ${slug}`);
|
|
9212
9213
|
}
|
|
9213
9214
|
if (rejectSymlink(projectsBase)) {
|
|
9214
|
-
throw new Error(
|
|
9215
|
+
throw new Error("Detected an unsafe file system configuration (symlink)");
|
|
9215
9216
|
}
|
|
9216
9217
|
fs4.mkdirSync(capsuleDir, { recursive: false });
|
|
9217
9218
|
for (const file of BASE_FILES) {
|
|
@@ -9239,7 +9240,7 @@ function upgradeCapsule(projectsBase, slug, name, type, currentVersion) {
|
|
|
9239
9240
|
throw new Error(`Path escapes projects directory: ${slug}`);
|
|
9240
9241
|
}
|
|
9241
9242
|
if (rejectSymlink(capsuleDir)) {
|
|
9242
|
-
throw new Error(`
|
|
9243
|
+
throw new Error(`Topic directory is a symlink: ${capsuleDir}`);
|
|
9243
9244
|
}
|
|
9244
9245
|
const addedFiles = [];
|
|
9245
9246
|
for (const file of BASE_FILES) {
|
|
@@ -9329,27 +9330,39 @@ function checkAuthorization(userId, command, registry, topicAllowFrom) {
|
|
|
9329
9330
|
|
|
9330
9331
|
// src/lib/telegram.ts
|
|
9331
9332
|
var TELEGRAM_MSG_LIMIT = 4096;
|
|
9332
|
-
|
|
9333
|
-
|
|
9333
|
+
var HEALTH_LABELS = {
|
|
9334
|
+
fresh: "\u2705 Active",
|
|
9335
|
+
// green check
|
|
9336
|
+
stale: "\u23F3 Inactive",
|
|
9337
|
+
// hourglass
|
|
9338
|
+
blocked: "\u26A0\uFE0F Blocked"
|
|
9339
|
+
// warning
|
|
9340
|
+
};
|
|
9341
|
+
function buildDailyReport(data, format = "html") {
|
|
9342
|
+
const isHtml = format === "html";
|
|
9343
|
+
const esc = (s) => isHtml ? htmlEscape(s) : s;
|
|
9344
|
+
const bold = (s) => isHtml ? `<b>${s}</b>` : `**${s}**`;
|
|
9345
|
+
const n = esc(data.name);
|
|
9346
|
+
const healthLabel = HEALTH_LABELS[data.health] ?? data.health;
|
|
9334
9347
|
const lines = [
|
|
9335
|
-
|
|
9348
|
+
bold(`Daily Report: ${n}`),
|
|
9336
9349
|
"",
|
|
9337
|
-
|
|
9338
|
-
|
|
9350
|
+
bold("Done today"),
|
|
9351
|
+
esc(data.doneContent),
|
|
9339
9352
|
"",
|
|
9340
|
-
|
|
9341
|
-
|
|
9353
|
+
bold("New learnings"),
|
|
9354
|
+
esc(data.learningsContent),
|
|
9342
9355
|
"",
|
|
9343
|
-
|
|
9344
|
-
|
|
9356
|
+
bold("Blockers/Risks"),
|
|
9357
|
+
esc(data.blockersContent),
|
|
9345
9358
|
"",
|
|
9346
|
-
|
|
9347
|
-
|
|
9359
|
+
bold("Next actions (now)"),
|
|
9360
|
+
esc(data.nextContent),
|
|
9348
9361
|
"",
|
|
9349
|
-
|
|
9350
|
-
|
|
9362
|
+
bold("Upcoming"),
|
|
9363
|
+
esc(data.upcomingContent),
|
|
9351
9364
|
"",
|
|
9352
|
-
|
|
9365
|
+
`${bold("Health:")} ${healthLabel}`
|
|
9353
9366
|
];
|
|
9354
9367
|
return truncateMessage(lines.join("\n"));
|
|
9355
9368
|
}
|
|
@@ -9364,13 +9377,9 @@ function buildDoctorButtons(groupId, threadId, secret, userId) {
|
|
|
9364
9377
|
const cb = (action) => buildCallbackData(action, groupId, threadId, secret, userId);
|
|
9365
9378
|
return buildInlineKeyboard([
|
|
9366
9379
|
[
|
|
9367
|
-
{ text: "Fix", callback_data: cb("fix") },
|
|
9368
9380
|
{ text: "Snooze 7d", callback_data: cb("snooze7d") },
|
|
9369
|
-
{ text: "Snooze 30d", callback_data: cb("snooze30d") }
|
|
9370
|
-
|
|
9371
|
-
[
|
|
9372
|
-
{ text: "Archive", callback_data: cb("archive") },
|
|
9373
|
-
{ text: "Ignore check", callback_data: cb("ignore") }
|
|
9381
|
+
{ text: "Snooze 30d", callback_data: cb("snooze30d") },
|
|
9382
|
+
{ text: "Archive topic", callback_data: cb("archive") }
|
|
9374
9383
|
]
|
|
9375
9384
|
]);
|
|
9376
9385
|
}
|
|
@@ -9397,23 +9406,19 @@ function buildInitConfirmButton(groupId, threadId, secret, userId, type) {
|
|
|
9397
9406
|
const cb = buildCallbackData(actionMap[type], groupId, threadId, secret, userId);
|
|
9398
9407
|
return buildInlineKeyboard([[{ text: "Use this name", callback_data: cb }]]);
|
|
9399
9408
|
}
|
|
9400
|
-
function buildTopicCard(name, slug, type
|
|
9409
|
+
function buildTopicCard(name, slug, type) {
|
|
9401
9410
|
return [
|
|
9402
9411
|
`**Topic: ${name}**`,
|
|
9403
|
-
|
|
9404
|
-
|
|
9412
|
+
`**Type:** ${type}`,
|
|
9413
|
+
`**Stored in:** projects/${slug}/`,
|
|
9405
9414
|
"",
|
|
9406
9415
|
"**How it works**",
|
|
9407
|
-
"
|
|
9408
|
-
"maintains STATUS.md and TODO.md automatically as it",
|
|
9409
|
-
"works \u2014 nothing is lost on reset or context compaction.",
|
|
9410
|
-
"Doctor checks run periodically and alert you if anything",
|
|
9411
|
-
"needs attention.",
|
|
9416
|
+
"Talk to the AI in this topic like you normally would \u2014 describe what you need, ask questions, or give instructions. Progress, TODOs, and decisions are tracked automatically so nothing is lost between sessions.",
|
|
9412
9417
|
"",
|
|
9413
|
-
"**
|
|
9414
|
-
"/tm status \u2014
|
|
9418
|
+
"**Available commands:**",
|
|
9419
|
+
"/tm status \u2014 see current progress",
|
|
9415
9420
|
"/tm doctor \u2014 run health checks",
|
|
9416
|
-
"/tm rename
|
|
9421
|
+
"/tm rename new-name \u2014 rename this topic",
|
|
9417
9422
|
"/tm list \u2014 all topics",
|
|
9418
9423
|
"/tm archive \u2014 archive this topic",
|
|
9419
9424
|
"/tm help \u2014 full command reference"
|
|
@@ -9423,7 +9428,7 @@ function buildInitWelcomeHtml() {
|
|
|
9423
9428
|
return [
|
|
9424
9429
|
"<b>Set up a new topic workcell</b>",
|
|
9425
9430
|
"",
|
|
9426
|
-
"A workcell gives this topic
|
|
9431
|
+
"A workcell gives this topic a persistent memory \u2014 The AI writes status, TODOs, and notes to disk so context survives between sessions.",
|
|
9427
9432
|
"",
|
|
9428
9433
|
"<b>Pick a type:</b>",
|
|
9429
9434
|
"\u2022 <b>Coding</b> \u2014 adds ARCHITECTURE.md + DEPLOY.md",
|
|
@@ -9441,59 +9446,61 @@ function buildInitNameConfirmHtml(name, type) {
|
|
|
9441
9446
|
`Name: <b>${n}</b>`,
|
|
9442
9447
|
`Type: ${t}`,
|
|
9443
9448
|
"",
|
|
9444
|
-
"
|
|
9449
|
+
"You'll see this name in reports and health checks.",
|
|
9445
9450
|
"",
|
|
9446
9451
|
`For a custom name: <code>/tm init your-name ${t}</code>`
|
|
9447
9452
|
].join("\n");
|
|
9448
9453
|
}
|
|
9449
|
-
function buildTopicCardHtml(name, slug, type
|
|
9454
|
+
function buildTopicCardHtml(name, slug, type) {
|
|
9450
9455
|
const n = htmlEscape(name);
|
|
9451
9456
|
const s = htmlEscape(slug);
|
|
9452
9457
|
const t = htmlEscape(type);
|
|
9453
9458
|
return [
|
|
9454
9459
|
`<b>Topic: ${n}</b>`,
|
|
9455
|
-
|
|
9456
|
-
|
|
9460
|
+
`<b>Type:</b> ${t}`,
|
|
9461
|
+
`<b>Stored in:</b> <code>projects/${s}/</code>`,
|
|
9457
9462
|
"",
|
|
9458
9463
|
"<b>How it works</b>",
|
|
9459
|
-
"
|
|
9460
|
-
"maintains STATUS.md and TODO.md automatically as it",
|
|
9461
|
-
"works \u2014 nothing is lost on reset or context compaction.",
|
|
9462
|
-
"Doctor checks run periodically and alert you if anything",
|
|
9463
|
-
"needs attention.",
|
|
9464
|
+
"Talk to the AI in this topic like you normally would \u2014 describe what you need, ask questions, or give instructions. Progress, TODOs, and decisions are tracked automatically so nothing is lost between sessions.",
|
|
9464
9465
|
"",
|
|
9465
|
-
"<b>
|
|
9466
|
-
"/tm status \u2014
|
|
9466
|
+
"<b>Available commands:</b>",
|
|
9467
|
+
"/tm status \u2014 see current progress",
|
|
9467
9468
|
"/tm doctor \u2014 run health checks",
|
|
9468
|
-
"/tm rename
|
|
9469
|
+
"/tm rename new-name \u2014 rename this topic",
|
|
9469
9470
|
"/tm list \u2014 all topics",
|
|
9470
9471
|
"/tm archive \u2014 archive this topic",
|
|
9471
9472
|
"/tm help \u2014 full command reference"
|
|
9472
9473
|
].join("\n");
|
|
9473
9474
|
}
|
|
9475
|
+
function formatCommands(text, isHtml) {
|
|
9476
|
+
return text.replace(
|
|
9477
|
+
/\/tm\s\S+(?:\s\S+)*/g,
|
|
9478
|
+
(match) => isHtml ? `<code>${htmlEscape(match)}</code>` : `\`${match}\``
|
|
9479
|
+
);
|
|
9480
|
+
}
|
|
9474
9481
|
function buildDoctorReport(name, results, format = "markdown") {
|
|
9475
9482
|
const isHtml = format === "html";
|
|
9476
9483
|
const n = isHtml ? htmlEscape(name) : name;
|
|
9477
9484
|
const bold = (s) => isHtml ? `<b>${s}</b>` : `**${s}**`;
|
|
9478
|
-
const
|
|
9479
|
-
const
|
|
9480
|
-
if (
|
|
9481
|
-
lines.push("All
|
|
9485
|
+
const lines = [bold(`Health check: ${n}`), ""];
|
|
9486
|
+
const significant = results.filter((r) => r.severity !== Severity.INFO);
|
|
9487
|
+
if (significant.length === 0) {
|
|
9488
|
+
lines.push("All good \u2014 no issues found.");
|
|
9482
9489
|
return lines.join("\n");
|
|
9483
9490
|
}
|
|
9484
|
-
for (
|
|
9491
|
+
for (let i = 0; i < significant.length; i++) {
|
|
9492
|
+
const r = significant[i];
|
|
9485
9493
|
const icon = severityIcon(r.severity);
|
|
9486
9494
|
const msg = isHtml ? htmlEscape(r.message) : r.message;
|
|
9487
|
-
|
|
9488
|
-
const fix = r.fixable ? " [fixable]" : "";
|
|
9489
|
-
lines.push(`${icon} ${code(checkId)}: ${msg}${fix}`);
|
|
9495
|
+
lines.push(`${icon} ${msg}`);
|
|
9490
9496
|
if (r.remediation) {
|
|
9491
|
-
const rem =
|
|
9492
|
-
lines.push(`
|
|
9497
|
+
const rem = formatCommands(r.remediation, isHtml);
|
|
9498
|
+
lines.push(` \u2192 ${rem}`);
|
|
9499
|
+
}
|
|
9500
|
+
if (i < significant.length - 1) {
|
|
9501
|
+
lines.push("");
|
|
9493
9502
|
}
|
|
9494
9503
|
}
|
|
9495
|
-
lines.push("");
|
|
9496
|
-
lines.push("Reply /tm doctor to re-check, or use the buttons below.");
|
|
9497
9504
|
return truncateMessage(lines.join("\n"));
|
|
9498
9505
|
}
|
|
9499
9506
|
function severityIcon(severity) {
|
|
@@ -9515,38 +9522,44 @@ function buildHelpCard() {
|
|
|
9515
9522
|
return [
|
|
9516
9523
|
"**Topic Manager Commands**",
|
|
9517
9524
|
"",
|
|
9518
|
-
"
|
|
9519
|
-
"/tm
|
|
9520
|
-
"/tm
|
|
9521
|
-
"/tm doctor --all \u2014 check all topics",
|
|
9522
|
-
"/tm rename <name> \u2014 rename this topic",
|
|
9525
|
+
"**Basics**",
|
|
9526
|
+
"/tm init \u2014 set up this topic",
|
|
9527
|
+
"/tm status \u2014 see current progress",
|
|
9523
9528
|
"/tm list \u2014 all topics",
|
|
9524
|
-
"/tm
|
|
9525
|
-
"
|
|
9526
|
-
"
|
|
9527
|
-
"/tm
|
|
9528
|
-
"/tm
|
|
9529
|
-
"/tm
|
|
9530
|
-
"/tm
|
|
9531
|
-
"/tm
|
|
9529
|
+
"/tm help \u2014 this message",
|
|
9530
|
+
"",
|
|
9531
|
+
"**Health & reports**",
|
|
9532
|
+
"/tm doctor \u2014 run health checks",
|
|
9533
|
+
"/tm doctor --all \u2014 check all topics at once",
|
|
9534
|
+
"/tm daily-report \u2014 post a daily summary",
|
|
9535
|
+
"/tm autopilot enable \u2014 automatic daily health checks",
|
|
9536
|
+
"/tm autopilot disable \u2014 turn off automatic checks",
|
|
9537
|
+
"",
|
|
9538
|
+
"**Manage topics**",
|
|
9539
|
+
"/tm rename new-name \u2014 rename this topic",
|
|
9540
|
+
"/tm snooze 7d \u2014 pause health checks (e.g. 7d, 30d)",
|
|
9541
|
+
"/tm archive \u2014 archive this topic",
|
|
9542
|
+
"/tm unarchive \u2014 bring back an archived topic",
|
|
9543
|
+
"/tm sync \u2014 fix config if out of sync",
|
|
9544
|
+
"/tm upgrade \u2014 update topic files to latest version"
|
|
9532
9545
|
].join("\n");
|
|
9533
9546
|
}
|
|
9534
9547
|
function buildListMessage(topics) {
|
|
9535
9548
|
if (topics.length === 0) {
|
|
9536
|
-
return "**
|
|
9549
|
+
return "**Your topics**\n\nNo topics yet. Type /tm init in any topic to get started.";
|
|
9537
9550
|
}
|
|
9538
9551
|
const sorted = [...topics].sort((a, b) => {
|
|
9539
9552
|
const order = { active: 0, snoozed: 1, archived: 2 };
|
|
9540
9553
|
return (order[a.status] ?? 3) - (order[b.status] ?? 3);
|
|
9541
9554
|
});
|
|
9542
|
-
const
|
|
9555
|
+
const count = topics.length;
|
|
9556
|
+
const lines = [`**Your topics** (${count})`, ""];
|
|
9543
9557
|
let rendered = 0;
|
|
9544
9558
|
for (const t of sorted) {
|
|
9545
|
-
const
|
|
9546
|
-
|
|
9547
|
-
|
|
9548
|
-
|
|
9549
|
-
].join("\n");
|
|
9559
|
+
const activity = t.lastMessageAt ? relativeTime(t.lastMessageAt) : "no activity yet";
|
|
9560
|
+
const statusTag = t.status !== "active" ? ` \u2014 ${t.status}` : "";
|
|
9561
|
+
const entry = `**${t.name}** \xB7 ${t.type}${statusTag}
|
|
9562
|
+
${activity}`;
|
|
9550
9563
|
const tentative = [...lines, entry, ""].join("\n");
|
|
9551
9564
|
if (tentative.length > TELEGRAM_MSG_LIMIT - 40) {
|
|
9552
9565
|
const remaining = sorted.length - rendered;
|
|
@@ -9629,7 +9642,7 @@ function getSystemPromptTemplate(name, slug, absoluteWorkspacePath) {
|
|
|
9629
9642
|
return `You are the assistant for the Telegram topic: ${name}.
|
|
9630
9643
|
|
|
9631
9644
|
Determinism rules:
|
|
9632
|
-
- Source of truth is the project
|
|
9645
|
+
- Source of truth is the project folder at: ${absoluteWorkspacePath}/projects/${slug}/
|
|
9633
9646
|
- After /reset, /new, or context compaction: ALWAYS re-read STATUS.md,
|
|
9634
9647
|
then TODO.md, then LEARNINGS.md (last 20 entries), then COMMANDS.md
|
|
9635
9648
|
before continuing work. Do not rely on summarized memory for paths,
|
|
@@ -9657,10 +9670,10 @@ Learning capture:
|
|
|
9657
9670
|
|
|
9658
9671
|
Separation:
|
|
9659
9672
|
- Your workspace is strictly projects/${slug}/. Do not read, write, or reference
|
|
9660
|
-
files in any other topic's
|
|
9673
|
+
files in any other topic's project directory.
|
|
9661
9674
|
- If the user mentions another topic by name or slug, ask for explicit
|
|
9662
9675
|
confirmation before mixing work: "This references topic X \u2014 switch context?"
|
|
9663
|
-
- Never copy data between topic
|
|
9676
|
+
- Never copy data between topic folders without explicit user instruction.
|
|
9664
9677
|
- Ask one clarifying question if the next action is ambiguous.`;
|
|
9665
9678
|
}
|
|
9666
9679
|
function computeRegistryHash(topics) {
|
|
@@ -9881,10 +9894,10 @@ async function handleInit(ctx, args) {
|
|
|
9881
9894
|
return { text: "Missing context: groupId, threadId, or userId not available. Run this command inside a Telegram forum topic." };
|
|
9882
9895
|
}
|
|
9883
9896
|
if (!validateGroupId(groupId)) {
|
|
9884
|
-
return { text: "
|
|
9897
|
+
return { text: "Something went wrong \u2014 this doesn't look like a valid forum topic." };
|
|
9885
9898
|
}
|
|
9886
9899
|
if (!validateThreadId(threadId)) {
|
|
9887
|
-
return { text: "
|
|
9900
|
+
return { text: "Something went wrong \u2014 this doesn't look like a valid forum topic." };
|
|
9888
9901
|
}
|
|
9889
9902
|
const registry = readRegistry(workspaceDir);
|
|
9890
9903
|
const auth = checkAuthorization(userId, "init", registry);
|
|
@@ -9917,17 +9930,17 @@ async function handleInit(ctx, args) {
|
|
|
9917
9930
|
const finalSlug = generateSlug(threadId, groupId, existingSlugs);
|
|
9918
9931
|
const projectsBase = path6.join(workspaceDir, "projects");
|
|
9919
9932
|
if (!jailCheck(projectsBase, finalSlug)) {
|
|
9920
|
-
return { text: "
|
|
9933
|
+
return { text: "Setup failed \u2014 internal path validation error. Please try again." };
|
|
9921
9934
|
}
|
|
9922
9935
|
if (rejectSymlink(projectsBase)) {
|
|
9923
|
-
return { text: "
|
|
9936
|
+
return { text: "Setup failed \u2014 detected an unsafe file system configuration." };
|
|
9924
9937
|
}
|
|
9925
9938
|
if (fs6.existsSync(path6.join(projectsBase, finalSlug))) {
|
|
9926
|
-
return { text:
|
|
9939
|
+
return { text: "A folder for this topic already exists. Run /tm doctor to investigate." };
|
|
9927
9940
|
}
|
|
9928
9941
|
const targetPath = path6.join(projectsBase, finalSlug);
|
|
9929
9942
|
if (rejectSymlink(targetPath)) {
|
|
9930
|
-
return { text: "
|
|
9943
|
+
return { text: "Setup failed \u2014 detected an unsafe file system configuration." };
|
|
9931
9944
|
}
|
|
9932
9945
|
const isFirstUser = registry.topicManagerAdmins.length === 0;
|
|
9933
9946
|
try {
|
|
@@ -9944,9 +9957,9 @@ async function handleInit(ctx, args) {
|
|
|
9944
9957
|
lastMessageAt: (/* @__PURE__ */ new Date()).toISOString(),
|
|
9945
9958
|
lastDoctorReportAt: null,
|
|
9946
9959
|
lastDoctorRunAt: null,
|
|
9960
|
+
lastDailyReportAt: null,
|
|
9947
9961
|
lastCapsuleWriteAt: null,
|
|
9948
9962
|
snoozeUntil: null,
|
|
9949
|
-
ignoreChecks: [],
|
|
9950
9963
|
consecutiveSilentDoctors: 0,
|
|
9951
9964
|
lastPostError: null,
|
|
9952
9965
|
extras: {}
|
|
@@ -9973,35 +9986,24 @@ async function handleInit(ctx, args) {
|
|
|
9973
9986
|
} catch (err) {
|
|
9974
9987
|
const msg = err instanceof Error ? err.message : String(err);
|
|
9975
9988
|
restartMsg = `
|
|
9976
|
-
Warning:
|
|
9989
|
+
Warning: config sync failed: ${msg}`;
|
|
9977
9990
|
}
|
|
9978
9991
|
}
|
|
9979
9992
|
appendAudit(
|
|
9980
9993
|
workspaceDir,
|
|
9981
9994
|
buildAuditEntry(userId, "init", finalSlug, `Initialized topic name="${name}" type=${topicType} group=${groupId} thread=${threadId}`)
|
|
9982
9995
|
);
|
|
9983
|
-
const topicCard = buildTopicCard(name, finalSlug, topicType
|
|
9984
|
-
let adminNote = "";
|
|
9985
|
-
let adminNoteHtml = "";
|
|
9986
|
-
if (isFirstUser) {
|
|
9987
|
-
adminNote = "\n\nYou are the first user and have been added as a telegram-manager admin.";
|
|
9988
|
-
adminNoteHtml = "\n\nYou are the first user and have been added as a telegram-manager admin.";
|
|
9989
|
-
}
|
|
9990
|
-
const autopilotTip = "\n\nTip: Enable daily health sweeps with /tm autopilot enable";
|
|
9991
|
-
const autopilotTipHtml = "\n\nTip: Enable daily health sweeps with <code>/tm autopilot enable</code>";
|
|
9996
|
+
const topicCard = buildTopicCard(name, finalSlug, topicType);
|
|
9992
9997
|
if (ctx.postFn && groupId && threadId) {
|
|
9993
9998
|
try {
|
|
9994
|
-
const htmlCard = buildTopicCardHtml(name, finalSlug, topicType
|
|
9995
|
-
await ctx.postFn(groupId, threadId,
|
|
9996
|
-
return {
|
|
9997
|
-
text: `Topic "${name}" initialized as ${topicType}. Capsule: projects/${finalSlug}/`,
|
|
9998
|
-
pin: true
|
|
9999
|
-
};
|
|
9999
|
+
const htmlCard = buildTopicCardHtml(name, finalSlug, topicType);
|
|
10000
|
+
await ctx.postFn(groupId, threadId, htmlCard);
|
|
10001
|
+
return { text: "", pin: true };
|
|
10000
10002
|
} catch {
|
|
10001
10003
|
}
|
|
10002
10004
|
}
|
|
10003
10005
|
return {
|
|
10004
|
-
text: `${topicCard}${
|
|
10006
|
+
text: `${topicCard}${restartMsg}`,
|
|
10005
10007
|
pin: true
|
|
10006
10008
|
};
|
|
10007
10009
|
}
|
|
@@ -10017,10 +10019,10 @@ async function buildTypePicker(ctx) {
|
|
|
10017
10019
|
return { text: "Missing context: groupId, threadId, or userId not available. Run this command inside a Telegram forum topic." };
|
|
10018
10020
|
}
|
|
10019
10021
|
if (!validateGroupId(groupId)) {
|
|
10020
|
-
return { text: "
|
|
10022
|
+
return { text: "Something went wrong \u2014 this doesn't look like a valid forum topic." };
|
|
10021
10023
|
}
|
|
10022
10024
|
if (!validateThreadId(threadId)) {
|
|
10023
|
-
return { text: "
|
|
10025
|
+
return { text: "Something went wrong \u2014 this doesn't look like a valid forum topic." };
|
|
10024
10026
|
}
|
|
10025
10027
|
const registry = readRegistry(workspaceDir);
|
|
10026
10028
|
const auth = checkAuthorization(userId, "init", registry);
|
|
@@ -10056,10 +10058,10 @@ async function handleInitTypeSelect(ctx, type) {
|
|
|
10056
10058
|
return { text: "Missing context: groupId, threadId, or userId not available. Run this command inside a Telegram forum topic." };
|
|
10057
10059
|
}
|
|
10058
10060
|
if (!validateGroupId(groupId)) {
|
|
10059
|
-
return { text: "
|
|
10061
|
+
return { text: "Something went wrong \u2014 this doesn't look like a valid forum topic." };
|
|
10060
10062
|
}
|
|
10061
10063
|
if (!validateThreadId(threadId)) {
|
|
10062
|
-
return { text: "
|
|
10064
|
+
return { text: "Something went wrong \u2014 this doesn't look like a valid forum topic." };
|
|
10063
10065
|
}
|
|
10064
10066
|
const registry = readRegistry(workspaceDir);
|
|
10065
10067
|
const auth = checkAuthorization(userId, "init", registry);
|
|
@@ -10081,7 +10083,7 @@ async function handleInitTypeSelect(ctx, type) {
|
|
|
10081
10083
|
if (ctx.postFn) {
|
|
10082
10084
|
try {
|
|
10083
10085
|
await ctx.postFn(groupId, threadId, buildInitNameConfirmHtml(name, type), keyboard);
|
|
10084
|
-
return { text: `Type selected: ${type}. Confirm the name or type /tm init
|
|
10086
|
+
return { text: `Type selected: ${type}. Confirm the name or type /tm init your-name ${type}.` };
|
|
10085
10087
|
} catch {
|
|
10086
10088
|
}
|
|
10087
10089
|
}
|
|
@@ -10097,11 +10099,12 @@ function buildInitConfirmMessage(name, type) {
|
|
|
10097
10099
|
return [
|
|
10098
10100
|
"**Almost there!**",
|
|
10099
10101
|
"",
|
|
10100
|
-
`Name: **${name}
|
|
10102
|
+
`Name: **${name}**`,
|
|
10103
|
+
`Type: ${type}`,
|
|
10101
10104
|
"",
|
|
10102
|
-
"
|
|
10105
|
+
"You'll see this name in reports and health checks.",
|
|
10103
10106
|
"",
|
|
10104
|
-
`
|
|
10107
|
+
`For a custom name: \`/tm init your-name ${type}\``
|
|
10105
10108
|
].join("\n");
|
|
10106
10109
|
}
|
|
10107
10110
|
|
|
@@ -10113,9 +10116,6 @@ import * as path8 from "node:path";
|
|
|
10113
10116
|
var import_json52 = __toESM(require_lib(), 1);
|
|
10114
10117
|
import * as fs7 from "node:fs";
|
|
10115
10118
|
import * as path7 from "node:path";
|
|
10116
|
-
function isIgnored(entry, checkId) {
|
|
10117
|
-
return entry.ignoreChecks.includes(checkId);
|
|
10118
|
-
}
|
|
10119
10119
|
function check(severity, checkId, message, fixable, remediation) {
|
|
10120
10120
|
return remediation ? { severity, checkId, message, fixable, remediation } : { severity, checkId, message, fixable };
|
|
10121
10121
|
}
|
|
@@ -10124,7 +10124,7 @@ function runRegistryChecks(entry, projectsBase) {
|
|
|
10124
10124
|
const capsuleDir = path7.join(projectsBase, entry.slug);
|
|
10125
10125
|
if (!fs7.existsSync(capsuleDir)) {
|
|
10126
10126
|
results.push(
|
|
10127
|
-
check(Severity.ERROR, "pathMissing", `
|
|
10127
|
+
check(Severity.ERROR, "pathMissing", `Project folder is missing (projects/${entry.slug}/)`, false, "Run /tm init to recreate it")
|
|
10128
10128
|
);
|
|
10129
10129
|
return results;
|
|
10130
10130
|
}
|
|
@@ -10132,12 +10132,12 @@ function runRegistryChecks(entry, projectsBase) {
|
|
|
10132
10132
|
const stat = fs7.statSync(capsuleDir);
|
|
10133
10133
|
if (!stat.isDirectory()) {
|
|
10134
10134
|
results.push(
|
|
10135
|
-
check(Severity.ERROR, "pathNotDir",
|
|
10135
|
+
check(Severity.ERROR, "pathNotDir", "Topic path exists but is not a folder", false)
|
|
10136
10136
|
);
|
|
10137
10137
|
}
|
|
10138
10138
|
} catch {
|
|
10139
10139
|
results.push(
|
|
10140
|
-
check(Severity.ERROR, "pathStatFailed",
|
|
10140
|
+
check(Severity.ERROR, "pathStatFailed", "Cannot verify topic folder on disk", false)
|
|
10141
10141
|
);
|
|
10142
10142
|
}
|
|
10143
10143
|
return results;
|
|
@@ -10148,39 +10148,32 @@ function runCapsuleChecks(entry, projectsBase) {
|
|
|
10148
10148
|
if (!fs7.existsSync(capsuleDir)) return results;
|
|
10149
10149
|
if (!fs7.existsSync(path7.join(capsuleDir, "STATUS.md"))) {
|
|
10150
10150
|
results.push(
|
|
10151
|
-
check(Severity.ERROR, "statusMissing", "
|
|
10151
|
+
check(Severity.ERROR, "statusMissing", "Status file is missing", true, "Run /tm upgrade to recreate it")
|
|
10152
10152
|
);
|
|
10153
10153
|
}
|
|
10154
10154
|
if (!fs7.existsSync(path7.join(capsuleDir, "TODO.md"))) {
|
|
10155
|
-
|
|
10156
|
-
|
|
10157
|
-
|
|
10158
|
-
);
|
|
10159
|
-
}
|
|
10155
|
+
results.push(
|
|
10156
|
+
check(Severity.WARN, "todoMissing", "TODO file is missing", true, "Run /tm upgrade to recreate it")
|
|
10157
|
+
);
|
|
10160
10158
|
}
|
|
10161
10159
|
const overlays = OVERLAY_FILES[entry.type] ?? [];
|
|
10162
10160
|
for (const file of overlays) {
|
|
10163
10161
|
if (!fs7.existsSync(path7.join(capsuleDir, file))) {
|
|
10164
|
-
const checkId = `overlayMissing:${file}`;
|
|
10165
|
-
if (!isIgnored(entry, checkId)) {
|
|
10166
|
-
results.push(
|
|
10167
|
-
check(Severity.INFO, checkId, `Optional overlay ${file} missing for type "${entry.type}"`, true)
|
|
10168
|
-
);
|
|
10169
|
-
}
|
|
10170
|
-
}
|
|
10171
|
-
}
|
|
10172
|
-
if (entry.capsuleVersion < CAPSULE_VERSION) {
|
|
10173
|
-
if (!isIgnored(entry, "capsuleVersionBehind")) {
|
|
10174
10162
|
results.push(
|
|
10175
|
-
check(
|
|
10176
|
-
Severity.INFO,
|
|
10177
|
-
"capsuleVersionBehind",
|
|
10178
|
-
`Capsule version ${entry.capsuleVersion} is behind current ${CAPSULE_VERSION}. Will auto-upgrade on next command.`,
|
|
10179
|
-
false
|
|
10180
|
-
)
|
|
10163
|
+
check(Severity.INFO, `overlayMissing:${file}`, `Optional overlay ${file} missing for type "${entry.type}"`, true)
|
|
10181
10164
|
);
|
|
10182
10165
|
}
|
|
10183
10166
|
}
|
|
10167
|
+
if (entry.capsuleVersion < CAPSULE_VERSION) {
|
|
10168
|
+
results.push(
|
|
10169
|
+
check(
|
|
10170
|
+
Severity.INFO,
|
|
10171
|
+
"capsuleVersionBehind",
|
|
10172
|
+
`Capsule version ${entry.capsuleVersion} is behind current ${CAPSULE_VERSION}. Will auto-upgrade on next command.`,
|
|
10173
|
+
false
|
|
10174
|
+
)
|
|
10175
|
+
);
|
|
10176
|
+
}
|
|
10184
10177
|
return results;
|
|
10185
10178
|
}
|
|
10186
10179
|
var LAST_DONE_RE = /^##\s*Last done\s*\(UTC\)/im;
|
|
@@ -10191,49 +10184,41 @@ var ADHOC_RE = /\[AD-HOC\]/g;
|
|
|
10191
10184
|
function runStatusQualityChecks(statusContent, entry) {
|
|
10192
10185
|
const results = [];
|
|
10193
10186
|
if (!LAST_DONE_RE.test(statusContent)) {
|
|
10194
|
-
|
|
10195
|
-
|
|
10196
|
-
|
|
10197
|
-
);
|
|
10198
|
-
}
|
|
10187
|
+
results.push(
|
|
10188
|
+
check(Severity.ERROR, "lastDoneMissing", "Status file is missing the last activity section", true, "The AI will fix this on next interaction")
|
|
10189
|
+
);
|
|
10199
10190
|
} else {
|
|
10200
10191
|
const lastDoneIndex = statusContent.search(LAST_DONE_RE);
|
|
10201
10192
|
const sectionAfter = statusContent.slice(lastDoneIndex);
|
|
10202
10193
|
const nextSectionIndex = sectionAfter.indexOf("\n## ", 1);
|
|
10203
10194
|
const lastDoneSection = nextSectionIndex > 0 ? sectionAfter.slice(0, nextSectionIndex) : sectionAfter;
|
|
10204
10195
|
if (!TIMESTAMP_RE.test(lastDoneSection)) {
|
|
10205
|
-
|
|
10206
|
-
|
|
10207
|
-
|
|
10208
|
-
);
|
|
10209
|
-
}
|
|
10196
|
+
results.push(
|
|
10197
|
+
check(Severity.ERROR, "lastDoneNoTimestamp", "Last activity has no timestamp", true, "The AI will fix this on next interaction")
|
|
10198
|
+
);
|
|
10210
10199
|
} else if (entry.status === "active") {
|
|
10211
10200
|
const tsMatch = lastDoneSection.match(TIMESTAMP_RE);
|
|
10212
10201
|
if (tsMatch) {
|
|
10213
10202
|
const ts = new Date(tsMatch[0]);
|
|
10214
10203
|
const ageDays = (Date.now() - ts.getTime()) / (1e3 * 60 * 60 * 24);
|
|
10215
10204
|
if (ageDays > 3) {
|
|
10216
|
-
|
|
10217
|
-
|
|
10218
|
-
|
|
10219
|
-
|
|
10220
|
-
|
|
10221
|
-
|
|
10222
|
-
|
|
10223
|
-
|
|
10224
|
-
|
|
10225
|
-
);
|
|
10226
|
-
}
|
|
10205
|
+
results.push(
|
|
10206
|
+
check(
|
|
10207
|
+
Severity.WARN,
|
|
10208
|
+
"lastDoneStale",
|
|
10209
|
+
`No activity for ${Math.floor(ageDays)} days`,
|
|
10210
|
+
false,
|
|
10211
|
+
"Send a message to resume, or /tm snooze 7d to silence"
|
|
10212
|
+
)
|
|
10213
|
+
);
|
|
10227
10214
|
}
|
|
10228
10215
|
}
|
|
10229
10216
|
}
|
|
10230
10217
|
}
|
|
10231
10218
|
if (!NEXT_ACTIONS_RE.test(statusContent)) {
|
|
10232
|
-
|
|
10233
|
-
|
|
10234
|
-
|
|
10235
|
-
);
|
|
10236
|
-
}
|
|
10219
|
+
results.push(
|
|
10220
|
+
check(Severity.ERROR, "nextActionsMissing", "Status file is missing the next actions section", true, "The AI will fix this on next interaction")
|
|
10221
|
+
);
|
|
10237
10222
|
} else {
|
|
10238
10223
|
const nextActionsIndex = statusContent.search(NEXT_ACTIONS_RE);
|
|
10239
10224
|
const sectionAfter = statusContent.slice(nextActionsIndex);
|
|
@@ -10242,17 +10227,15 @@ function runStatusQualityChecks(statusContent, entry) {
|
|
|
10242
10227
|
const taskIds = nextActionsSection.match(TASK_ID_RE) ?? [];
|
|
10243
10228
|
const adhocs = nextActionsSection.match(ADHOC_RE) ?? [];
|
|
10244
10229
|
if (taskIds.length === 0 && adhocs.length === 0) {
|
|
10245
|
-
|
|
10246
|
-
|
|
10247
|
-
|
|
10248
|
-
|
|
10249
|
-
|
|
10250
|
-
|
|
10251
|
-
|
|
10252
|
-
|
|
10253
|
-
|
|
10254
|
-
);
|
|
10255
|
-
}
|
|
10230
|
+
results.push(
|
|
10231
|
+
check(
|
|
10232
|
+
Severity.WARN,
|
|
10233
|
+
"nextActionsEmpty",
|
|
10234
|
+
"No next actions defined yet",
|
|
10235
|
+
false,
|
|
10236
|
+
"Send a message with your next task to get started"
|
|
10237
|
+
)
|
|
10238
|
+
);
|
|
10256
10239
|
}
|
|
10257
10240
|
}
|
|
10258
10241
|
return results;
|
|
@@ -10273,9 +10256,9 @@ function runNextVsTodoChecks(statusContent, todoContent) {
|
|
|
10273
10256
|
check(
|
|
10274
10257
|
Severity.WARN,
|
|
10275
10258
|
"nextNotInTodo",
|
|
10276
|
-
`${missing.length}
|
|
10259
|
+
`${missing.length} tasks referenced in next actions don't exist in the TODO list: ${missing.join(", ")}`,
|
|
10277
10260
|
false,
|
|
10278
|
-
"
|
|
10261
|
+
"The AI will clean these up on next interaction"
|
|
10279
10262
|
)
|
|
10280
10263
|
);
|
|
10281
10264
|
}
|
|
@@ -10286,21 +10269,17 @@ function runCommandsLinksChecks(entry, capsuleFiles) {
|
|
|
10286
10269
|
if (entry.type === "coding") {
|
|
10287
10270
|
const commandsContent = capsuleFiles.get("COMMANDS.md");
|
|
10288
10271
|
if (commandsContent !== void 0 && isEffectivelyEmpty(commandsContent)) {
|
|
10289
|
-
|
|
10290
|
-
|
|
10291
|
-
|
|
10292
|
-
);
|
|
10293
|
-
}
|
|
10272
|
+
results.push(
|
|
10273
|
+
check(Severity.INFO, "commandsEmpty", "COMMANDS.md is empty for a coding topic", false, "Add build/test/deploy commands to COMMANDS.md")
|
|
10274
|
+
);
|
|
10294
10275
|
}
|
|
10295
10276
|
}
|
|
10296
10277
|
if (entry.type === "coding" || entry.type === "research") {
|
|
10297
10278
|
const linksContent = capsuleFiles.get("LINKS.md");
|
|
10298
10279
|
if (linksContent !== void 0 && isEffectivelyEmpty(linksContent)) {
|
|
10299
|
-
|
|
10300
|
-
|
|
10301
|
-
|
|
10302
|
-
);
|
|
10303
|
-
}
|
|
10280
|
+
results.push(
|
|
10281
|
+
check(Severity.INFO, "linksEmpty", "LINKS.md is empty for a coding/research topic", false, "Add URLs and endpoints to LINKS.md")
|
|
10282
|
+
);
|
|
10304
10283
|
}
|
|
10305
10284
|
}
|
|
10306
10285
|
return results;
|
|
@@ -10317,7 +10296,7 @@ function runCronChecks(cronContent, cronJobsPath) {
|
|
|
10317
10296
|
const hasJobIds = lines.some((line) => JOB_ID_RE.test(line));
|
|
10318
10297
|
if (!hasJobIds) {
|
|
10319
10298
|
results.push(
|
|
10320
|
-
check(Severity.WARN, "cronNoJobIds", "
|
|
10299
|
+
check(Severity.WARN, "cronNoJobIds", "Scheduled jobs are listed but have no recognizable job IDs", false)
|
|
10321
10300
|
);
|
|
10322
10301
|
return results;
|
|
10323
10302
|
}
|
|
@@ -10333,7 +10312,7 @@ function runCronChecks(cronContent, cronJobsPath) {
|
|
|
10333
10312
|
check(
|
|
10334
10313
|
Severity.WARN,
|
|
10335
10314
|
"cronJobNotFound",
|
|
10336
|
-
`
|
|
10315
|
+
`Scheduled job "${match[0]}" not found in the jobs registry`,
|
|
10337
10316
|
false
|
|
10338
10317
|
)
|
|
10339
10318
|
);
|
|
@@ -10354,36 +10333,28 @@ function runConfigChecks(entry, includeContent, registry) {
|
|
|
10354
10333
|
}
|
|
10355
10334
|
const groupConfig = includeObj[entry.groupId];
|
|
10356
10335
|
if (!groupConfig) {
|
|
10357
|
-
|
|
10358
|
-
|
|
10359
|
-
|
|
10360
|
-
);
|
|
10361
|
-
}
|
|
10336
|
+
results.push(
|
|
10337
|
+
check(Severity.WARN, "configGroupMissing", "Config is out of sync", false, "Run /tm sync to fix")
|
|
10338
|
+
);
|
|
10362
10339
|
return results;
|
|
10363
10340
|
}
|
|
10364
10341
|
const topics = groupConfig["topics"];
|
|
10365
10342
|
const topicConfig = topics?.[entry.threadId];
|
|
10366
10343
|
if (!topicConfig) {
|
|
10367
|
-
|
|
10368
|
-
|
|
10369
|
-
|
|
10370
|
-
);
|
|
10371
|
-
}
|
|
10344
|
+
results.push(
|
|
10345
|
+
check(Severity.WARN, "configTopicMissing", "Topic not found in system config", false, "Run /tm sync to fix")
|
|
10346
|
+
);
|
|
10372
10347
|
return results;
|
|
10373
10348
|
}
|
|
10374
10349
|
if (!topicConfig["systemPrompt"]) {
|
|
10375
|
-
|
|
10376
|
-
|
|
10377
|
-
|
|
10378
|
-
);
|
|
10379
|
-
}
|
|
10350
|
+
results.push(
|
|
10351
|
+
check(Severity.WARN, "configNoSystemPrompt", "AI instructions are missing for this topic", false, "Run /tm sync to fix")
|
|
10352
|
+
);
|
|
10380
10353
|
}
|
|
10381
10354
|
if (!topicConfig["skills"] || !Array.isArray(topicConfig["skills"])) {
|
|
10382
|
-
|
|
10383
|
-
|
|
10384
|
-
|
|
10385
|
-
);
|
|
10386
|
-
}
|
|
10355
|
+
results.push(
|
|
10356
|
+
check(Severity.WARN, "configNoSkills", "Command list is missing for this topic", false, "Run /tm sync to fix")
|
|
10357
|
+
);
|
|
10387
10358
|
}
|
|
10388
10359
|
return results;
|
|
10389
10360
|
}
|
|
@@ -10395,9 +10366,9 @@ function runIncludeDriftCheck(includeFileContent, registry) {
|
|
|
10395
10366
|
check(
|
|
10396
10367
|
Severity.WARN,
|
|
10397
10368
|
"includeDrift",
|
|
10398
|
-
"
|
|
10369
|
+
"Config is out of sync with your topics",
|
|
10399
10370
|
false,
|
|
10400
|
-
"Run /tm sync to
|
|
10371
|
+
"Run /tm sync to fix"
|
|
10401
10372
|
)
|
|
10402
10373
|
);
|
|
10403
10374
|
return results;
|
|
@@ -10408,9 +10379,9 @@ function runIncludeDriftCheck(includeFileContent, registry) {
|
|
|
10408
10379
|
check(
|
|
10409
10380
|
Severity.WARN,
|
|
10410
10381
|
"includeDrift",
|
|
10411
|
-
"
|
|
10382
|
+
"Config is out of sync with your topics",
|
|
10412
10383
|
false,
|
|
10413
|
-
"Run /tm sync to
|
|
10384
|
+
"Run /tm sync to fix"
|
|
10414
10385
|
)
|
|
10415
10386
|
);
|
|
10416
10387
|
}
|
|
@@ -10423,9 +10394,9 @@ function runSpamControlCheck(entry) {
|
|
|
10423
10394
|
check(
|
|
10424
10395
|
Severity.INFO,
|
|
10425
10396
|
"spamControl",
|
|
10426
|
-
|
|
10397
|
+
`No activity for a while \u2014 auto-snoozing for 30 days`,
|
|
10427
10398
|
true,
|
|
10428
|
-
"
|
|
10399
|
+
"Send a message to resume"
|
|
10429
10400
|
)
|
|
10430
10401
|
);
|
|
10431
10402
|
}
|
|
@@ -10442,7 +10413,7 @@ function runAllChecksForTopic(entry, projectsBase, includeContent, registry, cro
|
|
|
10442
10413
|
if (statusContent) {
|
|
10443
10414
|
results.push(...runStatusQualityChecks(statusContent, entry));
|
|
10444
10415
|
const todoContent = capsuleFiles.get("TODO.md");
|
|
10445
|
-
if (todoContent
|
|
10416
|
+
if (todoContent) {
|
|
10446
10417
|
results.push(...runNextVsTodoChecks(statusContent, todoContent));
|
|
10447
10418
|
}
|
|
10448
10419
|
}
|
|
@@ -10526,11 +10497,11 @@ async function handleDoctor(ctx) {
|
|
|
10526
10497
|
}
|
|
10527
10498
|
const projectsBase = path8.join(workspaceDir, "projects");
|
|
10528
10499
|
if (!jailCheck(projectsBase, entry.slug)) {
|
|
10529
|
-
return { text: "
|
|
10500
|
+
return { text: "Something went wrong \u2014 path validation failed." };
|
|
10530
10501
|
}
|
|
10531
10502
|
const capsuleDir = path8.join(projectsBase, entry.slug);
|
|
10532
10503
|
if (rejectSymlink(capsuleDir)) {
|
|
10533
|
-
return { text: "
|
|
10504
|
+
return { text: "Something went wrong \u2014 detected an unsafe file system configuration." };
|
|
10534
10505
|
}
|
|
10535
10506
|
let includeContent;
|
|
10536
10507
|
const incPath = includePath(configDir);
|
|
@@ -10556,13 +10527,6 @@ async function handleDoctor(ctx) {
|
|
|
10556
10527
|
registry.callbackSecret,
|
|
10557
10528
|
userId
|
|
10558
10529
|
);
|
|
10559
|
-
const textCommands = [
|
|
10560
|
-
"",
|
|
10561
|
-
"Or use text commands:",
|
|
10562
|
-
"/tm snooze 7d",
|
|
10563
|
-
"/tm snooze 30d",
|
|
10564
|
-
"/tm archive"
|
|
10565
|
-
].join("\n");
|
|
10566
10530
|
await withRegistry(workspaceDir, (data) => {
|
|
10567
10531
|
const topic = data.topics[key];
|
|
10568
10532
|
if (topic) {
|
|
@@ -10570,7 +10534,7 @@ async function handleDoctor(ctx) {
|
|
|
10570
10534
|
}
|
|
10571
10535
|
});
|
|
10572
10536
|
return {
|
|
10573
|
-
text: reportText
|
|
10537
|
+
text: reportText,
|
|
10574
10538
|
inlineKeyboard: keyboard
|
|
10575
10539
|
};
|
|
10576
10540
|
}
|
|
@@ -10594,7 +10558,7 @@ async function handleDoctorAll(ctx) {
|
|
|
10594
10558
|
if (elapsed < DOCTOR_ALL_COOLDOWN_MS) {
|
|
10595
10559
|
const remainingMin = Math.ceil((DOCTOR_ALL_COOLDOWN_MS - elapsed) / 6e4);
|
|
10596
10560
|
return {
|
|
10597
|
-
text: `
|
|
10561
|
+
text: `Health checks were run ${Math.floor(elapsed / 6e4)} minutes ago. Try again in ${remainingMin} minute(s).`
|
|
10598
10562
|
};
|
|
10599
10563
|
}
|
|
10600
10564
|
}
|
|
@@ -10727,11 +10691,11 @@ async function handleDoctorAll(ctx) {
|
|
|
10727
10691
|
}
|
|
10728
10692
|
});
|
|
10729
10693
|
const lines = [
|
|
10730
|
-
`**
|
|
10694
|
+
`**Health Check Summary**`,
|
|
10731
10695
|
"",
|
|
10732
|
-
`
|
|
10733
|
-
`Skipped
|
|
10734
|
-
`Total: ${allEntries.length}`
|
|
10696
|
+
`Checked: ${processed}`,
|
|
10697
|
+
`Skipped: ${skipped}`,
|
|
10698
|
+
`Total topics: ${allEntries.length}`
|
|
10735
10699
|
];
|
|
10736
10700
|
if (ctx.postFn) {
|
|
10737
10701
|
lines.push(`Posted: ${postSuccesses}, Post failures: ${postErrors}`);
|
|
@@ -10748,10 +10712,7 @@ async function handleDoctorAll(ctx) {
|
|
|
10748
10712
|
}
|
|
10749
10713
|
if (migrationGroups.length > 0) {
|
|
10750
10714
|
lines.push("");
|
|
10751
|
-
lines.push(
|
|
10752
|
-
for (const gid of migrationGroups) {
|
|
10753
|
-
lines.push(`- Group ${gid}: all topics failed. Check for group migration.`);
|
|
10754
|
-
}
|
|
10715
|
+
lines.push(`**Warning:** ${migrationGroups.length} group(s) had all topics fail. The group may have been migrated or deleted.`);
|
|
10755
10716
|
}
|
|
10756
10717
|
return {
|
|
10757
10718
|
text: lines.join("\n")
|
|
@@ -10793,6 +10754,61 @@ async function handleList(ctx) {
|
|
|
10793
10754
|
// src/commands/status.ts
|
|
10794
10755
|
import * as fs10 from "node:fs";
|
|
10795
10756
|
import * as path10 from "node:path";
|
|
10757
|
+
var LAST_DONE_RE2 = /^##\s*Last done\s*\(UTC\)\s*\n([\s\S]*?)(?=\n##\s|\n*$)/im;
|
|
10758
|
+
var NEXT_ACTIONS_RE2 = /^##\s*Next (?:3 )?actions(?: \(now\))?\s*\n([\s\S]*?)(?=\n##\s|\n*$)/im;
|
|
10759
|
+
var UPCOMING_RE = /^##\s*Upcoming actions\s*\n([\s\S]*?)(?=\n##\s|\n*$)/im;
|
|
10760
|
+
var ISO_RE = /\d{4}-\d{2}-\d{2}T\d{2}:\d{2}/;
|
|
10761
|
+
function extractTimestamp(content) {
|
|
10762
|
+
const match = content.match(LAST_DONE_RE2);
|
|
10763
|
+
if (!match) return null;
|
|
10764
|
+
const iso = match[1]?.match(ISO_RE);
|
|
10765
|
+
return iso ? iso[0] : null;
|
|
10766
|
+
}
|
|
10767
|
+
function extractSection(content, re) {
|
|
10768
|
+
const match = content.match(re);
|
|
10769
|
+
if (!match) return "";
|
|
10770
|
+
return match[1]?.trim() ?? "";
|
|
10771
|
+
}
|
|
10772
|
+
function isPlaceholder(text) {
|
|
10773
|
+
if (!text) return true;
|
|
10774
|
+
const stripped = text.replace(/[_*]/g, "").trim().toLowerCase();
|
|
10775
|
+
return stripped === "none yet." || stripped === "none yet" || stripped === "" || stripped.startsWith("e.g.");
|
|
10776
|
+
}
|
|
10777
|
+
function formatSection(raw) {
|
|
10778
|
+
if (isPlaceholder(raw)) return "_None yet._";
|
|
10779
|
+
return raw;
|
|
10780
|
+
}
|
|
10781
|
+
function formatStatus(name, content) {
|
|
10782
|
+
const timestamp = extractTimestamp(content);
|
|
10783
|
+
const nextRaw = extractSection(content, NEXT_ACTIONS_RE2);
|
|
10784
|
+
const upcomingRaw = extractSection(content, UPCOMING_RE);
|
|
10785
|
+
const doneMatch = content.match(LAST_DONE_RE2);
|
|
10786
|
+
let lastDoneBody = "";
|
|
10787
|
+
if (doneMatch) {
|
|
10788
|
+
const section = doneMatch[1]?.trim() ?? "";
|
|
10789
|
+
lastDoneBody = section.replace(ISO_RE, "").trim();
|
|
10790
|
+
}
|
|
10791
|
+
const lines = [
|
|
10792
|
+
`**${name}**`,
|
|
10793
|
+
""
|
|
10794
|
+
];
|
|
10795
|
+
if (timestamp) {
|
|
10796
|
+
lines.push(`**Last activity:** ${relativeTime(timestamp)}`);
|
|
10797
|
+
}
|
|
10798
|
+
if (lastDoneBody && !isPlaceholder(lastDoneBody)) {
|
|
10799
|
+
lines.push(lastDoneBody);
|
|
10800
|
+
}
|
|
10801
|
+
lines.push("");
|
|
10802
|
+
lines.push("**Next actions**");
|
|
10803
|
+
lines.push(formatSection(nextRaw));
|
|
10804
|
+
const upcomingFormatted = formatSection(upcomingRaw);
|
|
10805
|
+
if (upcomingFormatted !== "_None yet._") {
|
|
10806
|
+
lines.push("");
|
|
10807
|
+
lines.push("**Upcoming**");
|
|
10808
|
+
lines.push(upcomingFormatted);
|
|
10809
|
+
}
|
|
10810
|
+
return lines.join("\n");
|
|
10811
|
+
}
|
|
10796
10812
|
async function handleStatus(ctx) {
|
|
10797
10813
|
const { workspaceDir, userId, groupId, threadId } = ctx;
|
|
10798
10814
|
if (!userId || !groupId || !threadId) {
|
|
@@ -10811,23 +10827,23 @@ async function handleStatus(ctx) {
|
|
|
10811
10827
|
const projectsBase = path10.join(workspaceDir, "projects");
|
|
10812
10828
|
const capsuleDir = path10.join(projectsBase, entry.slug);
|
|
10813
10829
|
if (!jailCheck(projectsBase, entry.slug)) {
|
|
10814
|
-
return { text: "
|
|
10830
|
+
return { text: "Something went wrong \u2014 path validation failed." };
|
|
10815
10831
|
}
|
|
10816
10832
|
if (rejectSymlink(capsuleDir)) {
|
|
10817
|
-
return { text: "
|
|
10833
|
+
return { text: "Something went wrong \u2014 detected an unsafe file system configuration." };
|
|
10818
10834
|
}
|
|
10819
10835
|
const statusPath = path10.join(capsuleDir, "STATUS.md");
|
|
10820
10836
|
if (!fs10.existsSync(statusPath)) {
|
|
10821
|
-
return { text: "
|
|
10837
|
+
return { text: "No status available yet. Run /tm doctor to diagnose." };
|
|
10822
10838
|
}
|
|
10823
10839
|
try {
|
|
10824
10840
|
const content = fs10.readFileSync(statusPath, "utf-8");
|
|
10825
10841
|
return {
|
|
10826
|
-
text: truncateMessage(content)
|
|
10842
|
+
text: truncateMessage(formatStatus(entry.name, content))
|
|
10827
10843
|
};
|
|
10828
10844
|
} catch (err) {
|
|
10829
10845
|
const msg = err instanceof Error ? err.message : String(err);
|
|
10830
|
-
return { text: `Failed to read
|
|
10846
|
+
return { text: `Failed to read topic status: ${msg}` };
|
|
10831
10847
|
}
|
|
10832
10848
|
}
|
|
10833
10849
|
|
|
@@ -10852,7 +10868,7 @@ async function handleSync(ctx) {
|
|
|
10852
10868
|
}
|
|
10853
10869
|
const restartResult = await triggerRestart(rpc, logger);
|
|
10854
10870
|
const topicCount = Object.keys(registry.topics).length;
|
|
10855
|
-
let text = `
|
|
10871
|
+
let text = `Config synced for ${topicCount} topic(s).`;
|
|
10856
10872
|
if (!restartResult.success && restartResult.fallbackMessage) {
|
|
10857
10873
|
text += "\n" + restartResult.fallbackMessage;
|
|
10858
10874
|
}
|
|
@@ -10869,7 +10885,7 @@ async function handleRename(ctx, newName) {
|
|
|
10869
10885
|
}
|
|
10870
10886
|
const trimmedName = newName.trim();
|
|
10871
10887
|
if (!trimmedName) {
|
|
10872
|
-
return { text: "
|
|
10888
|
+
return { text: "What should the new name be? Example: /tm rename my-project" };
|
|
10873
10889
|
}
|
|
10874
10890
|
if (trimmedName.length > MAX_NAME_LENGTH) {
|
|
10875
10891
|
return { text: `Name too long (max ${MAX_NAME_LENGTH} characters).` };
|
|
@@ -10914,11 +10930,8 @@ Warning: include generation failed: ${msg}`;
|
|
|
10914
10930
|
workspaceDir,
|
|
10915
10931
|
buildAuditEntry(userId, "rename", entry.slug, `Renamed from "${oldName}" to "${trimmedName}"`)
|
|
10916
10932
|
);
|
|
10917
|
-
const topicCard = buildTopicCard(trimmedName, entry.slug, entry.type, entry.capsuleVersion);
|
|
10918
10933
|
return {
|
|
10919
|
-
text: `Topic renamed from **${oldName}** to **${trimmedName}
|
|
10920
|
-
|
|
10921
|
-
${topicCard}${restartMsg}`
|
|
10934
|
+
text: `Topic renamed from **${oldName}** to **${trimmedName}**.${restartMsg}`
|
|
10922
10935
|
};
|
|
10923
10936
|
}
|
|
10924
10937
|
|
|
@@ -10941,7 +10954,7 @@ async function handleUpgrade(ctx) {
|
|
|
10941
10954
|
}
|
|
10942
10955
|
if (entry.capsuleVersion >= CAPSULE_VERSION) {
|
|
10943
10956
|
return {
|
|
10944
|
-
text: `Topic **${entry.name}** is already
|
|
10957
|
+
text: `Topic **${entry.name}** is already up to date. No upgrade needed.`
|
|
10945
10958
|
};
|
|
10946
10959
|
}
|
|
10947
10960
|
const projectsBase = path11.join(workspaceDir, "projects");
|
|
@@ -10960,7 +10973,7 @@ async function handleUpgrade(ctx) {
|
|
|
10960
10973
|
const addedList = result.addedFiles.length > 0 ? `
|
|
10961
10974
|
Added files: ${result.addedFiles.join(", ")}` : "\nNo new files added.";
|
|
10962
10975
|
return {
|
|
10963
|
-
text: `Topic **${entry.name}** upgraded
|
|
10976
|
+
text: `Topic **${entry.name}** upgraded.${addedList}`
|
|
10964
10977
|
};
|
|
10965
10978
|
}
|
|
10966
10979
|
|
|
@@ -10973,7 +10986,7 @@ async function handleSnooze(ctx, args) {
|
|
|
10973
10986
|
}
|
|
10974
10987
|
const trimmed = args.trim();
|
|
10975
10988
|
if (!trimmed) {
|
|
10976
|
-
return { text: "
|
|
10989
|
+
return { text: "How long to snooze? Example: /tm snooze 7d" };
|
|
10977
10990
|
}
|
|
10978
10991
|
const match = DURATION_RE.exec(trimmed);
|
|
10979
10992
|
if (!match) {
|
|
@@ -11007,7 +11020,7 @@ async function handleSnooze(ctx, args) {
|
|
|
11007
11020
|
buildAuditEntry(userId, "snooze", entry.slug, `Snoozed for ${days} days until ${snoozeUntil}`)
|
|
11008
11021
|
);
|
|
11009
11022
|
return {
|
|
11010
|
-
text: `Topic **${entry.name}** snoozed for ${days} days
|
|
11023
|
+
text: `Topic **${entry.name}** snoozed for ${days} days. Health checks will resume automatically after that.`
|
|
11011
11024
|
};
|
|
11012
11025
|
}
|
|
11013
11026
|
|
|
@@ -11137,7 +11150,7 @@ async function handleEnable(ctx) {
|
|
|
11137
11150
|
data.autopilotEnabled = true;
|
|
11138
11151
|
});
|
|
11139
11152
|
return {
|
|
11140
|
-
text: "**Autopilot enabled.**\
|
|
11153
|
+
text: "**Autopilot enabled.**\nHealth checks will run automatically every day."
|
|
11141
11154
|
};
|
|
11142
11155
|
}
|
|
11143
11156
|
async function handleDisable(ctx) {
|
|
@@ -11147,14 +11160,14 @@ async function handleDisable(ctx) {
|
|
|
11147
11160
|
await withRegistry(workspaceDir, (data) => {
|
|
11148
11161
|
data.autopilotEnabled = false;
|
|
11149
11162
|
});
|
|
11150
|
-
return { text: "Autopilot is
|
|
11163
|
+
return { text: "Autopilot is already disabled." };
|
|
11151
11164
|
}
|
|
11152
11165
|
let content = fs11.readFileSync(heartbeatPath, "utf-8");
|
|
11153
11166
|
if (!content.includes(MARKER_START)) {
|
|
11154
11167
|
await withRegistry(workspaceDir, (data) => {
|
|
11155
11168
|
data.autopilotEnabled = false;
|
|
11156
11169
|
});
|
|
11157
|
-
return { text: "Autopilot is
|
|
11170
|
+
return { text: "Autopilot is already disabled." };
|
|
11158
11171
|
}
|
|
11159
11172
|
const startIdx = content.indexOf(MARKER_START);
|
|
11160
11173
|
const endIdx = content.indexOf(MARKER_END);
|
|
@@ -11172,17 +11185,17 @@ async function handleDisable(ctx) {
|
|
|
11172
11185
|
data.autopilotEnabled = false;
|
|
11173
11186
|
});
|
|
11174
11187
|
return {
|
|
11175
|
-
text: "**Autopilot disabled.**\
|
|
11188
|
+
text: "**Autopilot disabled.**\nAutomatic health checks are now off."
|
|
11176
11189
|
};
|
|
11177
11190
|
}
|
|
11178
11191
|
async function handleStatus2(ctx) {
|
|
11179
11192
|
const { workspaceDir } = ctx;
|
|
11180
11193
|
const registry = readRegistry(workspaceDir);
|
|
11181
11194
|
const enabled = registry.autopilotEnabled;
|
|
11182
|
-
const lastRun = registry.lastDoctorAllRunAt
|
|
11195
|
+
const lastRun = registry.lastDoctorAllRunAt ? relativeTime(registry.lastDoctorAllRunAt) : "never";
|
|
11183
11196
|
const lines = [
|
|
11184
11197
|
`**Autopilot:** ${enabled ? "enabled" : "disabled"}`,
|
|
11185
|
-
`**Last
|
|
11198
|
+
`**Last health check run:** ${lastRun}`
|
|
11186
11199
|
];
|
|
11187
11200
|
return {
|
|
11188
11201
|
text: lines.join("\n")
|
|
@@ -11203,8 +11216,8 @@ async function handleDailyReport(ctx) {
|
|
|
11203
11216
|
if (!entry) {
|
|
11204
11217
|
return { text: "This topic is not registered. Run /tm init first." };
|
|
11205
11218
|
}
|
|
11206
|
-
if (entry.
|
|
11207
|
-
const lastReport = new Date(entry.
|
|
11219
|
+
if (entry.lastDailyReportAt) {
|
|
11220
|
+
const lastReport = new Date(entry.lastDailyReportAt);
|
|
11208
11221
|
const now = /* @__PURE__ */ new Date();
|
|
11209
11222
|
if (lastReport.getUTCFullYear() === now.getUTCFullYear() && lastReport.getUTCMonth() === now.getUTCMonth() && lastReport.getUTCDate() === now.getUTCDate()) {
|
|
11210
11223
|
return { text: "Daily report already generated today. Try again tomorrow." };
|
|
@@ -11213,7 +11226,7 @@ async function handleDailyReport(ctx) {
|
|
|
11213
11226
|
const projectsBase = path13.join(workspaceDir, "projects");
|
|
11214
11227
|
const capsuleDir = path13.join(projectsBase, entry.slug);
|
|
11215
11228
|
if (!fs12.existsSync(capsuleDir)) {
|
|
11216
|
-
return { text:
|
|
11229
|
+
return { text: "Topic files not found. Run /tm init to set up this topic." };
|
|
11217
11230
|
}
|
|
11218
11231
|
const statusContent = readFileOrNull(path13.join(capsuleDir, "STATUS.md"));
|
|
11219
11232
|
const todoContent = readFileOrNull(path13.join(capsuleDir, "TODO.md"));
|
|
@@ -11224,7 +11237,7 @@ async function handleDailyReport(ctx) {
|
|
|
11224
11237
|
const nextContent = extractNextActions(statusContent);
|
|
11225
11238
|
const upcomingContent = extractUpcoming(statusContent);
|
|
11226
11239
|
const health = computeHealth(entry.lastMessageAt, statusContent, blockers);
|
|
11227
|
-
const
|
|
11240
|
+
const reportData = {
|
|
11228
11241
|
name: entry.name,
|
|
11229
11242
|
doneContent,
|
|
11230
11243
|
learningsContent: newLearnings,
|
|
@@ -11232,14 +11245,15 @@ async function handleDailyReport(ctx) {
|
|
|
11232
11245
|
nextContent,
|
|
11233
11246
|
upcomingContent,
|
|
11234
11247
|
health
|
|
11235
|
-
}
|
|
11248
|
+
};
|
|
11236
11249
|
if (ctx.postFn) {
|
|
11237
11250
|
try {
|
|
11238
|
-
|
|
11251
|
+
const htmlReport = buildDailyReport(reportData, "html");
|
|
11252
|
+
await ctx.postFn(groupId, threadId, htmlReport);
|
|
11239
11253
|
await withRegistry(workspaceDir, (data) => {
|
|
11240
11254
|
const e = data.topics[key];
|
|
11241
11255
|
if (e) {
|
|
11242
|
-
e.
|
|
11256
|
+
e.lastDailyReportAt = (/* @__PURE__ */ new Date()).toISOString();
|
|
11243
11257
|
}
|
|
11244
11258
|
});
|
|
11245
11259
|
} catch (err) {
|
|
@@ -11251,11 +11265,11 @@ async function handleDailyReport(ctx) {
|
|
|
11251
11265
|
await withRegistry(workspaceDir, (data) => {
|
|
11252
11266
|
const e = data.topics[key];
|
|
11253
11267
|
if (e) {
|
|
11254
|
-
e.
|
|
11268
|
+
e.lastDailyReportAt = (/* @__PURE__ */ new Date()).toISOString();
|
|
11255
11269
|
}
|
|
11256
11270
|
});
|
|
11257
11271
|
}
|
|
11258
|
-
return { text:
|
|
11272
|
+
return { text: buildDailyReport(reportData, "markdown") };
|
|
11259
11273
|
}
|
|
11260
11274
|
function readFileOrNull(filePath) {
|
|
11261
11275
|
try {
|
|
@@ -11505,33 +11519,16 @@ async function handleCallback(data, ctx) {
|
|
|
11505
11519
|
return { text: "Topic not found." };
|
|
11506
11520
|
}
|
|
11507
11521
|
switch (action) {
|
|
11508
|
-
case "fix":
|
|
11509
|
-
return handleCallbackFix(cbCtx);
|
|
11510
11522
|
case "snooze7d":
|
|
11511
11523
|
return handleSnooze(cbCtx, "7d");
|
|
11512
11524
|
case "snooze30d":
|
|
11513
11525
|
return handleSnooze(cbCtx, "30d");
|
|
11514
11526
|
case "archive":
|
|
11515
11527
|
return handleArchive(cbCtx);
|
|
11516
|
-
case "ignore": {
|
|
11517
|
-
return {
|
|
11518
|
-
text: `To ignore a specific check, use: /tm snooze or contact an admin. The "Ignore" action requires specifying a check ID.`
|
|
11519
|
-
};
|
|
11520
|
-
}
|
|
11521
11528
|
default:
|
|
11522
11529
|
return { text: `Unknown callback action: ${action}` };
|
|
11523
11530
|
}
|
|
11524
11531
|
}
|
|
11525
|
-
async function handleCallbackFix(ctx) {
|
|
11526
|
-
const { userId, workspaceDir } = ctx;
|
|
11527
|
-
if (userId) {
|
|
11528
|
-
appendAudit(
|
|
11529
|
-
workspaceDir,
|
|
11530
|
-
buildAuditEntry(userId, "doctor fix", "callback", "Fix callback triggered")
|
|
11531
|
-
);
|
|
11532
|
-
}
|
|
11533
|
-
return handleDoctor(ctx);
|
|
11534
|
-
}
|
|
11535
11532
|
|
|
11536
11533
|
// src/index.ts
|
|
11537
11534
|
function resolveConfigDir() {
|