openclaw-telegram-manager 1.3.0 → 1.3.2
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 +33 -15
- 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
package/src/lib/telegram.ts
DELETED
|
@@ -1,311 +0,0 @@
|
|
|
1
|
-
import { htmlEscape, buildCallbackData } from './security.js';
|
|
2
|
-
import type { TopicEntry, DoctorCheckResult, InlineKeyboardButton, InlineKeyboardMarkup } from './types.js';
|
|
3
|
-
import { Severity } from './types.js';
|
|
4
|
-
import type { TopicType } from './types.js';
|
|
5
|
-
|
|
6
|
-
// ── Telegram message limit ─────────────────────────────────────────────
|
|
7
|
-
|
|
8
|
-
const TELEGRAM_MSG_LIMIT = 4096;
|
|
9
|
-
|
|
10
|
-
// Re-export keyboard types from canonical location
|
|
11
|
-
export type { InlineKeyboardButton, InlineKeyboardMarkup } from './types.js';
|
|
12
|
-
|
|
13
|
-
// ── Rate limiting config ───────────────────────────────────────────────
|
|
14
|
-
|
|
15
|
-
export interface RateLimitConfig {
|
|
16
|
-
sameGroupDelayMs: number;
|
|
17
|
-
crossGroupDelayMs: number;
|
|
18
|
-
}
|
|
19
|
-
|
|
20
|
-
export const DEFAULT_RATE_LIMIT: RateLimitConfig = {
|
|
21
|
-
sameGroupDelayMs: 4000,
|
|
22
|
-
crossGroupDelayMs: 1000,
|
|
23
|
-
};
|
|
24
|
-
|
|
25
|
-
// ── Builders ───────────────────────────────────────────────────────────
|
|
26
|
-
|
|
27
|
-
/**
|
|
28
|
-
* Build an InlineKeyboardMarkup from rows of buttons.
|
|
29
|
-
*/
|
|
30
|
-
export function buildInlineKeyboard(rows: InlineKeyboardButton[][]): InlineKeyboardMarkup {
|
|
31
|
-
return { inline_keyboard: rows };
|
|
32
|
-
}
|
|
33
|
-
|
|
34
|
-
/**
|
|
35
|
-
* Build inline keyboard buttons for a doctor report.
|
|
36
|
-
*/
|
|
37
|
-
export function buildDoctorButtons(
|
|
38
|
-
slug: string,
|
|
39
|
-
groupId: string,
|
|
40
|
-
threadId: string,
|
|
41
|
-
secret: string,
|
|
42
|
-
): InlineKeyboardMarkup {
|
|
43
|
-
const cb = (action: string) => buildCallbackData(action, slug, groupId, threadId, secret);
|
|
44
|
-
return buildInlineKeyboard([
|
|
45
|
-
[
|
|
46
|
-
{ text: 'Fix', callback_data: cb('fix') },
|
|
47
|
-
{ text: 'Snooze 7d', callback_data: cb('snooze7d') },
|
|
48
|
-
{ text: 'Snooze 30d', callback_data: cb('snooze30d') },
|
|
49
|
-
],
|
|
50
|
-
[
|
|
51
|
-
{ text: 'Archive', callback_data: cb('archive') },
|
|
52
|
-
{ text: 'Ignore check', callback_data: cb('ignore') },
|
|
53
|
-
],
|
|
54
|
-
]);
|
|
55
|
-
}
|
|
56
|
-
|
|
57
|
-
/**
|
|
58
|
-
* Build inline keyboard with a [Confirm] button for slug confirmation (init step 1).
|
|
59
|
-
*/
|
|
60
|
-
export function buildInitSlugButtons(
|
|
61
|
-
slug: string,
|
|
62
|
-
groupId: string,
|
|
63
|
-
threadId: string,
|
|
64
|
-
secret: string,
|
|
65
|
-
): InlineKeyboardMarkup {
|
|
66
|
-
const cb = (action: string) => buildCallbackData(action, slug, groupId, threadId, secret);
|
|
67
|
-
return buildInlineKeyboard([
|
|
68
|
-
[{ text: 'Confirm', callback_data: cb('is') }],
|
|
69
|
-
]);
|
|
70
|
-
}
|
|
71
|
-
|
|
72
|
-
/**
|
|
73
|
-
* Build inline keyboard with type picker buttons for init step 2.
|
|
74
|
-
*/
|
|
75
|
-
export function buildInitTypeButtons(
|
|
76
|
-
slug: string,
|
|
77
|
-
groupId: string,
|
|
78
|
-
threadId: string,
|
|
79
|
-
secret: string,
|
|
80
|
-
): InlineKeyboardMarkup {
|
|
81
|
-
const cb = (action: string) => buildCallbackData(action, slug, groupId, threadId, secret);
|
|
82
|
-
return buildInlineKeyboard([
|
|
83
|
-
[
|
|
84
|
-
{ text: 'Coding', callback_data: cb('ic') },
|
|
85
|
-
{ text: 'Research', callback_data: cb('ir') },
|
|
86
|
-
],
|
|
87
|
-
[
|
|
88
|
-
{ text: 'Marketing', callback_data: cb('im') },
|
|
89
|
-
{ text: 'Custom', callback_data: cb('ix') },
|
|
90
|
-
],
|
|
91
|
-
]);
|
|
92
|
-
}
|
|
93
|
-
|
|
94
|
-
/**
|
|
95
|
-
* Build HTML Topic Card displayed after init.
|
|
96
|
-
*/
|
|
97
|
-
export function buildTopicCard(slug: string, type: TopicType, capsuleVersion: number): string {
|
|
98
|
-
const s = htmlEscape(slug);
|
|
99
|
-
const t = htmlEscape(type);
|
|
100
|
-
const v = htmlEscape(String(capsuleVersion));
|
|
101
|
-
return [
|
|
102
|
-
`<b>Topic: ${s}</b>`,
|
|
103
|
-
`Type: ${t} | Version: ${v}`,
|
|
104
|
-
`Capsule: projects/${s}/`,
|
|
105
|
-
'',
|
|
106
|
-
'<b>Commands:</b>',
|
|
107
|
-
'/topic doctor \u2014 health checks',
|
|
108
|
-
'/topic status \u2014 quick view',
|
|
109
|
-
'/topic sync \u2014 re-apply config',
|
|
110
|
-
'/topic list \u2014 all topics',
|
|
111
|
-
'/topic archive \u2014 archive this topic',
|
|
112
|
-
'/topic help \u2014 command reference',
|
|
113
|
-
].join('\n');
|
|
114
|
-
}
|
|
115
|
-
|
|
116
|
-
/**
|
|
117
|
-
* Build HTML doctor report with severity icons.
|
|
118
|
-
*/
|
|
119
|
-
export function buildDoctorReport(slug: string, results: DoctorCheckResult[]): string {
|
|
120
|
-
const s = htmlEscape(slug);
|
|
121
|
-
const lines: string[] = [`<b>Doctor: ${s}</b>`, ''];
|
|
122
|
-
|
|
123
|
-
if (results.length === 0) {
|
|
124
|
-
lines.push('All checks passed.');
|
|
125
|
-
return lines.join('\n');
|
|
126
|
-
}
|
|
127
|
-
|
|
128
|
-
for (const r of results) {
|
|
129
|
-
const icon = severityIcon(r.severity);
|
|
130
|
-
const msg = htmlEscape(r.message);
|
|
131
|
-
const fix = r.fixable ? ' [fixable]' : '';
|
|
132
|
-
lines.push(`${icon} <code>${htmlEscape(r.checkId)}</code>: ${msg}${fix}`);
|
|
133
|
-
}
|
|
134
|
-
|
|
135
|
-
lines.push('');
|
|
136
|
-
lines.push('Reply /topic doctor to re-check, or use the buttons below.');
|
|
137
|
-
|
|
138
|
-
return truncateMessage(lines.join('\n'));
|
|
139
|
-
}
|
|
140
|
-
|
|
141
|
-
function severityIcon(severity: Severity): string {
|
|
142
|
-
switch (severity) {
|
|
143
|
-
case Severity.ERROR:
|
|
144
|
-
return '\u274c'; // red X
|
|
145
|
-
case Severity.WARN:
|
|
146
|
-
return '\u26a0\ufe0f'; // warning
|
|
147
|
-
case Severity.INFO:
|
|
148
|
-
return '\u2139\ufe0f'; // info
|
|
149
|
-
default:
|
|
150
|
-
return '\u2022';
|
|
151
|
-
}
|
|
152
|
-
}
|
|
153
|
-
|
|
154
|
-
/**
|
|
155
|
-
* Build HTML help card with command reference.
|
|
156
|
-
*/
|
|
157
|
-
export function buildHelpCard(): string {
|
|
158
|
-
return [
|
|
159
|
-
'<b>Topic Manager Commands</b>',
|
|
160
|
-
'',
|
|
161
|
-
'/topic init \u2014 register this topic',
|
|
162
|
-
'/topic doctor \u2014 run health checks',
|
|
163
|
-
'/topic doctor --all \u2014 check all topics',
|
|
164
|
-
'/topic status \u2014 quick STATUS.md view',
|
|
165
|
-
'/topic list \u2014 show all topics',
|
|
166
|
-
'/topic sync \u2014 re-apply config',
|
|
167
|
-
'/topic rename <slug> \u2014 rename topic',
|
|
168
|
-
'/topic upgrade \u2014 update capsule template',
|
|
169
|
-
'/topic snooze <Nd> \u2014 snooze doctor (7d, 30d, etc.)',
|
|
170
|
-
'/topic archive \u2014 archive topic',
|
|
171
|
-
'/topic unarchive \u2014 reactivate topic',
|
|
172
|
-
'/topic help \u2014 this message',
|
|
173
|
-
].join('\n');
|
|
174
|
-
}
|
|
175
|
-
|
|
176
|
-
/**
|
|
177
|
-
* Build compact topic list message in HTML.
|
|
178
|
-
* Groups by status: active first, snoozed, then archived.
|
|
179
|
-
*/
|
|
180
|
-
export function buildListMessage(topics: TopicEntry[]): string {
|
|
181
|
-
if (topics.length === 0) {
|
|
182
|
-
return '<b>Topic Registry</b> (0 topics)\n\nNo topics registered.';
|
|
183
|
-
}
|
|
184
|
-
|
|
185
|
-
const sorted = [...topics].sort((a, b) => {
|
|
186
|
-
const order = { active: 0, snoozed: 1, archived: 2 };
|
|
187
|
-
return (order[a.status] ?? 3) - (order[b.status] ?? 3);
|
|
188
|
-
});
|
|
189
|
-
|
|
190
|
-
const lines: string[] = [`<b>Topic Registry</b> (${topics.length} topics)`, ''];
|
|
191
|
-
let rendered = 0;
|
|
192
|
-
|
|
193
|
-
for (const t of sorted) {
|
|
194
|
-
const entry = [
|
|
195
|
-
`<code>${htmlEscape(t.slug)}</code> [${htmlEscape(t.type)}] ${htmlEscape(t.status)}`,
|
|
196
|
-
` Last active: ${t.lastMessageAt ? relativeTime(t.lastMessageAt) : 'never'}`,
|
|
197
|
-
` Thread: #${htmlEscape(t.threadId)}`,
|
|
198
|
-
].join('\n');
|
|
199
|
-
|
|
200
|
-
// Check if adding this entry would exceed limit
|
|
201
|
-
const tentative = [...lines, entry, ''].join('\n');
|
|
202
|
-
if (tentative.length > TELEGRAM_MSG_LIMIT - 40) {
|
|
203
|
-
const remaining = sorted.length - rendered;
|
|
204
|
-
lines.push(`... and ${remaining} more`);
|
|
205
|
-
break;
|
|
206
|
-
}
|
|
207
|
-
|
|
208
|
-
lines.push(entry);
|
|
209
|
-
lines.push('');
|
|
210
|
-
rendered++;
|
|
211
|
-
}
|
|
212
|
-
|
|
213
|
-
return truncateMessage(lines.join('\n'));
|
|
214
|
-
}
|
|
215
|
-
|
|
216
|
-
/**
|
|
217
|
-
* Convert an ISO timestamp to a relative time string.
|
|
218
|
-
*/
|
|
219
|
-
function relativeTime(iso: string): string {
|
|
220
|
-
const diff = Date.now() - new Date(iso).getTime();
|
|
221
|
-
const mins = Math.floor(diff / 60_000);
|
|
222
|
-
if (mins < 1) return 'just now';
|
|
223
|
-
if (mins < 60) return `${mins}m ago`;
|
|
224
|
-
const hours = Math.floor(mins / 60);
|
|
225
|
-
if (hours < 24) return `${hours}h ago`;
|
|
226
|
-
const days = Math.floor(hours / 24);
|
|
227
|
-
return `${days}d ago`;
|
|
228
|
-
}
|
|
229
|
-
|
|
230
|
-
// ── Rate-limited posting helper ────────────────────────────────────────
|
|
231
|
-
|
|
232
|
-
/**
|
|
233
|
-
* Helper that wraps a post function with rate limiting delays.
|
|
234
|
-
* Returns a function that posts messages respecting Telegram rate limits.
|
|
235
|
-
*
|
|
236
|
-
* The postFn should handle the actual Telegram API call.
|
|
237
|
-
* If postFn throws with a 429 status, the helper respects retry_after.
|
|
238
|
-
*/
|
|
239
|
-
export function createRateLimitedPoster(
|
|
240
|
-
postFn: (groupId: string, threadId: string, text: string, keyboard?: InlineKeyboardMarkup) => Promise<void>,
|
|
241
|
-
config: RateLimitConfig = DEFAULT_RATE_LIMIT,
|
|
242
|
-
): (groupId: string, threadId: string, text: string, keyboard?: InlineKeyboardMarkup) => Promise<void> {
|
|
243
|
-
let lastPostTime = 0;
|
|
244
|
-
let lastGroupId = '';
|
|
245
|
-
|
|
246
|
-
return async (groupId, threadId, text, keyboard) => {
|
|
247
|
-
const now = Date.now();
|
|
248
|
-
const delay = groupId === lastGroupId
|
|
249
|
-
? config.sameGroupDelayMs
|
|
250
|
-
: config.crossGroupDelayMs;
|
|
251
|
-
const elapsed = now - lastPostTime;
|
|
252
|
-
|
|
253
|
-
if (elapsed < delay) {
|
|
254
|
-
await sleep(delay - elapsed);
|
|
255
|
-
}
|
|
256
|
-
|
|
257
|
-
try {
|
|
258
|
-
await postFn(groupId, threadId, text, keyboard);
|
|
259
|
-
} catch (err: unknown) {
|
|
260
|
-
if (isTooManyRequestsError(err)) {
|
|
261
|
-
const retryAfter = getRetryAfter(err);
|
|
262
|
-
await sleep(retryAfter * 1000);
|
|
263
|
-
await postFn(groupId, threadId, text, keyboard);
|
|
264
|
-
} else {
|
|
265
|
-
throw err;
|
|
266
|
-
}
|
|
267
|
-
}
|
|
268
|
-
|
|
269
|
-
lastPostTime = Date.now();
|
|
270
|
-
lastGroupId = groupId;
|
|
271
|
-
};
|
|
272
|
-
}
|
|
273
|
-
|
|
274
|
-
function sleep(ms: number): Promise<void> {
|
|
275
|
-
return new Promise((resolve) => setTimeout(resolve, ms));
|
|
276
|
-
}
|
|
277
|
-
|
|
278
|
-
function isTooManyRequestsError(err: unknown): boolean {
|
|
279
|
-
if (err && typeof err === 'object' && 'status' in err) {
|
|
280
|
-
const status = (err as Record<string, unknown>)['status'];
|
|
281
|
-
return typeof status === 'number' && status === 429;
|
|
282
|
-
}
|
|
283
|
-
return false;
|
|
284
|
-
}
|
|
285
|
-
|
|
286
|
-
function getRetryAfter(err: unknown): number {
|
|
287
|
-
if (err && typeof err === 'object' && 'retryAfter' in err) {
|
|
288
|
-
const val = (err as { retryAfter: unknown }).retryAfter;
|
|
289
|
-
if (typeof val === 'number' && val > 0) return val;
|
|
290
|
-
}
|
|
291
|
-
// Default to 5 seconds if retry_after is not available
|
|
292
|
-
return 5;
|
|
293
|
-
}
|
|
294
|
-
|
|
295
|
-
// ── Message truncation ─────────────────────────────────────────────────
|
|
296
|
-
|
|
297
|
-
/**
|
|
298
|
-
* Truncate a message to fit within Telegram's limit.
|
|
299
|
-
* Appends a truncation indicator if the message was cut.
|
|
300
|
-
*/
|
|
301
|
-
export function truncateMessage(msg: string, limit: number = TELEGRAM_MSG_LIMIT): string {
|
|
302
|
-
if (msg.length <= limit) return msg;
|
|
303
|
-
const suffix = '\n\n... (truncated)';
|
|
304
|
-
let truncated = msg.slice(0, limit - suffix.length);
|
|
305
|
-
// Strip any incomplete HTML tag at the truncation point
|
|
306
|
-
const lastOpen = truncated.lastIndexOf('<');
|
|
307
|
-
if (lastOpen !== -1 && lastOpen > truncated.lastIndexOf('>')) {
|
|
308
|
-
truncated = truncated.slice(0, lastOpen);
|
|
309
|
-
}
|
|
310
|
-
return truncated + suffix;
|
|
311
|
-
}
|
package/src/lib/types.ts
DELETED
|
@@ -1,172 +0,0 @@
|
|
|
1
|
-
import { Type, type Static } from '@sinclair/typebox';
|
|
2
|
-
|
|
3
|
-
// ── Constants ──────────────────────────────────────────────────────────
|
|
4
|
-
|
|
5
|
-
export const CURRENT_REGISTRY_VERSION = 1;
|
|
6
|
-
export const CAPSULE_VERSION = 1;
|
|
7
|
-
export const MAX_EXTRAS_BYTES = 10_240;
|
|
8
|
-
export const MAX_POST_ERROR_LENGTH = 500;
|
|
9
|
-
export const MAX_TOPICS_DEFAULT = 100;
|
|
10
|
-
export const DOCTOR_ALL_COOLDOWN_MS = 60 * 60 * 1000; // 1 hour
|
|
11
|
-
export const DOCTOR_PER_TOPIC_CAP_MS = 24 * 60 * 60 * 1000; // 24 hours
|
|
12
|
-
export const INACTIVE_AFTER_DAYS = 7;
|
|
13
|
-
export const SPAM_THRESHOLD = 3;
|
|
14
|
-
|
|
15
|
-
// ── Enums ──────────────────────────────────────────────────────────────
|
|
16
|
-
|
|
17
|
-
export const TopicType = {
|
|
18
|
-
Coding: 'coding',
|
|
19
|
-
Research: 'research',
|
|
20
|
-
Marketing: 'marketing',
|
|
21
|
-
Custom: 'custom',
|
|
22
|
-
} as const;
|
|
23
|
-
|
|
24
|
-
export type TopicType = (typeof TopicType)[keyof typeof TopicType];
|
|
25
|
-
|
|
26
|
-
export const TopicStatus = {
|
|
27
|
-
Active: 'active',
|
|
28
|
-
Snoozed: 'snoozed',
|
|
29
|
-
Archived: 'archived',
|
|
30
|
-
} as const;
|
|
31
|
-
|
|
32
|
-
export type TopicStatus = (typeof TopicStatus)[keyof typeof TopicStatus];
|
|
33
|
-
|
|
34
|
-
export const Severity = {
|
|
35
|
-
ERROR: 'ERROR',
|
|
36
|
-
WARN: 'WARN',
|
|
37
|
-
INFO: 'INFO',
|
|
38
|
-
} as const;
|
|
39
|
-
|
|
40
|
-
export type Severity = (typeof Severity)[keyof typeof Severity];
|
|
41
|
-
|
|
42
|
-
// ── Typebox Schemas ────────────────────────────────────────────────────
|
|
43
|
-
|
|
44
|
-
export const TopicTypeSchema = Type.Union([
|
|
45
|
-
Type.Literal('coding'),
|
|
46
|
-
Type.Literal('research'),
|
|
47
|
-
Type.Literal('marketing'),
|
|
48
|
-
Type.Literal('custom'),
|
|
49
|
-
]);
|
|
50
|
-
|
|
51
|
-
export const TopicStatusSchema = Type.Union([
|
|
52
|
-
Type.Literal('active'),
|
|
53
|
-
Type.Literal('snoozed'),
|
|
54
|
-
Type.Literal('archived'),
|
|
55
|
-
]);
|
|
56
|
-
|
|
57
|
-
export const TopicEntrySchema = Type.Object({
|
|
58
|
-
groupId: Type.String({ pattern: '^-?\\d+$' }),
|
|
59
|
-
threadId: Type.String({ pattern: '^\\d+$' }),
|
|
60
|
-
slug: Type.String({ pattern: '^[a-z][a-z0-9-]{0,49}$' }),
|
|
61
|
-
type: TopicTypeSchema,
|
|
62
|
-
status: TopicStatusSchema,
|
|
63
|
-
capsuleVersion: Type.Integer({ minimum: 1 }),
|
|
64
|
-
lastMessageAt: Type.Union([Type.String(), Type.Null()]),
|
|
65
|
-
lastDoctorReportAt: Type.Union([Type.String(), Type.Null()]),
|
|
66
|
-
lastDoctorRunAt: Type.Union([Type.String(), Type.Null()]),
|
|
67
|
-
snoozeUntil: Type.Union([Type.String(), Type.Null()]),
|
|
68
|
-
ignoreChecks: Type.Array(Type.String()),
|
|
69
|
-
consecutiveSilentDoctors: Type.Integer({ minimum: 0 }),
|
|
70
|
-
lastPostError: Type.Union([Type.String({ maxLength: MAX_POST_ERROR_LENGTH }), Type.Null()]),
|
|
71
|
-
extras: Type.Record(Type.String(), Type.Unknown()),
|
|
72
|
-
});
|
|
73
|
-
|
|
74
|
-
export type TopicEntry = Static<typeof TopicEntrySchema>;
|
|
75
|
-
|
|
76
|
-
export const RegistrySchema = Type.Object({
|
|
77
|
-
version: Type.Integer({ minimum: 1 }),
|
|
78
|
-
topicManagerAdmins: Type.Array(Type.String()),
|
|
79
|
-
callbackSecret: Type.String(),
|
|
80
|
-
lastDoctorAllRunAt: Type.Union([Type.String(), Type.Null()]),
|
|
81
|
-
maxTopics: Type.Integer({ minimum: 1 }),
|
|
82
|
-
topics: Type.Record(Type.String(), TopicEntrySchema),
|
|
83
|
-
});
|
|
84
|
-
|
|
85
|
-
export type Registry = Static<typeof RegistrySchema>;
|
|
86
|
-
|
|
87
|
-
// ── Doctor Check Result ────────────────────────────────────────────────
|
|
88
|
-
|
|
89
|
-
export interface DoctorCheckResult {
|
|
90
|
-
severity: Severity;
|
|
91
|
-
checkId: string;
|
|
92
|
-
message: string;
|
|
93
|
-
fixable: boolean;
|
|
94
|
-
}
|
|
95
|
-
|
|
96
|
-
// ── Overlay mappings ───────────────────────────────────────────────────
|
|
97
|
-
|
|
98
|
-
export const OVERLAY_FILES: Record<TopicType, string[]> = {
|
|
99
|
-
coding: ['ARCHITECTURE.md', 'DEPLOY.md'],
|
|
100
|
-
research: ['SOURCES.md', 'FINDINGS.md'],
|
|
101
|
-
marketing: ['CAMPAIGNS.md', 'METRICS.md'],
|
|
102
|
-
custom: [],
|
|
103
|
-
};
|
|
104
|
-
|
|
105
|
-
export const BASE_FILES = [
|
|
106
|
-
'README.md',
|
|
107
|
-
'STATUS.md',
|
|
108
|
-
'TODO.md',
|
|
109
|
-
'COMMANDS.md',
|
|
110
|
-
'LINKS.md',
|
|
111
|
-
'CRON.md',
|
|
112
|
-
'NOTES.md',
|
|
113
|
-
] as const;
|
|
114
|
-
|
|
115
|
-
// ── Audit ──────────────────────────────────────────────────────────────
|
|
116
|
-
|
|
117
|
-
export interface AuditEntry {
|
|
118
|
-
ts: string;
|
|
119
|
-
userId: string;
|
|
120
|
-
cmd: string;
|
|
121
|
-
slug: string;
|
|
122
|
-
detail: string;
|
|
123
|
-
}
|
|
124
|
-
|
|
125
|
-
// ── Inline keyboard types ──────────────────────────────────────────────
|
|
126
|
-
|
|
127
|
-
export interface InlineKeyboardButton {
|
|
128
|
-
text: string;
|
|
129
|
-
callback_data: string;
|
|
130
|
-
}
|
|
131
|
-
|
|
132
|
-
export interface InlineKeyboardMarkup {
|
|
133
|
-
inline_keyboard: InlineKeyboardButton[][];
|
|
134
|
-
}
|
|
135
|
-
|
|
136
|
-
// ── Shared interfaces ─────────────────────────────────────────────────
|
|
137
|
-
|
|
138
|
-
export interface RpcInterface {
|
|
139
|
-
call(method: string, params: Record<string, unknown>): Promise<Record<string, unknown>>;
|
|
140
|
-
}
|
|
141
|
-
|
|
142
|
-
export interface Logger {
|
|
143
|
-
info(msg: string): void;
|
|
144
|
-
warn(msg: string): void;
|
|
145
|
-
error(msg: string): void;
|
|
146
|
-
}
|
|
147
|
-
|
|
148
|
-
// ── Command types ─────────────────────────────────────────────────────
|
|
149
|
-
|
|
150
|
-
export interface CommandContext {
|
|
151
|
-
workspaceDir: string;
|
|
152
|
-
configDir: string;
|
|
153
|
-
rpc?: RpcInterface | null;
|
|
154
|
-
logger: Logger;
|
|
155
|
-
groupId?: string;
|
|
156
|
-
threadId?: string;
|
|
157
|
-
userId?: string;
|
|
158
|
-
messageContext?: Record<string, unknown>;
|
|
159
|
-
}
|
|
160
|
-
|
|
161
|
-
export interface CommandResult {
|
|
162
|
-
text: string;
|
|
163
|
-
parseMode?: 'HTML';
|
|
164
|
-
inlineKeyboard?: InlineKeyboardMarkup;
|
|
165
|
-
pin?: boolean;
|
|
166
|
-
}
|
|
167
|
-
|
|
168
|
-
// ── Helper to build a topic map key ────────────────────────────────────
|
|
169
|
-
|
|
170
|
-
export function topicKey(groupId: string, threadId: string): string {
|
|
171
|
-
return `${groupId}:${threadId}`;
|
|
172
|
-
}
|