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.
Files changed (44) hide show
  1. package/dist/lib/include-generator.d.ts +1 -1
  2. package/dist/lib/include-generator.d.ts.map +1 -1
  3. package/dist/lib/include-generator.js +33 -2
  4. package/dist/lib/include-generator.js.map +1 -1
  5. package/dist/plugin.js +29 -2
  6. package/dist/setup.js +25 -8
  7. package/dist/setup.js.map +1 -1
  8. package/package.json +2 -3
  9. package/src/commands/archive.ts +0 -89
  10. package/src/commands/doctor-all.ts +0 -243
  11. package/src/commands/doctor.ts +0 -100
  12. package/src/commands/help.ts +0 -11
  13. package/src/commands/init.ts +0 -376
  14. package/src/commands/list.ts +0 -28
  15. package/src/commands/rename.ts +0 -140
  16. package/src/commands/snooze.ts +0 -69
  17. package/src/commands/status.ts +0 -59
  18. package/src/commands/sync.ts +0 -46
  19. package/src/commands/upgrade.ts +0 -64
  20. package/src/index.ts +0 -91
  21. package/src/lib/audit.ts +0 -44
  22. package/src/lib/auth.ts +0 -96
  23. package/src/lib/capsule.ts +0 -206
  24. package/src/lib/config-restart.ts +0 -167
  25. package/src/lib/doctor-checks.ts +0 -639
  26. package/src/lib/include-generator.ts +0 -174
  27. package/src/lib/registry.ts +0 -197
  28. package/src/lib/security.ts +0 -174
  29. package/src/lib/telegram.ts +0 -311
  30. package/src/lib/types.ts +0 -172
  31. package/src/setup.ts +0 -475
  32. package/src/templates/base/COMMANDS.md +0 -3
  33. package/src/templates/base/CRON.md +0 -3
  34. package/src/templates/base/LINKS.md +0 -3
  35. package/src/templates/base/NOTES.md +0 -3
  36. package/src/templates/base/README.md +0 -3
  37. package/src/templates/base/TODO.md +0 -11
  38. package/src/templates/overlays/coding/ARCHITECTURE.md +0 -3
  39. package/src/templates/overlays/coding/DEPLOY.md +0 -3
  40. package/src/templates/overlays/marketing/CAMPAIGNS.md +0 -3
  41. package/src/templates/overlays/marketing/METRICS.md +0 -3
  42. package/src/templates/overlays/research/FINDINGS.md +0 -3
  43. package/src/templates/overlays/research/SOURCES.md +0 -3
  44. 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
- }
@@ -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
- }
@@ -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
- }