openclaw-telegram-manager 1.3.0 → 1.3.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/dist/lib/include-generator.d.ts +1 -1
- package/dist/lib/include-generator.d.ts.map +1 -1
- package/dist/lib/include-generator.js +33 -2
- package/dist/lib/include-generator.js.map +1 -1
- package/dist/plugin.js +29 -2
- package/dist/setup.js +25 -8
- package/dist/setup.js.map +1 -1
- package/package.json +2 -3
- package/src/commands/archive.ts +0 -89
- package/src/commands/doctor-all.ts +0 -243
- package/src/commands/doctor.ts +0 -100
- package/src/commands/help.ts +0 -11
- package/src/commands/init.ts +0 -376
- package/src/commands/list.ts +0 -28
- package/src/commands/rename.ts +0 -140
- package/src/commands/snooze.ts +0 -69
- package/src/commands/status.ts +0 -59
- package/src/commands/sync.ts +0 -46
- package/src/commands/upgrade.ts +0 -64
- package/src/index.ts +0 -91
- package/src/lib/audit.ts +0 -44
- package/src/lib/auth.ts +0 -96
- package/src/lib/capsule.ts +0 -206
- package/src/lib/config-restart.ts +0 -167
- package/src/lib/doctor-checks.ts +0 -639
- package/src/lib/include-generator.ts +0 -174
- package/src/lib/registry.ts +0 -197
- package/src/lib/security.ts +0 -174
- package/src/lib/telegram.ts +0 -311
- package/src/lib/types.ts +0 -172
- package/src/setup.ts +0 -475
- package/src/templates/base/COMMANDS.md +0 -3
- package/src/templates/base/CRON.md +0 -3
- package/src/templates/base/LINKS.md +0 -3
- package/src/templates/base/NOTES.md +0 -3
- package/src/templates/base/README.md +0 -3
- package/src/templates/base/TODO.md +0 -11
- package/src/templates/overlays/coding/ARCHITECTURE.md +0 -3
- package/src/templates/overlays/coding/DEPLOY.md +0 -3
- package/src/templates/overlays/marketing/CAMPAIGNS.md +0 -3
- package/src/templates/overlays/marketing/METRICS.md +0 -3
- package/src/templates/overlays/research/FINDINGS.md +0 -3
- package/src/templates/overlays/research/SOURCES.md +0 -3
- package/src/tool.ts +0 -282
|
@@ -1,243 +0,0 @@
|
|
|
1
|
-
import * as fs from 'node:fs';
|
|
2
|
-
import * as path from 'node:path';
|
|
3
|
-
import { readRegistry, withRegistry } from '../lib/registry.js';
|
|
4
|
-
import { checkAuthorization } from '../lib/auth.js';
|
|
5
|
-
import {
|
|
6
|
-
DOCTOR_ALL_COOLDOWN_MS,
|
|
7
|
-
DOCTOR_PER_TOPIC_CAP_MS,
|
|
8
|
-
INACTIVE_AFTER_DAYS,
|
|
9
|
-
SPAM_THRESHOLD,
|
|
10
|
-
} from '../lib/types.js';
|
|
11
|
-
import type { TopicEntry, InlineKeyboardMarkup } from '../lib/types.js';
|
|
12
|
-
import { htmlEscape } from '../lib/security.js';
|
|
13
|
-
import { buildDoctorReport, buildDoctorButtons } from '../lib/telegram.js';
|
|
14
|
-
import { runAllChecksForTopic } from '../lib/doctor-checks.js';
|
|
15
|
-
import { includePath } from '../lib/include-generator.js';
|
|
16
|
-
import type { CommandContext, CommandResult } from './help.js';
|
|
17
|
-
|
|
18
|
-
interface TopicReport {
|
|
19
|
-
slug: string;
|
|
20
|
-
groupId: string;
|
|
21
|
-
threadId: string;
|
|
22
|
-
text: string;
|
|
23
|
-
keyboard: InlineKeyboardMarkup;
|
|
24
|
-
error?: string;
|
|
25
|
-
}
|
|
26
|
-
|
|
27
|
-
export async function handleDoctorAll(ctx: CommandContext): Promise<CommandResult> {
|
|
28
|
-
const { workspaceDir, configDir, userId, logger } = ctx;
|
|
29
|
-
|
|
30
|
-
if (!userId) {
|
|
31
|
-
return { text: 'Missing context: userId not available.' };
|
|
32
|
-
}
|
|
33
|
-
|
|
34
|
-
const registry = readRegistry(workspaceDir);
|
|
35
|
-
|
|
36
|
-
// Auth check (admin tier)
|
|
37
|
-
const auth = checkAuthorization(userId, 'doctor-all', registry);
|
|
38
|
-
if (!auth.authorized) {
|
|
39
|
-
return { text: auth.message ?? 'Not authorized.' };
|
|
40
|
-
}
|
|
41
|
-
|
|
42
|
-
// Global cooldown check
|
|
43
|
-
if (registry.lastDoctorAllRunAt) {
|
|
44
|
-
const lastRun = new Date(registry.lastDoctorAllRunAt).getTime();
|
|
45
|
-
const elapsed = Date.now() - lastRun;
|
|
46
|
-
if (elapsed < DOCTOR_ALL_COOLDOWN_MS) {
|
|
47
|
-
const remainingMin = Math.ceil((DOCTOR_ALL_COOLDOWN_MS - elapsed) / 60_000);
|
|
48
|
-
return {
|
|
49
|
-
text: `Doctor-all was run ${Math.floor(elapsed / 60_000)} minutes ago. Try again in ${remainingMin} minute(s).`,
|
|
50
|
-
};
|
|
51
|
-
}
|
|
52
|
-
}
|
|
53
|
-
|
|
54
|
-
const now = new Date();
|
|
55
|
-
const projectsBase = path.join(workspaceDir, 'projects');
|
|
56
|
-
|
|
57
|
-
// Read include content for config checks (optional)
|
|
58
|
-
let includeContent: string | undefined;
|
|
59
|
-
const incPath = includePath(configDir);
|
|
60
|
-
try {
|
|
61
|
-
if (fs.existsSync(incPath)) {
|
|
62
|
-
includeContent = fs.readFileSync(incPath, 'utf-8');
|
|
63
|
-
}
|
|
64
|
-
} catch {
|
|
65
|
-
// Not critical
|
|
66
|
-
}
|
|
67
|
-
|
|
68
|
-
const cronJobsPath = path.join(configDir, 'cron', 'jobs.json');
|
|
69
|
-
const allEntries = Object.entries(registry.topics);
|
|
70
|
-
const reports: TopicReport[] = [];
|
|
71
|
-
const errors: string[] = [];
|
|
72
|
-
let processed = 0;
|
|
73
|
-
let skipped = 0;
|
|
74
|
-
|
|
75
|
-
// Group tracking for migration detection
|
|
76
|
-
const groupPostResults = new Map<string, { total: number; failed: number }>();
|
|
77
|
-
|
|
78
|
-
for (const [_key, entry] of allEntries) {
|
|
79
|
-
// Eligibility gating
|
|
80
|
-
if (!isEligible(entry, now)) {
|
|
81
|
-
skipped++;
|
|
82
|
-
continue;
|
|
83
|
-
}
|
|
84
|
-
|
|
85
|
-
try {
|
|
86
|
-
const results = runAllChecksForTopic(
|
|
87
|
-
entry,
|
|
88
|
-
projectsBase,
|
|
89
|
-
includeContent,
|
|
90
|
-
registry,
|
|
91
|
-
cronJobsPath,
|
|
92
|
-
);
|
|
93
|
-
|
|
94
|
-
// Spam control: auto-snooze if threshold reached
|
|
95
|
-
const isSpam = entry.consecutiveSilentDoctors >= SPAM_THRESHOLD;
|
|
96
|
-
if (isSpam) {
|
|
97
|
-
// Auto-snooze is handled via the check result; we just note it
|
|
98
|
-
logger.info(`[doctor-all] Auto-snoozing ${entry.slug} (${entry.consecutiveSilentDoctors} silent runs)`);
|
|
99
|
-
}
|
|
100
|
-
|
|
101
|
-
const reportText = buildDoctorReport(entry.slug, results);
|
|
102
|
-
const keyboard = buildDoctorButtons(
|
|
103
|
-
entry.slug,
|
|
104
|
-
entry.groupId,
|
|
105
|
-
entry.threadId,
|
|
106
|
-
registry.callbackSecret,
|
|
107
|
-
);
|
|
108
|
-
|
|
109
|
-
reports.push({
|
|
110
|
-
slug: entry.slug,
|
|
111
|
-
groupId: entry.groupId,
|
|
112
|
-
threadId: entry.threadId,
|
|
113
|
-
text: reportText,
|
|
114
|
-
keyboard,
|
|
115
|
-
});
|
|
116
|
-
|
|
117
|
-
// Track group post results for migration detection
|
|
118
|
-
const gk = entry.groupId;
|
|
119
|
-
if (!groupPostResults.has(gk)) {
|
|
120
|
-
groupPostResults.set(gk, { total: 0, failed: 0 });
|
|
121
|
-
}
|
|
122
|
-
const group = groupPostResults.get(gk)!;
|
|
123
|
-
group.total++;
|
|
124
|
-
|
|
125
|
-
processed++;
|
|
126
|
-
} catch (err) {
|
|
127
|
-
const msg = err instanceof Error ? err.message : String(err);
|
|
128
|
-
errors.push(`${entry.slug}: ${msg}`);
|
|
129
|
-
logger.error(`[doctor-all] Error processing ${entry.slug}: ${msg}`);
|
|
130
|
-
|
|
131
|
-
// Track failures for migration detection
|
|
132
|
-
const gk = entry.groupId;
|
|
133
|
-
if (!groupPostResults.has(gk)) {
|
|
134
|
-
groupPostResults.set(gk, { total: 0, failed: 0 });
|
|
135
|
-
}
|
|
136
|
-
const group = groupPostResults.get(gk)!;
|
|
137
|
-
group.total++;
|
|
138
|
-
group.failed++;
|
|
139
|
-
}
|
|
140
|
-
}
|
|
141
|
-
|
|
142
|
-
// Migration detection: all topics in a group failed
|
|
143
|
-
const migrationGroups: string[] = [];
|
|
144
|
-
for (const [gid, stats] of groupPostResults) {
|
|
145
|
-
if (stats.total > 0 && stats.failed === stats.total) {
|
|
146
|
-
migrationGroups.push(gid);
|
|
147
|
-
}
|
|
148
|
-
}
|
|
149
|
-
|
|
150
|
-
// Update registry: lastDoctorAllRunAt and per-topic timestamps
|
|
151
|
-
await withRegistry(workspaceDir, (data) => {
|
|
152
|
-
data.lastDoctorAllRunAt = now.toISOString();
|
|
153
|
-
|
|
154
|
-
for (const [_key, entry] of Object.entries(data.topics)) {
|
|
155
|
-
if (!isEligible(entry, now)) continue;
|
|
156
|
-
|
|
157
|
-
// Update consecutiveSilentDoctors (compare against old lastDoctorRunAt before overwriting)
|
|
158
|
-
if (entry.lastMessageAt) {
|
|
159
|
-
const lastMsg = new Date(entry.lastMessageAt).getTime();
|
|
160
|
-
const lastDoctor = entry.lastDoctorRunAt
|
|
161
|
-
? new Date(entry.lastDoctorRunAt).getTime()
|
|
162
|
-
: 0;
|
|
163
|
-
|
|
164
|
-
if (lastMsg > lastDoctor) {
|
|
165
|
-
// User interacted since last doctor run — reset counter
|
|
166
|
-
entry.consecutiveSilentDoctors = 0;
|
|
167
|
-
} else {
|
|
168
|
-
entry.consecutiveSilentDoctors++;
|
|
169
|
-
}
|
|
170
|
-
} else {
|
|
171
|
-
entry.consecutiveSilentDoctors++;
|
|
172
|
-
}
|
|
173
|
-
|
|
174
|
-
entry.lastDoctorRunAt = now.toISOString();
|
|
175
|
-
|
|
176
|
-
// Auto-snooze for spam control
|
|
177
|
-
if (entry.consecutiveSilentDoctors >= SPAM_THRESHOLD) {
|
|
178
|
-
entry.snoozeUntil = new Date(Date.now() + 30 * 24 * 60 * 60 * 1000).toISOString();
|
|
179
|
-
entry.status = 'snoozed';
|
|
180
|
-
entry.consecutiveSilentDoctors = 0;
|
|
181
|
-
}
|
|
182
|
-
}
|
|
183
|
-
});
|
|
184
|
-
|
|
185
|
-
// Build summary
|
|
186
|
-
const lines: string[] = [
|
|
187
|
-
`<b>Doctor All Summary</b>`,
|
|
188
|
-
'',
|
|
189
|
-
`Processed: ${processed}`,
|
|
190
|
-
`Skipped (ineligible): ${skipped}`,
|
|
191
|
-
`Total: ${allEntries.length}`,
|
|
192
|
-
];
|
|
193
|
-
|
|
194
|
-
if (errors.length > 0) {
|
|
195
|
-
lines.push('');
|
|
196
|
-
lines.push(`<b>Errors (${errors.length}):</b>`);
|
|
197
|
-
for (const e of errors.slice(0, 10)) {
|
|
198
|
-
lines.push(`- ${htmlEscape(e)}`);
|
|
199
|
-
}
|
|
200
|
-
if (errors.length > 10) {
|
|
201
|
-
lines.push(`... and ${errors.length - 10} more`);
|
|
202
|
-
}
|
|
203
|
-
}
|
|
204
|
-
|
|
205
|
-
if (migrationGroups.length > 0) {
|
|
206
|
-
lines.push('');
|
|
207
|
-
lines.push('<b>Possible group migrations detected:</b>');
|
|
208
|
-
for (const gid of migrationGroups) {
|
|
209
|
-
lines.push(`- Group ${htmlEscape(gid)}: all topics failed. Check for group migration.`);
|
|
210
|
-
}
|
|
211
|
-
}
|
|
212
|
-
|
|
213
|
-
// Per-topic reports are returned as a summary for the calling context.
|
|
214
|
-
// The tool router is responsible for posting individual reports to each topic.
|
|
215
|
-
// Here we return the summary to the current thread.
|
|
216
|
-
return {
|
|
217
|
-
text: lines.join('\n'),
|
|
218
|
-
parseMode: 'HTML',
|
|
219
|
-
};
|
|
220
|
-
}
|
|
221
|
-
|
|
222
|
-
function isEligible(entry: TopicEntry, now: Date): boolean {
|
|
223
|
-
// Skip archived
|
|
224
|
-
if (entry.status === 'archived') return false;
|
|
225
|
-
|
|
226
|
-
// Skip snoozed
|
|
227
|
-
if (entry.snoozeUntil && new Date(entry.snoozeUntil).getTime() > now.getTime()) return false;
|
|
228
|
-
|
|
229
|
-
// Skip inactive (no activity for INACTIVE_AFTER_DAYS)
|
|
230
|
-
if (entry.lastMessageAt) {
|
|
231
|
-
const lastActive = new Date(entry.lastMessageAt).getTime();
|
|
232
|
-
const inactiveMs = INACTIVE_AFTER_DAYS * 24 * 60 * 60 * 1000;
|
|
233
|
-
if (now.getTime() - lastActive > inactiveMs) return false;
|
|
234
|
-
}
|
|
235
|
-
|
|
236
|
-
// Skip if reported in last 24 hours
|
|
237
|
-
if (entry.lastDoctorReportAt) {
|
|
238
|
-
const lastReport = new Date(entry.lastDoctorReportAt).getTime();
|
|
239
|
-
if (now.getTime() - lastReport < DOCTOR_PER_TOPIC_CAP_MS) return false;
|
|
240
|
-
}
|
|
241
|
-
|
|
242
|
-
return true;
|
|
243
|
-
}
|
package/src/commands/doctor.ts
DELETED
|
@@ -1,100 +0,0 @@
|
|
|
1
|
-
import * as fs from 'node:fs';
|
|
2
|
-
import * as path from 'node:path';
|
|
3
|
-
import { readRegistry, withRegistry } from '../lib/registry.js';
|
|
4
|
-
import { checkAuthorization } from '../lib/auth.js';
|
|
5
|
-
import { topicKey } from '../lib/types.js';
|
|
6
|
-
import { jailCheck, rejectSymlink } from '../lib/security.js';
|
|
7
|
-
import { buildDoctorReport, buildDoctorButtons } from '../lib/telegram.js';
|
|
8
|
-
import { runAllChecksForTopic } from '../lib/doctor-checks.js';
|
|
9
|
-
import { includePath } from '../lib/include-generator.js';
|
|
10
|
-
import type { CommandContext, CommandResult } from './help.js';
|
|
11
|
-
|
|
12
|
-
export async function handleDoctor(ctx: CommandContext): Promise<CommandResult> {
|
|
13
|
-
const { workspaceDir, configDir, userId, groupId, threadId } = ctx;
|
|
14
|
-
|
|
15
|
-
if (!userId || !groupId || !threadId) {
|
|
16
|
-
return { text: 'Missing context: userId, groupId, or threadId not available.' };
|
|
17
|
-
}
|
|
18
|
-
|
|
19
|
-
const registry = readRegistry(workspaceDir);
|
|
20
|
-
|
|
21
|
-
// Auth check (user tier)
|
|
22
|
-
const auth = checkAuthorization(userId, 'doctor', registry);
|
|
23
|
-
if (!auth.authorized) {
|
|
24
|
-
return { text: auth.message ?? 'Not authorized.' };
|
|
25
|
-
}
|
|
26
|
-
|
|
27
|
-
const key = topicKey(groupId, threadId);
|
|
28
|
-
const entry = registry.topics[key];
|
|
29
|
-
|
|
30
|
-
if (!entry) {
|
|
31
|
-
return { text: 'This topic is not registered. Run /topic init first.' };
|
|
32
|
-
}
|
|
33
|
-
|
|
34
|
-
const projectsBase = path.join(workspaceDir, 'projects');
|
|
35
|
-
|
|
36
|
-
// Path safety
|
|
37
|
-
if (!jailCheck(projectsBase, entry.slug)) {
|
|
38
|
-
return { text: 'Path safety check failed.' };
|
|
39
|
-
}
|
|
40
|
-
|
|
41
|
-
const capsuleDir = path.join(projectsBase, entry.slug);
|
|
42
|
-
if (rejectSymlink(capsuleDir)) {
|
|
43
|
-
return { text: 'Capsule directory is a symlink. Aborting for security.' };
|
|
44
|
-
}
|
|
45
|
-
|
|
46
|
-
// Read include content for config checks (optional)
|
|
47
|
-
let includeContent: string | undefined;
|
|
48
|
-
const incPath = includePath(configDir);
|
|
49
|
-
try {
|
|
50
|
-
if (fs.existsSync(incPath)) {
|
|
51
|
-
includeContent = fs.readFileSync(incPath, 'utf-8');
|
|
52
|
-
}
|
|
53
|
-
} catch {
|
|
54
|
-
// Not critical
|
|
55
|
-
}
|
|
56
|
-
|
|
57
|
-
// Run all checks
|
|
58
|
-
const cronJobsPath = path.join(configDir, 'cron', 'jobs.json');
|
|
59
|
-
const results = runAllChecksForTopic(
|
|
60
|
-
entry,
|
|
61
|
-
projectsBase,
|
|
62
|
-
includeContent,
|
|
63
|
-
registry,
|
|
64
|
-
cronJobsPath,
|
|
65
|
-
);
|
|
66
|
-
|
|
67
|
-
// Build report
|
|
68
|
-
const reportText = buildDoctorReport(entry.slug, results);
|
|
69
|
-
|
|
70
|
-
// Build inline keyboard with HMAC-signed callbacks
|
|
71
|
-
const keyboard = buildDoctorButtons(
|
|
72
|
-
entry.slug,
|
|
73
|
-
groupId,
|
|
74
|
-
threadId,
|
|
75
|
-
registry.callbackSecret,
|
|
76
|
-
);
|
|
77
|
-
|
|
78
|
-
// Append text command equivalents
|
|
79
|
-
const textCommands = [
|
|
80
|
-
'',
|
|
81
|
-
'Or use text commands:',
|
|
82
|
-
'/topic snooze 7d',
|
|
83
|
-
'/topic snooze 30d',
|
|
84
|
-
'/topic archive',
|
|
85
|
-
].join('\n');
|
|
86
|
-
|
|
87
|
-
// Update lastDoctorReportAt
|
|
88
|
-
await withRegistry(workspaceDir, (data) => {
|
|
89
|
-
const topic = data.topics[key];
|
|
90
|
-
if (topic) {
|
|
91
|
-
topic.lastDoctorReportAt = new Date().toISOString();
|
|
92
|
-
}
|
|
93
|
-
});
|
|
94
|
-
|
|
95
|
-
return {
|
|
96
|
-
text: reportText + textCommands,
|
|
97
|
-
parseMode: 'HTML',
|
|
98
|
-
inlineKeyboard: keyboard,
|
|
99
|
-
};
|
|
100
|
-
}
|
package/src/commands/help.ts
DELETED
|
@@ -1,11 +0,0 @@
|
|
|
1
|
-
import { buildHelpCard } from '../lib/telegram.js';
|
|
2
|
-
import type { CommandContext, CommandResult } from '../lib/types.js';
|
|
3
|
-
|
|
4
|
-
export type { CommandContext, CommandResult };
|
|
5
|
-
|
|
6
|
-
export function handleHelp(_ctx: CommandContext): CommandResult {
|
|
7
|
-
return {
|
|
8
|
-
text: buildHelpCard(),
|
|
9
|
-
parseMode: 'HTML',
|
|
10
|
-
};
|
|
11
|
-
}
|