@wrongstack/telegram 0.284.0 → 0.285.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/dist/bot.d.ts ADDED
@@ -0,0 +1,192 @@
1
+ import type { Logger } from '@wrongstack/core';
2
+ import type { PollLock } from './poll-lock.js';
3
+ interface TgUser {
4
+ id: number;
5
+ is_bot: boolean;
6
+ first_name: string;
7
+ username?: string | undefined;
8
+ }
9
+ interface TgChat {
10
+ id: number;
11
+ type: 'private' | 'group' | 'supergroup' | 'channel';
12
+ title?: string | undefined;
13
+ username?: string | undefined;
14
+ }
15
+ interface TgMessage {
16
+ message_id: number;
17
+ from?: TgUser | undefined;
18
+ chat: TgChat;
19
+ date: number;
20
+ text?: string | undefined;
21
+ }
22
+ interface TgResponse<T> {
23
+ ok: boolean;
24
+ result?: T | undefined;
25
+ description?: string | undefined;
26
+ error_code?: number | undefined;
27
+ }
28
+ export interface TelegramIncomingMessage {
29
+ messageId: number;
30
+ chatId: number;
31
+ chatType: string;
32
+ userId?: number | undefined;
33
+ userName?: string | undefined;
34
+ text: string;
35
+ timestamp: number;
36
+ }
37
+ export interface TelegramBotOptions {
38
+ token: string;
39
+ pollIntervalSec: number;
40
+ allowedUsers: Set<string>;
41
+ allowedChats: Set<string>;
42
+ /** Max messages to buffer for the agent to read. Default: 50. */
43
+ bufferSize: number;
44
+ log: Logger;
45
+ /** Called for each incoming message that passes allowlist checks. */
46
+ onMessage(msg: TelegramIncomingMessage): void;
47
+ /**
48
+ * Optional path to a file that stores the polling offset. When provided,
49
+ * the offset is persisted on every successful poll and restored on startup,
50
+ * preventing message replay after crashes or restarts.
51
+ */
52
+ offsetStoragePath?: string | undefined;
53
+ /**
54
+ * Optional cross-process single-poller lock. Telegram allows one
55
+ * `getUpdates` consumer per token; when another wstack instance holds the
56
+ * lock, this bot stands by (no polling) and takes over once the holder
57
+ * stops or its heartbeat goes stale.
58
+ */
59
+ lock?: PollLock | undefined;
60
+ /** How often a standby instance retries acquiring the lock. Default: 15s. */
61
+ standbyRetryMs?: number | undefined;
62
+ }
63
+ export declare class TelegramBot {
64
+ private readonly baseUrl;
65
+ /** Base URL with token redacted, safe to use in log calls. */
66
+ private readonly safeBaseUrl;
67
+ private readonly pollIntervalMs;
68
+ private readonly allowedUsers;
69
+ private readonly allowedChats;
70
+ private readonly log;
71
+ private readonly onMessage;
72
+ private readonly controller;
73
+ private pollTimer;
74
+ private pollActive;
75
+ private offset;
76
+ /**
77
+ * Consecutive HTTP 409 ("another getUpdates in flight") responses. Two
78
+ * wstack instances polling the same bot token used to fight at full poll
79
+ * speed forever, erroring on every cycle. After CONFLICT_BACKOFF_AFTER
80
+ * consecutive conflicts this instance backs off to a slow poll and warns
81
+ * once; any successful poll resets to the normal cadence.
82
+ */
83
+ private conflictStreak;
84
+ private static readonly CONFLICT_BACKOFF_AFTER;
85
+ private static readonly CONFLICT_POLL_MS;
86
+ private _startedAt;
87
+ /** If set, the offset is persisted here after each successful poll. */
88
+ private readonly offsetStoragePath?;
89
+ /** Single-poller election across wstack instances sharing this token. */
90
+ private readonly lock?;
91
+ private readonly standbyRetryMs;
92
+ private standbyTimer;
93
+ private standbyAnnounced;
94
+ private readonly bufferMax;
95
+ private readonly buffer;
96
+ private readonly callbackWaiters;
97
+ constructor(opts: TelegramBotOptions);
98
+ /** Start polling for updates. Idempotent. */
99
+ start(): void;
100
+ /** Stop polling and cancel all in-flight requests. */
101
+ stop(): void;
102
+ /** True when the bot is started but waiting for the poll lock. */
103
+ get standby(): boolean;
104
+ /**
105
+ * Acquire the poll lock (when configured) and start the poll loop, or
106
+ * stand by and retry until the current holder releases it.
107
+ */
108
+ private acquireAndPoll;
109
+ /** The lock was stolen while we held it — pause polling and stand by. */
110
+ private handleLockLost;
111
+ get startedAt(): number | null;
112
+ get running(): boolean;
113
+ /** Return buffered messages, newest first. Optionally filter by chat. */
114
+ getMessages(opts?: {
115
+ chatId?: string | number | undefined;
116
+ limit?: number | undefined;
117
+ }): TelegramIncomingMessage[];
118
+ /** Drop messages older than the given message ID from the buffer. */
119
+ acknowledge(lastMessageId: number): number;
120
+ get bufferCount(): number;
121
+ sendMessage(chatId: string | number, text: string): Promise<TgResponse<TgMessage>>;
122
+ /**
123
+ * Send a message that has up to one row of inline buttons (Telegram's
124
+ * `inline_keyboard`). Used by `telegram_approve` to present a
125
+ * yes/no prompt. The keyboard payload is opaque to the bot — callers
126
+ * pass already-encoded `callback_data` strings (≤ 64 bytes each).
127
+ */
128
+ sendMessageWithKeyboard(chatId: string | number, text: string, buttons: Array<{
129
+ text: string;
130
+ callback_data: string;
131
+ }>): Promise<TgResponse<TgMessage>>;
132
+ health(): Promise<{
133
+ ok: boolean;
134
+ username?: string | undefined;
135
+ error?: string | undefined;
136
+ }>;
137
+ private schedulePoll;
138
+ private poll;
139
+ private processMessage;
140
+ /**
141
+ * Handle an inbound `callback_query` update: route it to a registered
142
+ * waiter (if any), and acknowledge it via `answerCallbackQuery` so the
143
+ * client stops spinning. Telegram requires the answer within 10 s.
144
+ */
145
+ /**
146
+ * Resolve any pending waiter for `key` with a `{ approved: false, fromUser }`
147
+ * value, regardless of why the callback was rejected (allowlist, shutdown,
148
+ * etc.). Returns true if a waiter was found and resolved, false otherwise.
149
+ * This helper centralizes the race-safe `delete → resolve` pattern in one
150
+ * place so the deny and shutdown paths don't drift out of sync.
151
+ */
152
+ private rejectWaiter;
153
+ private dispatchCallback;
154
+ /**
155
+ * POST /answerCallbackQuery for a callback. Best-effort: failures are
156
+ * logged at debug and swallowed — the caller's resolve() must not depend
157
+ * on the ack reaching Telegram (the user may get a "loading" spinner if
158
+ * it fails, but the agent's approval flow continues normally).
159
+ */
160
+ private answerCallback;
161
+ /**
162
+ * Register a waiter for a callback_query whose `data` field equals `key`.
163
+ * Resolves with `{ approved, fromUser }` when a matching press arrives, or
164
+ * with `{ approved: false, fromUser: 'timeout' }` after `timeoutMs`.
165
+ *
166
+ * Callers are responsible for not registering the same key twice — a
167
+ * second `awaitCallback` for an in-flight key is undefined.
168
+ */
169
+ awaitCallback(key: string, timeoutMs: number): Promise<{
170
+ approved: boolean;
171
+ fromUser: string;
172
+ }>;
173
+ private loadOffset;
174
+ private saveOffset;
175
+ }
176
+ /**
177
+ * Truncate text to fit Telegram's 4096-char message limit.
178
+ * Preserves semantic boundaries in this priority order:
179
+ * 1. Paragraph break (double newline)
180
+ * 2. Sentence break (. ! ? followed by space/newline)
181
+ * 3. Word break (space)
182
+ * 4. Hard cut with ellipsis
183
+ *
184
+ * When a clean boundary is found, appends "…" to signal intentional truncation.
185
+ */
186
+ export declare function truncateForTelegram(text: string, maxLen?: number): string;
187
+ /**
188
+ * Escape HTML special chars for Telegram's HTML parse mode.
189
+ */
190
+ export declare function escapeHtml(text: string): string;
191
+ export {};
192
+ //# sourceMappingURL=bot.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"bot.d.ts","sourceRoot":"","sources":["../src/bot.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,108 @@
1
+ import type { PluginAPI } from '@wrongstack/core';
2
+ export declare const PLUGIN_NAME = "telegram";
3
+ export interface TelegramPluginConfig {
4
+ /** Telegram Bot API token (from @BotFather). */
5
+ botToken: string;
6
+ /**
7
+ * Default chat ID for outgoing notifications.
8
+ * The agent's `telegram_send` tool can override per-call.
9
+ */
10
+ notifyChatId?: string | number | undefined;
11
+ /**
12
+ * List of user/chat IDs allowed to interact with the bot.
13
+ * Empty = allow all. Recommended to set in production.
14
+ */
15
+ allowedUsers?: Array<string | number> | undefined;
16
+ /**
17
+ * List of group/chat IDs the bot is allowed to read from.
18
+ * Empty = allow all. Narrow this to prevent noise.
19
+ */
20
+ allowedChats?: Array<string | number> | undefined;
21
+ /** Polling interval in seconds (default: 2). */
22
+ pollIntervalSec?: number | undefined;
23
+ /** Notify on Telegram when a session ends. */
24
+ notifyOnSessionEnd?: boolean | undefined;
25
+ /** Notify when a tool runs longer than this threshold (ms). Set 0 to disable. */
26
+ longToolThresholdMs?: number | undefined;
27
+ /** Notify (humanized) when a `delegate` subagent finishes. Default: true. */
28
+ notifyOnDelegate?: boolean | undefined;
29
+ /** Maximum message length for Telegram (Telegram caps at 4096). */
30
+ maxMessageLength?: number | undefined;
31
+ /**
32
+ * Path to a file that stores the Telegram polling offset. When set,
33
+ * the offset is persisted on every successful poll and restored on startup,
34
+ * preventing message replay after crashes or restarts.
35
+ * The directory must already exist and be writable.
36
+ */
37
+ offsetStoragePath?: string | undefined;
38
+ /**
39
+ * Elect a single poller per bot token across wstack instances (default:
40
+ * true). Telegram allows one `getUpdates` consumer per token; without this,
41
+ * two instances sharing a token fight and get HTTP 409 on every poll.
42
+ * Extra instances stand by and take over when the active poller stops.
43
+ * Set false only if this is guaranteed to be the sole consumer.
44
+ */
45
+ singleInstanceLock?: boolean | undefined;
46
+ }
47
+ export declare const DEFAULT_CONFIG: Required<Omit<TelegramPluginConfig, 'botToken' | 'notifyChatId' | 'offsetStoragePath'>>;
48
+ export declare const telegramConfigSchema: {
49
+ type: string;
50
+ properties: {
51
+ botToken: {
52
+ type: string;
53
+ description: string;
54
+ };
55
+ notifyChatId: {
56
+ oneOf: {
57
+ type: string;
58
+ }[];
59
+ description: string;
60
+ };
61
+ allowedUsers: {
62
+ type: string;
63
+ items: {
64
+ oneOf: {
65
+ type: string;
66
+ }[];
67
+ };
68
+ description: string;
69
+ };
70
+ allowedChats: {
71
+ type: string;
72
+ items: {
73
+ oneOf: {
74
+ type: string;
75
+ }[];
76
+ };
77
+ description: string;
78
+ };
79
+ pollIntervalSec: {
80
+ type: string;
81
+ minimum: number;
82
+ maximum: number;
83
+ description: string;
84
+ };
85
+ notifyOnSessionEnd: {
86
+ type: string;
87
+ };
88
+ longToolThresholdMs: {
89
+ type: string;
90
+ minimum: number;
91
+ };
92
+ notifyOnDelegate: {
93
+ type: string;
94
+ };
95
+ maxMessageLength: {
96
+ type: string;
97
+ minimum: number;
98
+ maximum: number;
99
+ };
100
+ singleInstanceLock: {
101
+ type: string;
102
+ description: string;
103
+ };
104
+ };
105
+ required: string[];
106
+ };
107
+ export declare function readTelegramConfig(api: Pick<PluginAPI, 'config'>): Required<Omit<TelegramPluginConfig, 'notifyChatId' | 'offsetStoragePath'>> & Pick<TelegramPluginConfig, 'notifyChatId' | 'offsetStoragePath'>;
108
+ //# sourceMappingURL=config.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"config.d.ts","sourceRoot":"","sources":["../src/config.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,SAAS,EAAE,MAAM,kBAAkB,CAAC;AAElD,eAAO,MAAM,WAAW,aAAa,CAAC;AAEtC,MAAM,WAAW,oBAAoB;IACnC,gDAAgD;IAChD,QAAQ,EAAE,MAAM,CAAC;IACjB;;;OAGG;IACH,YAAY,CAAC,EAAE,MAAM,GAAG,MAAM,GAAG,SAAS,CAAC;IAC3C;;;OAGG;IACH,YAAY,CAAC,EAAE,KAAK,CAAC,MAAM,GAAG,MAAM,CAAC,GAAG,SAAS,CAAC;IAClD;;;OAGG;IACH,YAAY,CAAC,EAAE,KAAK,CAAC,MAAM,GAAG,MAAM,CAAC,GAAG,SAAS,CAAC;IAClD,gDAAgD;IAChD,eAAe,CAAC,EAAE,MAAM,GAAG,SAAS,CAAC;IACrC,8CAA8C;IAC9C,kBAAkB,CAAC,EAAE,OAAO,GAAG,SAAS,CAAC;IACzC,iFAAiF;IACjF,mBAAmB,CAAC,EAAE,MAAM,GAAG,SAAS,CAAC;IACzC,6EAA6E;IAC7E,gBAAgB,CAAC,EAAE,OAAO,GAAG,SAAS,CAAC;IACvC,mEAAmE;IACnE,gBAAgB,CAAC,EAAE,MAAM,GAAG,SAAS,CAAC;IACtC;;;;;OAKG;IACH,iBAAiB,CAAC,EAAE,MAAM,GAAG,SAAS,CAAC;IACvC;;;;;;OAMG;IACH,kBAAkB,CAAC,EAAE,OAAO,GAAG,SAAS,CAAC;CAC1C;AAED,eAAO,MAAM,cAAc,EAAE,QAAQ,CAAC,IAAI,CAAC,oBAAoB,EAAE,UAAU,GAAG,cAAc,GAAG,mBAAmB,CAAC,CASlH,CAAC;AAEF,eAAO,MAAM,oBAAoB;;;;YAGjB,IAAI;YAAY,WAAW;;;;;;;;;;;;;;;;;;;;;;;;;;;YAgBrC,IAAI;YACJ,OAAO;YACP,OAAO;YACP,WAAW;;;YAES,IAAI;;;YACH,IAAI;YAAa,OAAO;;;YAC3B,IAAI;;;YACJ,IAAI;YAAa,OAAO;YAAO,OAAO;;;YAExD,IAAI;YACJ,WAAW;;;;CAIhB,CAAC;AAEF,wBAAgB,kBAAkB,CAChC,GAAG,EAAE,IAAI,CAAC,SAAS,EAAE,QAAQ,CAAC,GAC7B,QAAQ,CAAC,IAAI,CAAC,oBAAoB,EAAE,cAAc,GAAG,mBAAmB,CAAC,CAAC,GAC3E,IAAI,CAAC,oBAAoB,EAAE,cAAc,GAAG,mBAAmB,CAAC,CAgBjE"}
@@ -0,0 +1,70 @@
1
+ /** Subset of the core `delegate.completed` event payload we render. */
2
+ export interface DelegateCompletedLike {
3
+ target: string;
4
+ task: string;
5
+ ok: boolean;
6
+ status?: string | undefined;
7
+ summary: string;
8
+ durationMs: number;
9
+ iterations: number;
10
+ toolCalls: number;
11
+ costUsd?: number | undefined;
12
+ subagentId?: string | undefined;
13
+ }
14
+ /** Subset of core `tool.executed` event payload. */
15
+ export interface ToolExecutedLike {
16
+ name: string;
17
+ ok: boolean;
18
+ durationMs: number;
19
+ /** Raw tool output — only the first 300 chars are rendered. */
20
+ output?: string | undefined;
21
+ }
22
+ /** Subset of core `session.ended` event payload (from Usage). */
23
+ export interface SessionEndedLike {
24
+ id: string;
25
+ inputTokens: number;
26
+ outputTokens: number;
27
+ cacheRead?: number | undefined;
28
+ cacheWrite?: number | undefined;
29
+ }
30
+ /** Compact human duration: `42s`, `3m`, `1.5h`. */
31
+ export declare function fmtDuration(ms: number): string;
32
+ /**
33
+ * Format a numeric count of tokens for human readability.
34
+ * Uses comma-separated thousands: 1,234, 56,789.
35
+ */
36
+ export declare function fmtTokens(n: number): string;
37
+ /**
38
+ * Try to render a tool's output as a short human-readable snippet.
39
+ * Strips JSON braces/quoting, redacts secrets, limits to ~300 chars,
40
+ * preserves first/last lines.
41
+ */
42
+ export declare function fmtToolOutput(raw: string | undefined): string;
43
+ /**
44
+ * Render a finished delegation as a readable Telegram message.
45
+ *
46
+ * Example:
47
+ * ✅ Delegate → bug-hunter · success
48
+ * Found 3 null-deref risks in auth.ts and patched the worst one…
49
+ * ⏱ 3m · 4 iter · 37 tools · 💲0.0820
50
+ */
51
+ export declare function formatDelegateCompleted(e: DelegateCompletedLike): string;
52
+ /**
53
+ * Render a long-running tool execution notification.
54
+ *
55
+ * Example:
56
+ * ✅ bash completed in 45.2s
57
+ * pnpm test — 12 suites, 47 tests passed
58
+ * …
59
+ */
60
+ export declare function formatToolExecuted(e: ToolExecutedLike): string;
61
+ /**
62
+ * Render a session-end notification.
63
+ *
64
+ * Example:
65
+ * 🏁 Session sess_abcd ended
66
+ * ⬇ 8,234 in · ⬆ 3,456 out · 11,690 total
67
+ * Cache: 1,200 read · 800 written
68
+ */
69
+ export declare function formatSessionEnded(e: SessionEndedLike): string;
70
+ //# sourceMappingURL=format.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"format.d.ts","sourceRoot":"","sources":["../src/format.ts"],"names":[],"mappings":"AAuBA,uEAAuE;AACvE,MAAM,WAAW,qBAAqB;IACpC,MAAM,EAAE,MAAM,CAAC;IACf,IAAI,EAAE,MAAM,CAAC;IACb,EAAE,EAAE,OAAO,CAAC;IACZ,MAAM,CAAC,EAAE,MAAM,GAAG,SAAS,CAAC;IAC5B,OAAO,EAAE,MAAM,CAAC;IAChB,UAAU,EAAE,MAAM,CAAC;IACnB,UAAU,EAAE,MAAM,CAAC;IACnB,SAAS,EAAE,MAAM,CAAC;IAClB,OAAO,CAAC,EAAE,MAAM,GAAG,SAAS,CAAC;IAC7B,UAAU,CAAC,EAAE,MAAM,GAAG,SAAS,CAAC;CACjC;AAED,oDAAoD;AACpD,MAAM,WAAW,gBAAgB;IAC/B,IAAI,EAAE,MAAM,CAAC;IACb,EAAE,EAAE,OAAO,CAAC;IACZ,UAAU,EAAE,MAAM,CAAC;IACnB,+DAA+D;IAC/D,MAAM,CAAC,EAAE,MAAM,GAAG,SAAS,CAAC;CAC7B;AAED,iEAAiE;AACjE,MAAM,WAAW,gBAAgB;IAC/B,EAAE,EAAE,MAAM,CAAC;IACX,WAAW,EAAE,MAAM,CAAC;IACpB,YAAY,EAAE,MAAM,CAAC;IACrB,SAAS,CAAC,EAAE,MAAM,GAAG,SAAS,CAAC;IAC/B,UAAU,CAAC,EAAE,MAAM,GAAG,SAAS,CAAC;CACjC;AAMD,mDAAmD;AACnD,wBAAgB,WAAW,CAAC,EAAE,EAAE,MAAM,GAAG,MAAM,CAI9C;AAED;;;GAGG;AACH,wBAAgB,SAAS,CAAC,CAAC,EAAE,MAAM,GAAG,MAAM,CAE3C;AAED;;;;GAIG;AACH,wBAAgB,aAAa,CAAC,GAAG,EAAE,MAAM,GAAG,SAAS,GAAG,MAAM,CAoB7D;AAMD;;;;;;;GAOG;AACH,wBAAgB,uBAAuB,CAAC,CAAC,EAAE,qBAAqB,GAAG,MAAM,CAqBxE;AAED;;;;;;;GAOG;AACH,wBAAgB,kBAAkB,CAAC,CAAC,EAAE,gBAAgB,GAAG,MAAM,CAS9D;AAED;;;;;;;GAOG;AACH,wBAAgB,kBAAkB,CAAC,CAAC,EAAE,gBAAgB,GAAG,MAAM,CAkB9D"}
package/dist/index.d.ts CHANGED
@@ -1,60 +1,6 @@
1
- import { Plugin } from '@wrongstack/core';
2
-
3
- interface TelegramIncomingMessage {
4
- messageId: number;
5
- chatId: number;
6
- chatType: string;
7
- userId?: number | undefined;
8
- userName?: string | undefined;
9
- text: string;
10
- timestamp: number;
11
- }
12
-
13
- interface TelegramPluginConfig {
14
- /** Telegram Bot API token (from @BotFather). */
15
- botToken: string;
16
- /**
17
- * Default chat ID for outgoing notifications.
18
- * The agent's `telegram_send` tool can override per-call.
19
- */
20
- notifyChatId?: string | number | undefined;
21
- /**
22
- * List of user/chat IDs allowed to interact with the bot.
23
- * Empty = allow all. Recommended to set in production.
24
- */
25
- allowedUsers?: Array<string | number> | undefined;
26
- /**
27
- * List of group/chat IDs the bot is allowed to read from.
28
- * Empty = allow all. Narrow this to prevent noise.
29
- */
30
- allowedChats?: Array<string | number> | undefined;
31
- /** Polling interval in seconds (default: 2). */
32
- pollIntervalSec?: number | undefined;
33
- /** Notify on Telegram when a session ends. */
34
- notifyOnSessionEnd?: boolean | undefined;
35
- /** Notify when a tool runs longer than this threshold (ms). Set 0 to disable. */
36
- longToolThresholdMs?: number | undefined;
37
- /** Notify (humanized) when a `delegate` subagent finishes. Default: true. */
38
- notifyOnDelegate?: boolean | undefined;
39
- /** Maximum message length for Telegram (Telegram caps at 4096). */
40
- maxMessageLength?: number | undefined;
41
- /**
42
- * Path to a file that stores the Telegram polling offset. When set,
43
- * the offset is persisted on every successful poll and restored on startup,
44
- * preventing message replay after crashes or restarts.
45
- * The directory must already exist and be writable.
46
- */
47
- offsetStoragePath?: string | undefined;
48
- /**
49
- * Elect a single poller per bot token across wstack instances (default:
50
- * true). Telegram allows one `getUpdates` consumer per token; without this,
51
- * two instances sharing a token fight and get HTTP 409 on every poll.
52
- * Extra instances stand by and take over when the active poller stops.
53
- * Set false only if this is guaranteed to be the sole consumer.
54
- */
55
- singleInstanceLock?: boolean | undefined;
56
- }
57
-
1
+ import type { Plugin } from '@wrongstack/core';
58
2
  declare const plugin: Plugin;
59
-
60
- export { type TelegramIncomingMessage, type TelegramPluginConfig, plugin as default };
3
+ export default plugin;
4
+ export type { TelegramIncomingMessage } from './bot.js';
5
+ export type { TelegramPluginConfig } from './config.js';
6
+ //# sourceMappingURL=index.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AACA,OAAO,KAAK,EAAU,MAAM,EAAE,MAAM,kBAAkB,CAAC;AA2DvD,QAAA,MAAM,MAAM,EAAE,MAkPb,CAAC;eAEa,MAAM;AAGrB,YAAY,EAAE,uBAAuB,EAAE,MAAM,UAAU,CAAC;AACxD,YAAY,EAAE,oBAAoB,EAAE,MAAM,aAAa,CAAC"}
package/dist/index.js CHANGED
@@ -1,10 +1,8 @@
1
- import { expectDefined } from '@wrongstack/core';
2
- import { wstackGlobalRoot, sleep } from '@wrongstack/core/utils';
3
- import { randomUUID, createHash } from 'crypto';
4
- import { mkdirSync, unlinkSync, writeFileSync, renameSync, readFileSync } from 'fs';
5
- import { dirname, join } from 'path';
6
-
7
1
  // src/index.ts
2
+ import { expectDefined as expectDefined2 } from "@wrongstack/core";
3
+
4
+ // src/bot.ts
5
+ import { sleep } from "@wrongstack/core/utils";
8
6
  function redactToken(url, token) {
9
7
  return url.replace(token, "[REDACTED]");
10
8
  }
@@ -436,7 +434,7 @@ var TelegramBot = class _TelegramBot {
436
434
  async loadOffset() {
437
435
  if (!this.offsetStoragePath) return;
438
436
  try {
439
- const { readFileSync: readFileSync2 } = await import('fs');
437
+ const { readFileSync: readFileSync2 } = await import("node:fs");
440
438
  const raw = readFileSync2(this.offsetStoragePath, "utf8").trim();
441
439
  const n = Number.parseInt(raw, 10);
442
440
  if (Number.isFinite(n) && n >= 0) {
@@ -449,7 +447,7 @@ var TelegramBot = class _TelegramBot {
449
447
  async saveOffset() {
450
448
  if (!this.offsetStoragePath) return;
451
449
  try {
452
- const { writeFileSync: writeFileSync2 } = await import('fs');
450
+ const { writeFileSync: writeFileSync2 } = await import("node:fs");
453
451
  writeFileSync2(this.offsetStoragePath, String(this.offset), "utf8");
454
452
  } catch (err) {
455
453
  this.log.debug(`Failed to persist Telegram offset: ${err}`);
@@ -665,6 +663,12 @@ function formatSessionEnded(e) {
665
663
  }
666
664
  return lines.join("\n");
667
665
  }
666
+
667
+ // src/poll-lock.ts
668
+ import { createHash, randomUUID } from "node:crypto";
669
+ import { mkdirSync, readFileSync, renameSync, unlinkSync, writeFileSync } from "node:fs";
670
+ import { dirname, join } from "node:path";
671
+ import { wstackGlobalRoot } from "@wrongstack/core/utils";
668
672
  function lockPathForToken(token, globalRoot = wstackGlobalRoot()) {
669
673
  const hash = createHash("sha256").update(token).digest("hex").slice(0, 12);
670
674
  return join(globalRoot, "telegram", `poll-${hash}.lock`);
@@ -783,6 +787,9 @@ var PollLock = class {
783
787
  }
784
788
  }
785
789
  };
790
+
791
+ // src/slash-commands/index.ts
792
+ import { expectDefined } from "@wrongstack/core";
786
793
  function tgHealthCommand(bot, cfg) {
787
794
  return {
788
795
  name: "telegram-health",
@@ -980,6 +987,9 @@ function makeTelegramSendTool(opts) {
980
987
  }
981
988
  };
982
989
  }
990
+
991
+ // src/tools/telegram-approve.ts
992
+ import { randomUUID as randomUUID2 } from "node:crypto";
983
993
  function makeTelegramApproveTool(opts) {
984
994
  return {
985
995
  name: "telegram_approve",
@@ -1023,7 +1033,7 @@ function makeTelegramApproveTool(opts) {
1023
1033
  );
1024
1034
  }
1025
1035
  const timeoutMs = Math.min(Math.max(input.timeout_ms ?? 6e4, 1e3), 6e5);
1026
- const token = randomUUID().slice(0, 16);
1036
+ const token = randomUUID2().slice(0, 16);
1027
1037
  const yesKey = `approve:${token}:yes`;
1028
1038
  const noKey = `approve:${token}:no`;
1029
1039
  const heading = `\u26A0\uFE0F ${input.prompt}`;
@@ -1170,7 +1180,7 @@ var plugin = {
1170
1180
  formatSessionEnded(payload),
1171
1181
  runtimeCfg.maxMessageLength
1172
1182
  );
1173
- void bot.sendMessage(expectDefined(runtimeCfg.notifyChatId), msg).catch((err) => {
1183
+ void bot.sendMessage(expectDefined2(runtimeCfg.notifyChatId), msg).catch((err) => {
1174
1184
  log.debug(`Failed to send session end notification: ${err.message}`);
1175
1185
  });
1176
1186
  })
@@ -1188,7 +1198,7 @@ var plugin = {
1188
1198
  formatToolExecuted(payload),
1189
1199
  runtimeCfg.maxMessageLength
1190
1200
  );
1191
- void bot.sendMessage(expectDefined(runtimeCfg.notifyChatId), msg).catch((err) => {
1201
+ void bot.sendMessage(expectDefined2(runtimeCfg.notifyChatId), msg).catch((err) => {
1192
1202
  log.debug(`Failed to send tool notification: ${err.message}`);
1193
1203
  });
1194
1204
  })
@@ -1200,7 +1210,7 @@ var plugin = {
1200
1210
  formatDelegateCompleted(event),
1201
1211
  runtimeCfg.maxMessageLength
1202
1212
  );
1203
- void bot.sendMessage(expectDefined(runtimeCfg.notifyChatId), msg).catch((err) => {
1213
+ void bot.sendMessage(expectDefined2(runtimeCfg.notifyChatId), msg).catch((err) => {
1204
1214
  log.debug(`Failed to send delegate notification: ${err.message}`);
1205
1215
  });
1206
1216
  })
@@ -1256,8 +1266,8 @@ var plugin = {
1256
1266
  return h;
1257
1267
  }
1258
1268
  };
1259
- var index_default = plugin;
1260
-
1261
- export { index_default as default };
1269
+ var src_default = plugin;
1270
+ export {
1271
+ src_default as default
1272
+ };
1262
1273
  //# sourceMappingURL=index.js.map
1263
- //# sourceMappingURL=index.js.map