clementine-agent 1.0.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/.env.example +44 -0
- package/LICENSE +21 -0
- package/README.md +795 -0
- package/dist/agent/agent-manager.d.ts +69 -0
- package/dist/agent/agent-manager.js +441 -0
- package/dist/agent/assistant.d.ts +225 -0
- package/dist/agent/assistant.js +3888 -0
- package/dist/agent/auto-update.d.ts +32 -0
- package/dist/agent/auto-update.js +186 -0
- package/dist/agent/daily-planner.d.ts +24 -0
- package/dist/agent/daily-planner.js +379 -0
- package/dist/agent/execution-advisor.d.ts +10 -0
- package/dist/agent/execution-advisor.js +272 -0
- package/dist/agent/hooks.d.ts +45 -0
- package/dist/agent/hooks.js +564 -0
- package/dist/agent/insight-engine.d.ts +66 -0
- package/dist/agent/insight-engine.js +225 -0
- package/dist/agent/intent-classifier.d.ts +48 -0
- package/dist/agent/intent-classifier.js +214 -0
- package/dist/agent/link-extractor.d.ts +19 -0
- package/dist/agent/link-extractor.js +90 -0
- package/dist/agent/mcp-bridge.d.ts +62 -0
- package/dist/agent/mcp-bridge.js +435 -0
- package/dist/agent/metacognition.d.ts +66 -0
- package/dist/agent/metacognition.js +221 -0
- package/dist/agent/orchestrator.d.ts +81 -0
- package/dist/agent/orchestrator.js +790 -0
- package/dist/agent/profiles.d.ts +22 -0
- package/dist/agent/profiles.js +91 -0
- package/dist/agent/prompt-cache.d.ts +24 -0
- package/dist/agent/prompt-cache.js +68 -0
- package/dist/agent/prompt-evolver.d.ts +28 -0
- package/dist/agent/prompt-evolver.js +279 -0
- package/dist/agent/role-scaffolds.d.ts +28 -0
- package/dist/agent/role-scaffolds.js +433 -0
- package/dist/agent/safe-restart.d.ts +41 -0
- package/dist/agent/safe-restart.js +150 -0
- package/dist/agent/self-improve.d.ts +66 -0
- package/dist/agent/self-improve.js +1706 -0
- package/dist/agent/session-event-log.d.ts +114 -0
- package/dist/agent/session-event-log.js +233 -0
- package/dist/agent/skill-extractor.d.ts +72 -0
- package/dist/agent/skill-extractor.js +435 -0
- package/dist/agent/source-mods.d.ts +61 -0
- package/dist/agent/source-mods.js +230 -0
- package/dist/agent/source-preflight.d.ts +25 -0
- package/dist/agent/source-preflight.js +100 -0
- package/dist/agent/stall-guard.d.ts +62 -0
- package/dist/agent/stall-guard.js +109 -0
- package/dist/agent/strategic-planner.d.ts +60 -0
- package/dist/agent/strategic-planner.js +352 -0
- package/dist/agent/team-bus.d.ts +89 -0
- package/dist/agent/team-bus.js +556 -0
- package/dist/agent/team-router.d.ts +26 -0
- package/dist/agent/team-router.js +37 -0
- package/dist/agent/tool-loop-detector.d.ts +59 -0
- package/dist/agent/tool-loop-detector.js +242 -0
- package/dist/agent/workflow-runner.d.ts +36 -0
- package/dist/agent/workflow-runner.js +317 -0
- package/dist/agent/workflow-variables.d.ts +16 -0
- package/dist/agent/workflow-variables.js +62 -0
- package/dist/channels/discord-agent-bot.d.ts +101 -0
- package/dist/channels/discord-agent-bot.js +881 -0
- package/dist/channels/discord-bot-manager.d.ts +80 -0
- package/dist/channels/discord-bot-manager.js +262 -0
- package/dist/channels/discord-utils.d.ts +51 -0
- package/dist/channels/discord-utils.js +293 -0
- package/dist/channels/discord.d.ts +12 -0
- package/dist/channels/discord.js +1832 -0
- package/dist/channels/slack-agent-bot.d.ts +73 -0
- package/dist/channels/slack-agent-bot.js +320 -0
- package/dist/channels/slack-bot-manager.d.ts +66 -0
- package/dist/channels/slack-bot-manager.js +236 -0
- package/dist/channels/slack-utils.d.ts +39 -0
- package/dist/channels/slack-utils.js +189 -0
- package/dist/channels/slack.d.ts +11 -0
- package/dist/channels/slack.js +196 -0
- package/dist/channels/telegram.d.ts +10 -0
- package/dist/channels/telegram.js +235 -0
- package/dist/channels/webhook.d.ts +9 -0
- package/dist/channels/webhook.js +78 -0
- package/dist/channels/whatsapp.d.ts +11 -0
- package/dist/channels/whatsapp.js +181 -0
- package/dist/cli/chat.d.ts +14 -0
- package/dist/cli/chat.js +220 -0
- package/dist/cli/cron.d.ts +17 -0
- package/dist/cli/cron.js +552 -0
- package/dist/cli/dashboard.d.ts +15 -0
- package/dist/cli/dashboard.js +17677 -0
- package/dist/cli/index.d.ts +3 -0
- package/dist/cli/index.js +2474 -0
- package/dist/cli/routes/delegations.d.ts +19 -0
- package/dist/cli/routes/delegations.js +154 -0
- package/dist/cli/routes/digest.d.ts +17 -0
- package/dist/cli/routes/digest.js +375 -0
- package/dist/cli/routes/goals.d.ts +14 -0
- package/dist/cli/routes/goals.js +258 -0
- package/dist/cli/routes/workflows.d.ts +18 -0
- package/dist/cli/routes/workflows.js +97 -0
- package/dist/cli/setup.d.ts +8 -0
- package/dist/cli/setup.js +619 -0
- package/dist/cli/tunnel.d.ts +35 -0
- package/dist/cli/tunnel.js +141 -0
- package/dist/config.d.ts +145 -0
- package/dist/config.js +278 -0
- package/dist/events/bus.d.ts +43 -0
- package/dist/events/bus.js +136 -0
- package/dist/gateway/cron-scheduler.d.ts +166 -0
- package/dist/gateway/cron-scheduler.js +1767 -0
- package/dist/gateway/delivery-queue.d.ts +30 -0
- package/dist/gateway/delivery-queue.js +110 -0
- package/dist/gateway/heartbeat-scheduler.d.ts +99 -0
- package/dist/gateway/heartbeat-scheduler.js +1298 -0
- package/dist/gateway/heartbeat.d.ts +3 -0
- package/dist/gateway/heartbeat.js +3 -0
- package/dist/gateway/lanes.d.ts +24 -0
- package/dist/gateway/lanes.js +76 -0
- package/dist/gateway/notifications.d.ts +29 -0
- package/dist/gateway/notifications.js +75 -0
- package/dist/gateway/router.d.ts +210 -0
- package/dist/gateway/router.js +1330 -0
- package/dist/index.d.ts +12 -0
- package/dist/index.js +1015 -0
- package/dist/memory/chunker.d.ts +28 -0
- package/dist/memory/chunker.js +226 -0
- package/dist/memory/consolidation.d.ts +44 -0
- package/dist/memory/consolidation.js +171 -0
- package/dist/memory/context-assembler.d.ts +50 -0
- package/dist/memory/context-assembler.js +149 -0
- package/dist/memory/embeddings.d.ts +38 -0
- package/dist/memory/embeddings.js +180 -0
- package/dist/memory/graph-store.d.ts +66 -0
- package/dist/memory/graph-store.js +613 -0
- package/dist/memory/mmr.d.ts +21 -0
- package/dist/memory/mmr.js +75 -0
- package/dist/memory/search.d.ts +26 -0
- package/dist/memory/search.js +67 -0
- package/dist/memory/store.d.ts +530 -0
- package/dist/memory/store.js +2022 -0
- package/dist/security/integrity.d.ts +24 -0
- package/dist/security/integrity.js +58 -0
- package/dist/security/patterns.d.ts +34 -0
- package/dist/security/patterns.js +110 -0
- package/dist/security/scanner.d.ts +32 -0
- package/dist/security/scanner.js +263 -0
- package/dist/tools/admin-tools.d.ts +12 -0
- package/dist/tools/admin-tools.js +1278 -0
- package/dist/tools/external-tools.d.ts +11 -0
- package/dist/tools/external-tools.js +1327 -0
- package/dist/tools/goal-tools.d.ts +9 -0
- package/dist/tools/goal-tools.js +159 -0
- package/dist/tools/mcp-server.d.ts +13 -0
- package/dist/tools/mcp-server.js +141 -0
- package/dist/tools/memory-tools.d.ts +10 -0
- package/dist/tools/memory-tools.js +568 -0
- package/dist/tools/session-tools.d.ts +6 -0
- package/dist/tools/session-tools.js +146 -0
- package/dist/tools/shared.d.ts +216 -0
- package/dist/tools/shared.js +340 -0
- package/dist/tools/team-tools.d.ts +6 -0
- package/dist/tools/team-tools.js +447 -0
- package/dist/tools/tool-meta.d.ts +34 -0
- package/dist/tools/tool-meta.js +133 -0
- package/dist/tools/vault-tools.d.ts +8 -0
- package/dist/tools/vault-tools.js +457 -0
- package/dist/types.d.ts +716 -0
- package/dist/types.js +16 -0
- package/dist/vault-migrations/0001-add-execution-framework.d.ts +10 -0
- package/dist/vault-migrations/0001-add-execution-framework.js +47 -0
- package/dist/vault-migrations/0002-add-agentic-communication.d.ts +12 -0
- package/dist/vault-migrations/0002-add-agentic-communication.js +79 -0
- package/dist/vault-migrations/0003-update-execution-pipeline-narration.d.ts +11 -0
- package/dist/vault-migrations/0003-update-execution-pipeline-narration.js +73 -0
- package/dist/vault-migrations/helpers.d.ts +14 -0
- package/dist/vault-migrations/helpers.js +44 -0
- package/dist/vault-migrations/runner.d.ts +14 -0
- package/dist/vault-migrations/runner.js +139 -0
- package/dist/vault-migrations/types.d.ts +42 -0
- package/dist/vault-migrations/types.js +9 -0
- package/install.sh +320 -0
- package/package.json +84 -0
- package/scripts/postinstall.js +125 -0
- package/vault/00-System/AGENTS.md +66 -0
- package/vault/00-System/CRON.md +71 -0
- package/vault/00-System/HEARTBEAT.md +58 -0
- package/vault/00-System/MEMORY.md +16 -0
- package/vault/00-System/SOUL.md +96 -0
- package/vault/05-Tasks/TASKS.md +19 -0
- package/vault/06-Templates/_Daily-Template.md +28 -0
- package/vault/06-Templates/_People-Template.md +22 -0
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Clementine TypeScript — Shared Slack utilities.
|
|
3
|
+
*
|
|
4
|
+
* Extracted from slack.ts so agent bot clients can reuse streaming,
|
|
5
|
+
* markdown conversion, and chunked sending without importing the monolith.
|
|
6
|
+
*/
|
|
7
|
+
import type { App } from '@slack/bolt';
|
|
8
|
+
export declare const STREAM_UPDATE_INTERVAL = 1500;
|
|
9
|
+
export declare const SLACK_MSG_LIMIT = 3900;
|
|
10
|
+
export declare function mdToSlack(text: string): string;
|
|
11
|
+
export declare function sendChunkedSlack(client: App['client'], channel: string, text: string, threadTs?: string): Promise<void>;
|
|
12
|
+
export declare class SlackStreamingMessage {
|
|
13
|
+
private client;
|
|
14
|
+
private channel;
|
|
15
|
+
private threadTs?;
|
|
16
|
+
private ts;
|
|
17
|
+
private lastEdit;
|
|
18
|
+
private pendingText;
|
|
19
|
+
private lastFlushedText;
|
|
20
|
+
private isFinal;
|
|
21
|
+
private toolStatus;
|
|
22
|
+
private flushTimer;
|
|
23
|
+
private progressTimer;
|
|
24
|
+
private startTime;
|
|
25
|
+
private toolCallCount;
|
|
26
|
+
private lastTextTime;
|
|
27
|
+
/** The message timestamp (available after start). Used for reaction tracking. */
|
|
28
|
+
get messageTs(): string | null;
|
|
29
|
+
constructor(client: App['client'], channel: string, threadTs?: string);
|
|
30
|
+
start(): Promise<void>;
|
|
31
|
+
/** Update the tool activity status line shown during streaming. */
|
|
32
|
+
setToolStatus(status: string): void;
|
|
33
|
+
update(text: string): Promise<void>;
|
|
34
|
+
finalize(text: string): Promise<void>;
|
|
35
|
+
/** Format elapsed milliseconds as human-readable duration. */
|
|
36
|
+
private formatElapsed;
|
|
37
|
+
private flush;
|
|
38
|
+
}
|
|
39
|
+
//# sourceMappingURL=slack-utils.d.ts.map
|
|
@@ -0,0 +1,189 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Clementine TypeScript — Shared Slack utilities.
|
|
3
|
+
*
|
|
4
|
+
* Extracted from slack.ts so agent bot clients can reuse streaming,
|
|
5
|
+
* markdown conversion, and chunked sending without importing the monolith.
|
|
6
|
+
*/
|
|
7
|
+
export const STREAM_UPDATE_INTERVAL = 1500; // ms
|
|
8
|
+
export const SLACK_MSG_LIMIT = 3900;
|
|
9
|
+
// ── Markdown to Slack mrkdwn ──────────────────────────────────────────
|
|
10
|
+
export function mdToSlack(text) {
|
|
11
|
+
// Convert Markdown bold **text** to Slack bold *text*
|
|
12
|
+
return text.replace(/\*\*(.+?)\*\*/g, '*$1*');
|
|
13
|
+
}
|
|
14
|
+
// ── Chunked sending ───────────────────────────────────────────────────
|
|
15
|
+
export async function sendChunkedSlack(client, channel, text, threadTs) {
|
|
16
|
+
let remaining = text;
|
|
17
|
+
while (remaining) {
|
|
18
|
+
if (remaining.length <= SLACK_MSG_LIMIT) {
|
|
19
|
+
await client.chat.postMessage({ channel, text: remaining, thread_ts: threadTs });
|
|
20
|
+
break;
|
|
21
|
+
}
|
|
22
|
+
let splitAt = remaining.lastIndexOf('\n', SLACK_MSG_LIMIT);
|
|
23
|
+
if (splitAt === -1)
|
|
24
|
+
splitAt = SLACK_MSG_LIMIT;
|
|
25
|
+
await client.chat.postMessage({ channel, text: remaining.slice(0, splitAt), thread_ts: threadTs });
|
|
26
|
+
remaining = remaining.slice(splitAt).replace(/^\n+/, '');
|
|
27
|
+
}
|
|
28
|
+
}
|
|
29
|
+
// ── Streaming message ─────────────────────────────────────────────────
|
|
30
|
+
export class SlackStreamingMessage {
|
|
31
|
+
client;
|
|
32
|
+
channel;
|
|
33
|
+
threadTs;
|
|
34
|
+
ts = null;
|
|
35
|
+
lastEdit = 0;
|
|
36
|
+
pendingText = '';
|
|
37
|
+
lastFlushedText = '';
|
|
38
|
+
isFinal = false;
|
|
39
|
+
toolStatus = '';
|
|
40
|
+
flushTimer = null;
|
|
41
|
+
progressTimer = null;
|
|
42
|
+
startTime = Date.now();
|
|
43
|
+
toolCallCount = 0;
|
|
44
|
+
lastTextTime = 0;
|
|
45
|
+
/** The message timestamp (available after start). Used for reaction tracking. */
|
|
46
|
+
get messageTs() { return this.ts; }
|
|
47
|
+
constructor(client, channel, threadTs) {
|
|
48
|
+
this.client = client;
|
|
49
|
+
this.channel = channel;
|
|
50
|
+
this.threadTs = threadTs;
|
|
51
|
+
}
|
|
52
|
+
async start() {
|
|
53
|
+
const result = await this.client.chat.postMessage({
|
|
54
|
+
channel: this.channel,
|
|
55
|
+
text: ':sparkles: _thinking..._',
|
|
56
|
+
thread_ts: this.threadTs,
|
|
57
|
+
});
|
|
58
|
+
this.ts = result.ts ?? null;
|
|
59
|
+
this.lastEdit = Date.now();
|
|
60
|
+
// Periodic refresh keeps elapsed time display current during long silent stretches
|
|
61
|
+
this.progressTimer = setInterval(() => {
|
|
62
|
+
if (!this.isFinal && this.toolCallCount > 3)
|
|
63
|
+
this.flush().catch(() => { });
|
|
64
|
+
}, 30_000);
|
|
65
|
+
}
|
|
66
|
+
/** Update the tool activity status line shown during streaming. */
|
|
67
|
+
setToolStatus(status) {
|
|
68
|
+
this.toolStatus = status;
|
|
69
|
+
this.toolCallCount++;
|
|
70
|
+
// Trigger a flush so the status is displayed during long tool chains
|
|
71
|
+
const elapsed = Date.now() - this.lastEdit;
|
|
72
|
+
if (elapsed >= STREAM_UPDATE_INTERVAL) {
|
|
73
|
+
this.flush().catch(() => { });
|
|
74
|
+
}
|
|
75
|
+
else if (!this.flushTimer) {
|
|
76
|
+
this.flushTimer = setTimeout(() => {
|
|
77
|
+
this.flushTimer = null;
|
|
78
|
+
this.flush().catch(() => { });
|
|
79
|
+
}, STREAM_UPDATE_INTERVAL - elapsed);
|
|
80
|
+
}
|
|
81
|
+
}
|
|
82
|
+
async update(text) {
|
|
83
|
+
this.pendingText = text;
|
|
84
|
+
this.lastTextTime = Date.now();
|
|
85
|
+
const elapsed = Date.now() - this.lastEdit;
|
|
86
|
+
if (elapsed >= STREAM_UPDATE_INTERVAL) {
|
|
87
|
+
await this.flush();
|
|
88
|
+
}
|
|
89
|
+
else if (!this.flushTimer) {
|
|
90
|
+
this.flushTimer = setTimeout(() => {
|
|
91
|
+
this.flushTimer = null;
|
|
92
|
+
this.flush().catch(() => { });
|
|
93
|
+
}, STREAM_UPDATE_INTERVAL - elapsed);
|
|
94
|
+
}
|
|
95
|
+
}
|
|
96
|
+
async finalize(text) {
|
|
97
|
+
this.isFinal = true;
|
|
98
|
+
if (this.flushTimer) {
|
|
99
|
+
clearTimeout(this.flushTimer);
|
|
100
|
+
this.flushTimer = null;
|
|
101
|
+
}
|
|
102
|
+
if (this.progressTimer) {
|
|
103
|
+
clearInterval(this.progressTimer);
|
|
104
|
+
this.progressTimer = null;
|
|
105
|
+
}
|
|
106
|
+
if (!text)
|
|
107
|
+
text = '_(no response)_';
|
|
108
|
+
text = mdToSlack(text);
|
|
109
|
+
if (this.ts) {
|
|
110
|
+
if (text.length <= SLACK_MSG_LIMIT) {
|
|
111
|
+
await this.client.chat.update({
|
|
112
|
+
channel: this.channel,
|
|
113
|
+
ts: this.ts,
|
|
114
|
+
text,
|
|
115
|
+
});
|
|
116
|
+
}
|
|
117
|
+
else {
|
|
118
|
+
await this.client.chat.delete({ channel: this.channel, ts: this.ts }).catch(() => { });
|
|
119
|
+
await sendChunkedSlack(this.client, this.channel, text, this.threadTs);
|
|
120
|
+
}
|
|
121
|
+
}
|
|
122
|
+
else {
|
|
123
|
+
await sendChunkedSlack(this.client, this.channel, text, this.threadTs);
|
|
124
|
+
}
|
|
125
|
+
}
|
|
126
|
+
/** Format elapsed milliseconds as human-readable duration. */
|
|
127
|
+
formatElapsed(ms) {
|
|
128
|
+
const s = Math.floor(ms / 1000);
|
|
129
|
+
if (s < 60)
|
|
130
|
+
return `${s}s`;
|
|
131
|
+
const m = Math.floor(s / 60);
|
|
132
|
+
const rem = s % 60;
|
|
133
|
+
return rem > 0 ? `${m}m ${rem}s` : `${m}m`;
|
|
134
|
+
}
|
|
135
|
+
async flush() {
|
|
136
|
+
if (!this.ts || this.isFinal)
|
|
137
|
+
return;
|
|
138
|
+
// Enhanced status when tools have been running 60s+ with no text output
|
|
139
|
+
const silenceDuration = Date.now() - (this.lastTextTime || this.startTime);
|
|
140
|
+
const showProgress = this.toolCallCount > 3 && silenceDuration > 60_000;
|
|
141
|
+
// Skip flush if nothing changed — but always allow when showing progress (elapsed time updates)
|
|
142
|
+
if (!showProgress) {
|
|
143
|
+
if (!this.pendingText && !this.toolStatus)
|
|
144
|
+
return;
|
|
145
|
+
if (this.pendingText === this.lastFlushedText && !this.toolStatus)
|
|
146
|
+
return;
|
|
147
|
+
}
|
|
148
|
+
let display = mdToSlack(this.pendingText);
|
|
149
|
+
let statusLine;
|
|
150
|
+
if (showProgress) {
|
|
151
|
+
const elapsed = this.formatElapsed(Date.now() - this.startTime);
|
|
152
|
+
const current = this.toolStatus ? ` \u2014 ${this.toolStatus}` : '';
|
|
153
|
+
statusLine = `\n\n_:wrench: Working... (${this.toolCallCount} steps, ${elapsed})${current}_`;
|
|
154
|
+
}
|
|
155
|
+
else {
|
|
156
|
+
statusLine = this.toolStatus ? `\n\n_${this.toolStatus}_` : '\n\n:writing_hand: _typing..._';
|
|
157
|
+
}
|
|
158
|
+
if (display.length > SLACK_MSG_LIMIT) {
|
|
159
|
+
display = display.slice(0, SLACK_MSG_LIMIT) + '\n\n_...streaming..._';
|
|
160
|
+
}
|
|
161
|
+
else if (display) {
|
|
162
|
+
display = display + statusLine;
|
|
163
|
+
}
|
|
164
|
+
else {
|
|
165
|
+
// No text yet — show tool status or progress as the main content
|
|
166
|
+
if (showProgress) {
|
|
167
|
+
const elapsed = this.formatElapsed(Date.now() - this.startTime);
|
|
168
|
+
const current = this.toolStatus ? ` \u2014 ${this.toolStatus}` : '';
|
|
169
|
+
display = `:sparkles: _Working... (${this.toolCallCount} steps, ${elapsed})${current}_`;
|
|
170
|
+
}
|
|
171
|
+
else {
|
|
172
|
+
display = this.toolStatus ? `:sparkles: _${this.toolStatus}_` : ':sparkles: _thinking..._';
|
|
173
|
+
}
|
|
174
|
+
}
|
|
175
|
+
try {
|
|
176
|
+
await this.client.chat.update({
|
|
177
|
+
channel: this.channel,
|
|
178
|
+
ts: this.ts,
|
|
179
|
+
text: display,
|
|
180
|
+
});
|
|
181
|
+
this.lastFlushedText = this.pendingText;
|
|
182
|
+
this.lastEdit = Date.now();
|
|
183
|
+
}
|
|
184
|
+
catch {
|
|
185
|
+
// Rate limit or message deleted — ignore
|
|
186
|
+
}
|
|
187
|
+
}
|
|
188
|
+
}
|
|
189
|
+
//# sourceMappingURL=slack-utils.js.map
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Clementine TypeScript — Slack channel adapter.
|
|
3
|
+
*
|
|
4
|
+
* Uses @slack/bolt with Socket Mode (no public URL required).
|
|
5
|
+
* Supports streaming message updates, markdown conversion, and chunked sending.
|
|
6
|
+
*/
|
|
7
|
+
import type { NotificationDispatcher } from '../gateway/notifications.js';
|
|
8
|
+
import type { Gateway } from '../gateway/router.js';
|
|
9
|
+
import type { SlackBotManager } from './slack-bot-manager.js';
|
|
10
|
+
export declare function startSlack(gateway: Gateway, dispatcher: NotificationDispatcher, slackBotManager?: SlackBotManager): Promise<void>;
|
|
11
|
+
//# sourceMappingURL=slack.d.ts.map
|
|
@@ -0,0 +1,196 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Clementine TypeScript — Slack channel adapter.
|
|
3
|
+
*
|
|
4
|
+
* Uses @slack/bolt with Socket Mode (no public URL required).
|
|
5
|
+
* Supports streaming message updates, markdown conversion, and chunked sending.
|
|
6
|
+
*/
|
|
7
|
+
import { App } from '@slack/bolt';
|
|
8
|
+
import pino from 'pino';
|
|
9
|
+
import { SLACK_BOT_TOKEN, SLACK_APP_TOKEN, SLACK_OWNER_USER_ID, VAULT_DIR, } from '../config.js';
|
|
10
|
+
import { mdToSlack, sendChunkedSlack, SlackStreamingMessage } from './slack-utils.js';
|
|
11
|
+
import { friendlyToolName } from './discord-utils.js';
|
|
12
|
+
const logger = pino({ name: 'clementine.slack' });
|
|
13
|
+
const BOT_MESSAGE_TRACKING_LIMIT = 100;
|
|
14
|
+
/** Map of bot message ts -> context for reaction feedback. */
|
|
15
|
+
const slackBotMessageMap = new Map();
|
|
16
|
+
function trackSlackBotMessage(ts, context) {
|
|
17
|
+
slackBotMessageMap.set(ts, context);
|
|
18
|
+
if (slackBotMessageMap.size > BOT_MESSAGE_TRACKING_LIMIT) {
|
|
19
|
+
const firstKey = slackBotMessageMap.keys().next().value;
|
|
20
|
+
if (firstKey)
|
|
21
|
+
slackBotMessageMap.delete(firstKey);
|
|
22
|
+
}
|
|
23
|
+
}
|
|
24
|
+
// ── Lazy memory store for feedback logging ──────────────────────────────
|
|
25
|
+
let _slackFeedbackStore = null;
|
|
26
|
+
async function getSlackFeedbackStore() {
|
|
27
|
+
if (_slackFeedbackStore)
|
|
28
|
+
return _slackFeedbackStore;
|
|
29
|
+
try {
|
|
30
|
+
const { MemoryStore } = await import('../memory/store.js');
|
|
31
|
+
const { MEMORY_DB_PATH } = await import('../config.js');
|
|
32
|
+
const store = new MemoryStore(MEMORY_DB_PATH, VAULT_DIR);
|
|
33
|
+
store.initialize();
|
|
34
|
+
_slackFeedbackStore = store;
|
|
35
|
+
return _slackFeedbackStore;
|
|
36
|
+
}
|
|
37
|
+
catch {
|
|
38
|
+
return null;
|
|
39
|
+
}
|
|
40
|
+
}
|
|
41
|
+
// ── Slack reaction to rating mapping ────────────────────────────────────
|
|
42
|
+
function slackReactionToRating(reaction) {
|
|
43
|
+
const positive = ['+1', 'thumbsup', 'heart', 'star', 'tada', 'raised_hands', 'white_check_mark'];
|
|
44
|
+
const negative = ['-1', 'thumbsdown'];
|
|
45
|
+
if (positive.includes(reaction))
|
|
46
|
+
return 'positive';
|
|
47
|
+
if (negative.includes(reaction))
|
|
48
|
+
return 'negative';
|
|
49
|
+
return null;
|
|
50
|
+
}
|
|
51
|
+
// ── Entry point ───────────────────────────────────────────────────────
|
|
52
|
+
export async function startSlack(gateway, dispatcher, slackBotManager) {
|
|
53
|
+
const app = new App({
|
|
54
|
+
token: SLACK_BOT_TOKEN,
|
|
55
|
+
appToken: SLACK_APP_TOKEN,
|
|
56
|
+
socketMode: true,
|
|
57
|
+
});
|
|
58
|
+
// Catch Socket Mode errors so they don't crash the daemon
|
|
59
|
+
app.error(async (error) => {
|
|
60
|
+
logger.error({ err: error }, 'Slack app error — continuing');
|
|
61
|
+
});
|
|
62
|
+
app.message(async ({ message, client }) => {
|
|
63
|
+
try {
|
|
64
|
+
// Type guard: only handle regular user messages
|
|
65
|
+
if (!('user' in message) || !('text' in message))
|
|
66
|
+
return;
|
|
67
|
+
if ('bot_id' in message && message.bot_id)
|
|
68
|
+
return;
|
|
69
|
+
if ('subtype' in message && message.subtype)
|
|
70
|
+
return;
|
|
71
|
+
// Skip channels owned by agent bots (they handle their own messages)
|
|
72
|
+
if (slackBotManager?.getOwnedChannelIds().includes(message.channel))
|
|
73
|
+
return;
|
|
74
|
+
const userId = message.user;
|
|
75
|
+
// Owner-only check
|
|
76
|
+
if (SLACK_OWNER_USER_ID && userId !== SLACK_OWNER_USER_ID) {
|
|
77
|
+
logger.warn(`Ignored Slack message from non-owner: ${userId}`);
|
|
78
|
+
return;
|
|
79
|
+
}
|
|
80
|
+
let text = message.text ?? '';
|
|
81
|
+
// Extract file attachments (images and files)
|
|
82
|
+
const msgFiles = 'files' in message ? message.files : undefined;
|
|
83
|
+
if (msgFiles && Array.isArray(msgFiles) && msgFiles.length > 0) {
|
|
84
|
+
const fileLines = msgFiles.map((file) => {
|
|
85
|
+
if (file.mimetype?.startsWith('image/')) {
|
|
86
|
+
return `[Image attached: ${file.name} (${file.url_private})]`;
|
|
87
|
+
}
|
|
88
|
+
return `[File attached: ${file.name}, ${file.mimetype || 'unknown type'}, ${file.url_private}]`;
|
|
89
|
+
});
|
|
90
|
+
text = fileLines.join('\n') + (text ? '\n' + text : '');
|
|
91
|
+
}
|
|
92
|
+
if (!text)
|
|
93
|
+
return;
|
|
94
|
+
const channel = message.channel;
|
|
95
|
+
const threadTs = ('thread_ts' in message ? message.thread_ts : undefined) ?? message.ts;
|
|
96
|
+
const sessionKey = `slack:user:${userId}`;
|
|
97
|
+
// ── !stop — abort active query (bypasses session lock) ────────────
|
|
98
|
+
if (text === '!stop' || text === '/stop') {
|
|
99
|
+
const stopped = gateway.stopSession(sessionKey);
|
|
100
|
+
await client.chat.postMessage({ channel, thread_ts: threadTs, text: stopped ? 'Stopping...' : 'Nothing running to stop.' });
|
|
101
|
+
return;
|
|
102
|
+
}
|
|
103
|
+
// ── !verbose command intercept ─────────────────────────────────
|
|
104
|
+
if (text.startsWith('!verbose')) {
|
|
105
|
+
const parts = text.split(/\s+/);
|
|
106
|
+
const level = parts[1]?.toLowerCase();
|
|
107
|
+
if (level === 'quiet' || level === 'normal' || level === 'detailed') {
|
|
108
|
+
gateway.setSessionVerboseLevel(sessionKey, level);
|
|
109
|
+
await client.chat.postMessage({ channel, thread_ts: threadTs, text: `Verbose level set to *${level}*.` });
|
|
110
|
+
}
|
|
111
|
+
else {
|
|
112
|
+
const current = gateway.getSessionVerboseLevel(sessionKey) ?? 'normal';
|
|
113
|
+
await client.chat.postMessage({ channel, thread_ts: threadTs, text: `Current verbose level: *${current}*\nOptions: \`!verbose quiet\`, \`!verbose normal\`, \`!verbose detailed\`` });
|
|
114
|
+
}
|
|
115
|
+
return;
|
|
116
|
+
}
|
|
117
|
+
const streamer = new SlackStreamingMessage(client, channel, threadTs);
|
|
118
|
+
await streamer.start();
|
|
119
|
+
try {
|
|
120
|
+
const response = await gateway.handleMessage(sessionKey, text, (t) => streamer.update(t), undefined, // model
|
|
121
|
+
undefined, // maxTurns
|
|
122
|
+
async (toolName, toolInput) => { streamer.setToolStatus(friendlyToolName(toolName, toolInput)); });
|
|
123
|
+
await streamer.finalize(response);
|
|
124
|
+
// Track bot message for feedback reactions
|
|
125
|
+
if (streamer.messageTs) {
|
|
126
|
+
trackSlackBotMessage(streamer.messageTs, {
|
|
127
|
+
sessionKey,
|
|
128
|
+
userMessage: text.slice(0, 500),
|
|
129
|
+
botResponse: response.slice(0, 500),
|
|
130
|
+
channel,
|
|
131
|
+
});
|
|
132
|
+
}
|
|
133
|
+
}
|
|
134
|
+
catch (err) {
|
|
135
|
+
logger.error({ err }, 'Error processing Slack message');
|
|
136
|
+
await streamer.finalize(`Something went wrong: ${err}`);
|
|
137
|
+
}
|
|
138
|
+
}
|
|
139
|
+
catch (err) {
|
|
140
|
+
logger.error({ err }, 'Unhandled error in Slack message handler');
|
|
141
|
+
}
|
|
142
|
+
});
|
|
143
|
+
// ── Reaction-based feedback handler ─────────────────────────────────
|
|
144
|
+
app.event('reaction_added', async ({ event }) => {
|
|
145
|
+
// Owner-only
|
|
146
|
+
if (SLACK_OWNER_USER_ID && event.user !== SLACK_OWNER_USER_ID)
|
|
147
|
+
return;
|
|
148
|
+
// Check if reaction is on a tracked bot message
|
|
149
|
+
const itemTs = 'ts' in event.item ? event.item.ts : undefined;
|
|
150
|
+
if (!itemTs)
|
|
151
|
+
return;
|
|
152
|
+
const context = slackBotMessageMap.get(itemTs);
|
|
153
|
+
if (!context)
|
|
154
|
+
return;
|
|
155
|
+
// Map reaction to rating
|
|
156
|
+
const rating = slackReactionToRating(event.reaction);
|
|
157
|
+
if (!rating)
|
|
158
|
+
return;
|
|
159
|
+
// Log feedback
|
|
160
|
+
try {
|
|
161
|
+
const store = await getSlackFeedbackStore();
|
|
162
|
+
if (store) {
|
|
163
|
+
store.logFeedback({
|
|
164
|
+
sessionKey: context.sessionKey,
|
|
165
|
+
channel: 'slack',
|
|
166
|
+
messageSnippet: context.userMessage,
|
|
167
|
+
responseSnippet: context.botResponse,
|
|
168
|
+
rating,
|
|
169
|
+
});
|
|
170
|
+
logger.info({ rating, ts: itemTs }, 'Feedback logged via Slack reaction');
|
|
171
|
+
}
|
|
172
|
+
}
|
|
173
|
+
catch (err) {
|
|
174
|
+
logger.warn({ err }, 'Failed to log Slack reaction feedback');
|
|
175
|
+
}
|
|
176
|
+
});
|
|
177
|
+
// Register notification sender
|
|
178
|
+
async function slackNotify(text) {
|
|
179
|
+
if (!SLACK_OWNER_USER_ID)
|
|
180
|
+
return;
|
|
181
|
+
try {
|
|
182
|
+
const dm = await app.client.conversations.open({ users: SLACK_OWNER_USER_ID });
|
|
183
|
+
const channelId = dm.channel?.id;
|
|
184
|
+
if (channelId) {
|
|
185
|
+
await sendChunkedSlack(app.client, channelId, mdToSlack(text));
|
|
186
|
+
}
|
|
187
|
+
}
|
|
188
|
+
catch (err) {
|
|
189
|
+
logger.error({ err }, 'Failed to send Slack notification');
|
|
190
|
+
}
|
|
191
|
+
}
|
|
192
|
+
dispatcher.register('slack', slackNotify);
|
|
193
|
+
logger.info('Starting Slack bot (Socket Mode)...');
|
|
194
|
+
await app.start();
|
|
195
|
+
}
|
|
196
|
+
//# sourceMappingURL=slack.js.map
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Clementine TypeScript — Telegram channel adapter.
|
|
3
|
+
*
|
|
4
|
+
* Uses grammY for long polling. Supports streaming message edits,
|
|
5
|
+
* markdown conversion, message chunking, and voice message handling (placeholder).
|
|
6
|
+
*/
|
|
7
|
+
import type { NotificationDispatcher } from '../gateway/notifications.js';
|
|
8
|
+
import type { Gateway } from '../gateway/router.js';
|
|
9
|
+
export declare function startTelegram(gateway: Gateway, dispatcher: NotificationDispatcher): Promise<void>;
|
|
10
|
+
//# sourceMappingURL=telegram.d.ts.map
|
|
@@ -0,0 +1,235 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Clementine TypeScript — Telegram channel adapter.
|
|
3
|
+
*
|
|
4
|
+
* Uses grammY for long polling. Supports streaming message edits,
|
|
5
|
+
* markdown conversion, message chunking, and voice message handling (placeholder).
|
|
6
|
+
*/
|
|
7
|
+
import { Bot } from 'grammy';
|
|
8
|
+
import pino from 'pino';
|
|
9
|
+
import { TELEGRAM_BOT_TOKEN, TELEGRAM_OWNER_ID, } from '../config.js';
|
|
10
|
+
const logger = pino({ name: 'clementine.telegram' });
|
|
11
|
+
const STREAM_UPDATE_INTERVAL = 1500; // ms
|
|
12
|
+
const TELEGRAM_MSG_LIMIT = 4096;
|
|
13
|
+
// ── Markdown conversion ───────────────────────────────────────────────
|
|
14
|
+
function mdToTelegram(text) {
|
|
15
|
+
// Convert Markdown bold **text** to single-asterisk for plain text mode
|
|
16
|
+
return text.replace(/\*\*(.+?)\*\*/g, '*$1*');
|
|
17
|
+
}
|
|
18
|
+
// ── Chunked sending ───────────────────────────────────────────────────
|
|
19
|
+
async function sendChunked(bot, chatId, text) {
|
|
20
|
+
let remaining = text;
|
|
21
|
+
while (remaining) {
|
|
22
|
+
if (remaining.length <= TELEGRAM_MSG_LIMIT) {
|
|
23
|
+
await bot.api.sendMessage(chatId, remaining);
|
|
24
|
+
break;
|
|
25
|
+
}
|
|
26
|
+
let splitAt = remaining.lastIndexOf('\n', TELEGRAM_MSG_LIMIT);
|
|
27
|
+
if (splitAt === -1)
|
|
28
|
+
splitAt = TELEGRAM_MSG_LIMIT;
|
|
29
|
+
await bot.api.sendMessage(chatId, remaining.slice(0, splitAt));
|
|
30
|
+
remaining = remaining.slice(splitAt).replace(/^\n+/, '');
|
|
31
|
+
}
|
|
32
|
+
}
|
|
33
|
+
// ── Streaming message ─────────────────────────────────────────────────
|
|
34
|
+
class TelegramStreamingMessage {
|
|
35
|
+
bot;
|
|
36
|
+
chatId;
|
|
37
|
+
messageId = null;
|
|
38
|
+
lastEdit = 0;
|
|
39
|
+
pendingText = '';
|
|
40
|
+
isFinal = false;
|
|
41
|
+
constructor(bot, chatId) {
|
|
42
|
+
this.bot = bot;
|
|
43
|
+
this.chatId = chatId;
|
|
44
|
+
}
|
|
45
|
+
async start() {
|
|
46
|
+
const msg = await this.bot.api.sendMessage(this.chatId, '\u2728 _thinking\\.\\.\\._', { parse_mode: 'MarkdownV2' });
|
|
47
|
+
this.messageId = msg.message_id;
|
|
48
|
+
this.lastEdit = Date.now();
|
|
49
|
+
}
|
|
50
|
+
async update(text) {
|
|
51
|
+
this.pendingText = text;
|
|
52
|
+
if (Date.now() - this.lastEdit >= STREAM_UPDATE_INTERVAL) {
|
|
53
|
+
await this.flush();
|
|
54
|
+
}
|
|
55
|
+
}
|
|
56
|
+
async finalize(text) {
|
|
57
|
+
this.isFinal = true;
|
|
58
|
+
if (!text)
|
|
59
|
+
text = '_(no response)_';
|
|
60
|
+
text = mdToTelegram(text);
|
|
61
|
+
if (this.messageId) {
|
|
62
|
+
if (text.length <= TELEGRAM_MSG_LIMIT) {
|
|
63
|
+
try {
|
|
64
|
+
await this.bot.api.editMessageText(this.chatId, this.messageId, text);
|
|
65
|
+
}
|
|
66
|
+
catch {
|
|
67
|
+
// If edit fails (message unchanged), send new
|
|
68
|
+
await sendChunked(this.bot, this.chatId, text);
|
|
69
|
+
}
|
|
70
|
+
}
|
|
71
|
+
else {
|
|
72
|
+
// Delete placeholder and send in chunks
|
|
73
|
+
try {
|
|
74
|
+
await this.bot.api.deleteMessage(this.chatId, this.messageId);
|
|
75
|
+
}
|
|
76
|
+
catch {
|
|
77
|
+
// Ignore delete failure
|
|
78
|
+
}
|
|
79
|
+
await sendChunked(this.bot, this.chatId, text);
|
|
80
|
+
}
|
|
81
|
+
}
|
|
82
|
+
else {
|
|
83
|
+
await sendChunked(this.bot, this.chatId, text);
|
|
84
|
+
}
|
|
85
|
+
}
|
|
86
|
+
async flush() {
|
|
87
|
+
if (this.messageId === null || !this.pendingText || this.isFinal)
|
|
88
|
+
return;
|
|
89
|
+
let display = mdToTelegram(this.pendingText);
|
|
90
|
+
if (display.length > TELEGRAM_MSG_LIMIT - 20) {
|
|
91
|
+
display = display.slice(0, TELEGRAM_MSG_LIMIT - 20) + '\n\n...streaming...';
|
|
92
|
+
}
|
|
93
|
+
else {
|
|
94
|
+
display = display + '\n\n\u270d\ufe0f typing...';
|
|
95
|
+
}
|
|
96
|
+
try {
|
|
97
|
+
await this.bot.api.editMessageText(this.chatId, this.messageId, display);
|
|
98
|
+
this.lastEdit = Date.now();
|
|
99
|
+
}
|
|
100
|
+
catch {
|
|
101
|
+
// Rate limit or message unchanged — ignore
|
|
102
|
+
}
|
|
103
|
+
}
|
|
104
|
+
}
|
|
105
|
+
// ── Entry point ───────────────────────────────────────────────────────
|
|
106
|
+
export async function startTelegram(gateway, dispatcher) {
|
|
107
|
+
const bot = new Bot(TELEGRAM_BOT_TOKEN);
|
|
108
|
+
const ownerIdNum = Number(TELEGRAM_OWNER_ID);
|
|
109
|
+
// Catch errors from Grammy so they don't crash the daemon
|
|
110
|
+
bot.catch((err) => {
|
|
111
|
+
logger.error({ err: err.error, ctx: err.ctx?.update?.update_id }, 'Telegram bot error — continuing');
|
|
112
|
+
});
|
|
113
|
+
bot.on('message:text', async (ctx) => {
|
|
114
|
+
const userId = ctx.from?.id;
|
|
115
|
+
if (!userId)
|
|
116
|
+
return;
|
|
117
|
+
// Owner-only check
|
|
118
|
+
if (TELEGRAM_OWNER_ID && userId !== ownerIdNum) {
|
|
119
|
+
logger.warn(`Ignored Telegram message from non-owner: ${userId}`);
|
|
120
|
+
return;
|
|
121
|
+
}
|
|
122
|
+
const text = ctx.message.text ?? '';
|
|
123
|
+
if (!text)
|
|
124
|
+
return;
|
|
125
|
+
const chatId = ctx.chat.id;
|
|
126
|
+
const sessionKey = `telegram:user:${userId}`;
|
|
127
|
+
// ── Approval responses ──────────────────────────────────────────
|
|
128
|
+
const lower = text.toLowerCase().trim();
|
|
129
|
+
if (['yes', 'no', 'approve', 'deny', 'go', 'skip', 'always'].includes(lower)) {
|
|
130
|
+
const approvals = gateway.getPendingApprovals();
|
|
131
|
+
if (approvals.length > 0) {
|
|
132
|
+
const result = lower === 'always' ? 'always' :
|
|
133
|
+
(lower === 'yes' || lower === 'approve' || lower === 'go');
|
|
134
|
+
gateway.resolveApproval(approvals[approvals.length - 1], result);
|
|
135
|
+
const approved = result !== false;
|
|
136
|
+
await ctx.reply(approved ? '✅ Approved.' : '❌ Denied.');
|
|
137
|
+
return;
|
|
138
|
+
}
|
|
139
|
+
}
|
|
140
|
+
const streamer = new TelegramStreamingMessage(bot, chatId);
|
|
141
|
+
await streamer.start();
|
|
142
|
+
try {
|
|
143
|
+
const response = await gateway.handleMessage(sessionKey, text, (t) => streamer.update(t));
|
|
144
|
+
await streamer.finalize(response);
|
|
145
|
+
}
|
|
146
|
+
catch (err) {
|
|
147
|
+
logger.error({ err }, 'Error processing Telegram message');
|
|
148
|
+
await streamer.finalize(`Something went wrong: ${err}`);
|
|
149
|
+
}
|
|
150
|
+
});
|
|
151
|
+
// Photo message handler — extracts image URL and forwards to gateway
|
|
152
|
+
bot.on('message:photo', async (ctx) => {
|
|
153
|
+
const userId = ctx.from?.id;
|
|
154
|
+
if (!userId)
|
|
155
|
+
return;
|
|
156
|
+
if (TELEGRAM_OWNER_ID && userId !== ownerIdNum)
|
|
157
|
+
return;
|
|
158
|
+
// Get the largest photo size (last in the array)
|
|
159
|
+
const photo = ctx.message.photo[ctx.message.photo.length - 1];
|
|
160
|
+
const file = await ctx.api.getFile(photo.file_id);
|
|
161
|
+
const fileUrl = `https://api.telegram.org/file/bot${TELEGRAM_BOT_TOKEN}/${file.file_path}`;
|
|
162
|
+
const caption = ctx.message.caption || '';
|
|
163
|
+
const text = `[Image attached: photo (${fileUrl})]\n${caption}`.trim();
|
|
164
|
+
const chatId = ctx.chat.id;
|
|
165
|
+
const sessionKey = `telegram:user:${userId}`;
|
|
166
|
+
const streamer = new TelegramStreamingMessage(bot, chatId);
|
|
167
|
+
await streamer.start();
|
|
168
|
+
try {
|
|
169
|
+
const response = await gateway.handleMessage(sessionKey, text, (t) => streamer.update(t));
|
|
170
|
+
await streamer.finalize(response);
|
|
171
|
+
}
|
|
172
|
+
catch (err) {
|
|
173
|
+
logger.error({ err }, 'Error processing Telegram photo');
|
|
174
|
+
await streamer.finalize(`Something went wrong: ${err}`);
|
|
175
|
+
}
|
|
176
|
+
});
|
|
177
|
+
// Document message handler — extracts file URL and forwards to gateway
|
|
178
|
+
bot.on('message:document', async (ctx) => {
|
|
179
|
+
const userId = ctx.from?.id;
|
|
180
|
+
if (!userId)
|
|
181
|
+
return;
|
|
182
|
+
if (TELEGRAM_OWNER_ID && userId !== ownerIdNum)
|
|
183
|
+
return;
|
|
184
|
+
const doc = ctx.message.document;
|
|
185
|
+
const file = await ctx.api.getFile(doc.file_id);
|
|
186
|
+
const fileUrl = `https://api.telegram.org/file/bot${TELEGRAM_BOT_TOKEN}/${file.file_path}`;
|
|
187
|
+
const caption = ctx.message.caption || '';
|
|
188
|
+
const isImage = doc.mime_type?.startsWith('image/');
|
|
189
|
+
const prefix = isImage
|
|
190
|
+
? `[Image attached: ${doc.file_name} (${fileUrl})]`
|
|
191
|
+
: `[File attached: ${doc.file_name}, ${doc.mime_type || 'unknown type'}, ${fileUrl}]`;
|
|
192
|
+
const text = `${prefix}\n${caption}`.trim();
|
|
193
|
+
const chatId = ctx.chat.id;
|
|
194
|
+
const sessionKey = `telegram:user:${userId}`;
|
|
195
|
+
const streamer = new TelegramStreamingMessage(bot, chatId);
|
|
196
|
+
await streamer.start();
|
|
197
|
+
try {
|
|
198
|
+
const response = await gateway.handleMessage(sessionKey, text, (t) => streamer.update(t));
|
|
199
|
+
await streamer.finalize(response);
|
|
200
|
+
}
|
|
201
|
+
catch (err) {
|
|
202
|
+
logger.error({ err }, 'Error processing Telegram document');
|
|
203
|
+
await streamer.finalize(`Something went wrong: ${err}`);
|
|
204
|
+
}
|
|
205
|
+
});
|
|
206
|
+
// Voice message handler (placeholder — note for future STT integration)
|
|
207
|
+
bot.on('message:voice', async (ctx) => {
|
|
208
|
+
const userId = ctx.from?.id;
|
|
209
|
+
if (!userId)
|
|
210
|
+
return;
|
|
211
|
+
if (TELEGRAM_OWNER_ID && userId !== ownerIdNum)
|
|
212
|
+
return;
|
|
213
|
+
// TODO: Download voice file, transcribe via Groq Whisper STT
|
|
214
|
+
await ctx.reply('Voice messages are not yet supported in this version.');
|
|
215
|
+
});
|
|
216
|
+
// Register notification sender
|
|
217
|
+
async function telegramNotify(text) {
|
|
218
|
+
if (!TELEGRAM_OWNER_ID || ownerIdNum === 0)
|
|
219
|
+
return;
|
|
220
|
+
try {
|
|
221
|
+
const notifyText = mdToTelegram(text);
|
|
222
|
+
await sendChunked(bot, ownerIdNum, notifyText);
|
|
223
|
+
}
|
|
224
|
+
catch (err) {
|
|
225
|
+
logger.error({ err }, 'Failed to send Telegram notification');
|
|
226
|
+
}
|
|
227
|
+
}
|
|
228
|
+
dispatcher.register('telegram', telegramNotify);
|
|
229
|
+
logger.info('Starting Telegram bot (long polling)...');
|
|
230
|
+
await bot.start({
|
|
231
|
+
drop_pending_updates: true,
|
|
232
|
+
onStart: () => logger.info('Telegram bot polling started'),
|
|
233
|
+
});
|
|
234
|
+
}
|
|
235
|
+
//# sourceMappingURL=telegram.js.map
|