omnikey-cli 1.5.7 → 1.6.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/backend-dist/__tests__/ai-client.nemotron.test.js +127 -0
- package/backend-dist/agent/agentPrompts.js +4 -3
- package/backend-dist/agent/utils.js +6 -5
- package/backend-dist/ai-client.js +151 -16
- package/backend-dist/config.js +16 -1
- package/backend-dist/db.js +5 -1
- package/backend-dist/mcpServerRoutes.js +16 -4
- package/backend-dist/scheduledJobRoutes.js +5 -2
- package/dist/index.js +1 -1
- package/dist/onboard.js +38 -0
- package/dist/telegramClient.js +1 -1
- package/dist/telegramDaemon.js +6 -4
- package/package.json +8 -6
- package/src/index.ts +1 -1
- package/src/onboard.ts +38 -0
- package/src/telegramClient.ts +1 -1
- package/src/telegramDaemon.ts +6 -8
- package/telegram-client-dist/{dist/agentClient.js → agentClient.js} +91 -75
- package/telegram-client-dist/{dist/config.js → config.js} +10 -12
- package/telegram-client-dist/{dist/index.js → index.js} +23 -32
- package/telegram-client-dist/{dist/notifyTelegram.js → notifyTelegram.js} +193 -194
- package/telegram-client-dist/{dist/omnikeyAuth.js → omnikeyAuth.js} +8 -13
- package/telegram-client-dist/dist/db.js +0 -78
|
@@ -8,28 +8,27 @@ exports.notify = notify;
|
|
|
8
8
|
exports.setupMessageListener = setupMessageListener;
|
|
9
9
|
const node_telegram_bot_api_1 = __importDefault(require("node-telegram-bot-api"));
|
|
10
10
|
const agentClient_1 = require("./agentClient");
|
|
11
|
-
const db_1 = require("./db");
|
|
12
11
|
let bot = null;
|
|
13
12
|
function initTelegram(botToken) {
|
|
14
13
|
if (!botToken)
|
|
15
|
-
throw new Error(
|
|
14
|
+
throw new Error('Missing telegram bot token');
|
|
16
15
|
bot = new node_telegram_bot_api_1.default(botToken, { polling: true });
|
|
17
16
|
return bot;
|
|
18
17
|
}
|
|
19
18
|
async function notify(logger, message, options = {}) {
|
|
20
19
|
if (!bot) {
|
|
21
|
-
throw new Error(
|
|
20
|
+
throw new Error('Telegram bot not initialized. Call initTelegram first.');
|
|
22
21
|
}
|
|
23
22
|
const chatId = options.chatId ?? process.env.TELEGRAM_CHAT_ID;
|
|
24
23
|
if (!chatId) {
|
|
25
|
-
throw new Error(
|
|
24
|
+
throw new Error('Missing chat ID');
|
|
26
25
|
}
|
|
27
|
-
const parseMode = options.parseMode ??
|
|
26
|
+
const parseMode = options.parseMode ?? 'Markdown';
|
|
28
27
|
try {
|
|
29
28
|
return await bot.sendMessage(chatId, message, { parse_mode: parseMode });
|
|
30
29
|
}
|
|
31
30
|
catch (err) {
|
|
32
|
-
logger.error(
|
|
31
|
+
logger.error('Failed to send Telegram message:', err);
|
|
33
32
|
throw err;
|
|
34
33
|
}
|
|
35
34
|
}
|
|
@@ -37,18 +36,18 @@ const pendingPrompts = new Map();
|
|
|
37
36
|
const runningSessions = new Map();
|
|
38
37
|
// Callback-data prefixes. Telegram limits callback_data to 64 bytes — using
|
|
39
38
|
// short prefixes + indices keeps every payload comfortably under the cap.
|
|
40
|
-
const CB_SESSION =
|
|
41
|
-
const CB_INSTRUCTION =
|
|
42
|
-
const CB_PROJECT =
|
|
43
|
-
const CB_CANCEL =
|
|
39
|
+
const CB_SESSION = 's:'; // session picker; "s:new" or "s:<idx>"
|
|
40
|
+
const CB_INSTRUCTION = 't:'; // instruction picker; "t:skip" or "t:<idx>"
|
|
41
|
+
const CB_PROJECT = 'g:'; // project picker; "g:skip" or "g:<idx>"
|
|
42
|
+
const CB_CANCEL = 'x:cancel';
|
|
44
43
|
function isAuthorizedChat(chatId) {
|
|
45
|
-
const allowed = parseInt(process.env.TELEGRAM_CHAT_ID ||
|
|
44
|
+
const allowed = parseInt(process.env.TELEGRAM_CHAT_ID || '0', 10);
|
|
46
45
|
return allowed !== 0 && chatId === allowed;
|
|
47
46
|
}
|
|
48
47
|
function truncate(text, max) {
|
|
49
48
|
if (text.length <= max)
|
|
50
49
|
return text;
|
|
51
|
-
return text.slice(0, max - 1) +
|
|
50
|
+
return text.slice(0, max - 1) + '…';
|
|
52
51
|
}
|
|
53
52
|
/**
|
|
54
53
|
* Strip XML-ish agent tags, code fences, markdown emphasis, and collapse
|
|
@@ -56,14 +55,14 @@ function truncate(text, max) {
|
|
|
56
55
|
*/
|
|
57
56
|
function cleanForTelegram(text) {
|
|
58
57
|
return text
|
|
59
|
-
.replace(/<\/?(?:shell_script|final_answer|user_input|stored_instructions|project_context|shell_function_calls)[^>]*>/gi,
|
|
60
|
-
.replace(/```[a-zA-Z0-9_-]*\n?/g,
|
|
61
|
-
.replace(/```/g,
|
|
62
|
-
.replace(/\*\*([^*]+)\*\*/g,
|
|
63
|
-
.replace(/\*([^*\n]+)\*/g,
|
|
64
|
-
.replace(/`([^`\n]+)`/g,
|
|
65
|
-
.replace(/[ \t]+\n/g,
|
|
66
|
-
.replace(/\n{3,}/g,
|
|
58
|
+
.replace(/<\/?(?:shell_script|final_answer|user_input|stored_instructions|project_context|shell_function_calls)[^>]*>/gi, '')
|
|
59
|
+
.replace(/```[a-zA-Z0-9_-]*\n?/g, '')
|
|
60
|
+
.replace(/```/g, '')
|
|
61
|
+
.replace(/\*\*([^*]+)\*\*/g, '$1')
|
|
62
|
+
.replace(/\*([^*\n]+)\*/g, '$1')
|
|
63
|
+
.replace(/`([^`\n]+)`/g, '$1')
|
|
64
|
+
.replace(/[ \t]+\n/g, '\n')
|
|
65
|
+
.replace(/\n{3,}/g, '\n\n')
|
|
67
66
|
.trim();
|
|
68
67
|
}
|
|
69
68
|
/**
|
|
@@ -75,11 +74,11 @@ function cleanForTelegram(text) {
|
|
|
75
74
|
* <pre><code class="language-...">, <a href="...">, <blockquote>.
|
|
76
75
|
*/
|
|
77
76
|
function escapeHtml(s) {
|
|
78
|
-
return s.replace(/&/g,
|
|
77
|
+
return s.replace(/&/g, '&').replace(/</g, '<').replace(/>/g, '>');
|
|
79
78
|
}
|
|
80
79
|
function markdownToTelegramHtml(input) {
|
|
81
80
|
// Strip agent envelope tags but preserve the body's markdown.
|
|
82
|
-
let text = input.replace(/<\/?(?:shell_script|final_answer|user_input|stored_instructions|project_context|shell_function_calls)[^>]*>/gi,
|
|
81
|
+
let text = input.replace(/<\/?(?:shell_script|final_answer|user_input|stored_instructions|project_context|shell_function_calls)[^>]*>/gi, '');
|
|
83
82
|
// 1. Extract fenced code blocks and inline code into placeholders so
|
|
84
83
|
// subsequent transforms do not corrupt their contents.
|
|
85
84
|
const placeholders = [];
|
|
@@ -88,8 +87,8 @@ function markdownToTelegramHtml(input) {
|
|
|
88
87
|
return `\u0000PH${idx}\u0000`;
|
|
89
88
|
};
|
|
90
89
|
text = text.replace(/```([a-zA-Z0-9_+.-]*)\n?([\s\S]*?)```/g, (_m, lang, body) => {
|
|
91
|
-
const cls = lang ? ` class="language-${escapeHtml(lang)}"` :
|
|
92
|
-
return stash(`<pre><code${cls}>${escapeHtml(body.replace(/\n$/,
|
|
90
|
+
const cls = lang ? ` class="language-${escapeHtml(lang)}"` : '';
|
|
91
|
+
return stash(`<pre><code${cls}>${escapeHtml(body.replace(/\n$/, ''))}</code></pre>`);
|
|
93
92
|
});
|
|
94
93
|
text = text.replace(/`([^`\n]+)`/g, (_m, body) => stash(`<code>${escapeHtml(body)}</code>`));
|
|
95
94
|
// 1b. GFM tables → fixed-width <pre> blocks (Telegram HTML has no <table>).
|
|
@@ -98,15 +97,15 @@ function markdownToTelegramHtml(input) {
|
|
|
98
97
|
text = text.replace(/(^|\n)([^\n]*\|[^\n]*)\n[ \t]*\|?[ \t]*:?-{2,}:?[ \t]*(?:\|[ \t]*:?-{2,}:?[ \t]*)+\|?[ \t]*\n((?:[^\n]*\|[^\n]*(?:\n|$))+)/g, (_m, lead, header, body) => {
|
|
99
98
|
const splitRow = (row) => {
|
|
100
99
|
let r = row.trim();
|
|
101
|
-
if (r.startsWith(
|
|
100
|
+
if (r.startsWith('|'))
|
|
102
101
|
r = r.slice(1);
|
|
103
|
-
if (r.endsWith(
|
|
102
|
+
if (r.endsWith('|'))
|
|
104
103
|
r = r.slice(0, -1);
|
|
105
|
-
return r.split(
|
|
104
|
+
return r.split('|').map((c) => c.trim());
|
|
106
105
|
};
|
|
107
106
|
const headerCells = splitRow(header);
|
|
108
107
|
const bodyRows = body
|
|
109
|
-
.split(
|
|
108
|
+
.split('\n')
|
|
110
109
|
.map((l) => l.trim())
|
|
111
110
|
.filter((l) => l.length > 0)
|
|
112
111
|
.map(splitRow);
|
|
@@ -114,16 +113,16 @@ function markdownToTelegramHtml(input) {
|
|
|
114
113
|
const pad = (cells) => {
|
|
115
114
|
const out = cells.slice();
|
|
116
115
|
while (out.length < colCount)
|
|
117
|
-
out.push(
|
|
116
|
+
out.push('');
|
|
118
117
|
return out;
|
|
119
118
|
};
|
|
120
119
|
const allRows = [pad(headerCells), ...bodyRows.map(pad)];
|
|
121
120
|
// Strip inline markdown that won't render inside <pre>.
|
|
122
121
|
const cleanCell = (s) => s
|
|
123
|
-
.replace(/\*\*([^*]+)\*\*/g,
|
|
124
|
-
.replace(/__([^_]+)__/g,
|
|
125
|
-
.replace(/`([^`]+)`/g,
|
|
126
|
-
.replace(/\[([^\]]+)\]\([^)]+\)/g,
|
|
122
|
+
.replace(/\*\*([^*]+)\*\*/g, '$1')
|
|
123
|
+
.replace(/__([^_]+)__/g, '$1')
|
|
124
|
+
.replace(/`([^`]+)`/g, '$1')
|
|
125
|
+
.replace(/\[([^\]]+)\]\([^)]+\)/g, '$1');
|
|
127
126
|
const cleaned = allRows.map((row) => row.map(cleanCell));
|
|
128
127
|
const widths = [];
|
|
129
128
|
for (let c = 0; c < colCount; c++) {
|
|
@@ -132,45 +131,45 @@ function markdownToTelegramHtml(input) {
|
|
|
132
131
|
w = Math.max(w, row[c]?.length ?? 0);
|
|
133
132
|
widths[c] = w;
|
|
134
133
|
}
|
|
135
|
-
const renderRow = (row) => row.map((cell, i) => cell.padEnd(widths[i],
|
|
136
|
-
const sep = widths.map((w) =>
|
|
134
|
+
const renderRow = (row) => row.map((cell, i) => cell.padEnd(widths[i], ' ')).join(' │ ');
|
|
135
|
+
const sep = widths.map((w) => '─'.repeat(w)).join('─┼─');
|
|
137
136
|
const lines = [];
|
|
138
137
|
lines.push(renderRow(cleaned[0]));
|
|
139
138
|
lines.push(sep);
|
|
140
139
|
for (let i = 1; i < cleaned.length; i++)
|
|
141
140
|
lines.push(renderRow(cleaned[i]));
|
|
142
|
-
return `${lead}${stash(`<pre>${escapeHtml(lines.join(
|
|
141
|
+
return `${lead}${stash(`<pre>${escapeHtml(lines.join('\n'))}</pre>`)}\n`;
|
|
143
142
|
});
|
|
144
143
|
// 2. Everything outside code is HTML-escaped now.
|
|
145
144
|
text = escapeHtml(text);
|
|
146
145
|
// 3. Inline-link conversion before emphasis so the URL never gets italicised.
|
|
147
146
|
text = text.replace(/\[([^\]]+)\]\(([^)\s]+)\)/g, (_m, label, href) => `<a href="${href}">${label}</a>`);
|
|
148
147
|
// 4. Bold / italic / strike-through. Order matters — handle ** before *.
|
|
149
|
-
text = text.replace(/\*\*([^\n*]+)\*\*/g,
|
|
150
|
-
text = text.replace(/__([^\n_]+)__/g,
|
|
151
|
-
text = text.replace(/(^|[^*])\*([^\n*]+)\*(?!\*)/g,
|
|
152
|
-
text = text.replace(/(^|[^_])_([^\n_]+)_(?!_)/g,
|
|
153
|
-
text = text.replace(/~~([^\n~]+)~~/g,
|
|
148
|
+
text = text.replace(/\*\*([^\n*]+)\*\*/g, '<b>$1</b>');
|
|
149
|
+
text = text.replace(/__([^\n_]+)__/g, '<b>$1</b>');
|
|
150
|
+
text = text.replace(/(^|[^*])\*([^\n*]+)\*(?!\*)/g, '$1<i>$2</i>');
|
|
151
|
+
text = text.replace(/(^|[^_])_([^\n_]+)_(?!_)/g, '$1<i>$2</i>');
|
|
152
|
+
text = text.replace(/~~([^\n~]+)~~/g, '<s>$1</s>');
|
|
154
153
|
// 5. Headings (#, ##, ###) → bold line. Telegram has no real heading style.
|
|
155
|
-
text = text.replace(/^[ \t]*#{1,6}[ \t]+(.+)$/gm,
|
|
154
|
+
text = text.replace(/^[ \t]*#{1,6}[ \t]+(.+)$/gm, '<b>$1</b>');
|
|
156
155
|
// 6. Bullet lists: turn "- " / "* " / "+ " into "• ".
|
|
157
|
-
text = text.replace(/^[ \t]*[-*+][ \t]+/gm,
|
|
156
|
+
text = text.replace(/^[ \t]*[-*+][ \t]+/gm, '• ');
|
|
158
157
|
// 7. Block quotes — Telegram supports <blockquote>.
|
|
159
158
|
text = text.replace(/(^|\n)((?:> .*(?:\n|$))+)/g, (_m, lead, block) => {
|
|
160
159
|
const inner = block
|
|
161
160
|
.split(/\n/)
|
|
162
161
|
.filter((l) => l.length)
|
|
163
|
-
.map((l) => l.replace(/^> ?/,
|
|
164
|
-
.join(
|
|
162
|
+
.map((l) => l.replace(/^> ?/, ''))
|
|
163
|
+
.join('\n');
|
|
165
164
|
return `${lead}<blockquote>${inner}</blockquote>\n`;
|
|
166
165
|
});
|
|
167
166
|
// 8. Collapse trailing whitespace and excessive blank lines.
|
|
168
167
|
text = text
|
|
169
|
-
.replace(/[ \t]+\n/g,
|
|
170
|
-
.replace(/\n{3,}/g,
|
|
168
|
+
.replace(/[ \t]+\n/g, '\n')
|
|
169
|
+
.replace(/\n{3,}/g, '\n\n')
|
|
171
170
|
.trim();
|
|
172
171
|
// 9. Restore the stashed code segments.
|
|
173
|
-
text = text.replace(/\u0000PH(\d+)\u0000/g, (_m, idx) => placeholders[Number(idx)] ??
|
|
172
|
+
text = text.replace(/\u0000PH(\d+)\u0000/g, (_m, idx) => placeholders[Number(idx)] ?? '');
|
|
174
173
|
return text;
|
|
175
174
|
}
|
|
176
175
|
/**
|
|
@@ -183,11 +182,11 @@ function splitForTelegram(html, max = 3800) {
|
|
|
183
182
|
return [html];
|
|
184
183
|
const chunks = [];
|
|
185
184
|
const paragraphs = html.split(/\n\n+/);
|
|
186
|
-
let current =
|
|
185
|
+
let current = '';
|
|
187
186
|
const flush = () => {
|
|
188
187
|
if (current.trim())
|
|
189
188
|
chunks.push(current.trim());
|
|
190
|
-
current =
|
|
189
|
+
current = '';
|
|
191
190
|
};
|
|
192
191
|
for (const p of paragraphs) {
|
|
193
192
|
if (p.length > max) {
|
|
@@ -200,7 +199,7 @@ function splitForTelegram(html, max = 3800) {
|
|
|
200
199
|
if (current.length + p.length + 2 > max) {
|
|
201
200
|
flush();
|
|
202
201
|
}
|
|
203
|
-
current += (current ?
|
|
202
|
+
current += (current ? '\n\n' : '') + p;
|
|
204
203
|
}
|
|
205
204
|
flush();
|
|
206
205
|
return chunks;
|
|
@@ -208,7 +207,7 @@ function splitForTelegram(html, max = 3800) {
|
|
|
208
207
|
// ─── Inline-keyboard builders ────────────────────────────────────────────────
|
|
209
208
|
function buildSessionKeyboard(sessions) {
|
|
210
209
|
const rows = [
|
|
211
|
-
[{ text:
|
|
210
|
+
[{ text: '🆕 New session', callback_data: `${CB_SESSION}new` }],
|
|
212
211
|
];
|
|
213
212
|
sessions.forEach((s, idx) => {
|
|
214
213
|
const label = truncate(s.title || s.id, 48);
|
|
@@ -219,13 +218,13 @@ function buildSessionKeyboard(sessions) {
|
|
|
219
218
|
},
|
|
220
219
|
]);
|
|
221
220
|
});
|
|
222
|
-
rows.push([{ text:
|
|
221
|
+
rows.push([{ text: '✕ Cancel', callback_data: CB_CANCEL }]);
|
|
223
222
|
return rows;
|
|
224
223
|
}
|
|
225
224
|
function buildInstructionKeyboard(templates) {
|
|
226
225
|
const rows = [];
|
|
227
226
|
templates.forEach((t, idx) => {
|
|
228
|
-
const marker = t.isDefault ?
|
|
227
|
+
const marker = t.isDefault ? '⭐' : '📝';
|
|
229
228
|
rows.push([
|
|
230
229
|
{
|
|
231
230
|
text: `${marker} ${truncate(t.heading, 50)}`,
|
|
@@ -233,10 +232,8 @@ function buildInstructionKeyboard(templates) {
|
|
|
233
232
|
},
|
|
234
233
|
]);
|
|
235
234
|
});
|
|
236
|
-
rows.push([
|
|
237
|
-
|
|
238
|
-
]);
|
|
239
|
-
rows.push([{ text: "✕ Cancel", callback_data: CB_CANCEL }]);
|
|
235
|
+
rows.push([{ text: '⏭ Skip instructions', callback_data: `${CB_INSTRUCTION}skip` }]);
|
|
236
|
+
rows.push([{ text: '✕ Cancel', callback_data: CB_CANCEL }]);
|
|
240
237
|
return rows;
|
|
241
238
|
}
|
|
242
239
|
function buildProjectKeyboard(groups) {
|
|
@@ -249,94 +246,82 @@ function buildProjectKeyboard(groups) {
|
|
|
249
246
|
},
|
|
250
247
|
]);
|
|
251
248
|
});
|
|
252
|
-
rows.push([{ text:
|
|
253
|
-
rows.push([{ text:
|
|
249
|
+
rows.push([{ text: '⏭ Skip project', callback_data: `${CB_PROJECT}skip` }]);
|
|
250
|
+
rows.push([{ text: '✕ Cancel', callback_data: CB_CANCEL }]);
|
|
254
251
|
return rows;
|
|
255
252
|
}
|
|
256
253
|
function renderInstructionStep(state) {
|
|
257
254
|
if (state.templates.length === 0) {
|
|
258
255
|
return {
|
|
259
256
|
text: [
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
].join(
|
|
257
|
+
'*Step 1 of 3 · Task instructions*',
|
|
258
|
+
'',
|
|
259
|
+
'_No saved templates. Skip to continue._',
|
|
260
|
+
].join('\n'),
|
|
264
261
|
keyboard: [
|
|
265
262
|
[
|
|
266
263
|
{
|
|
267
|
-
text:
|
|
264
|
+
text: '⏭ Skip instructions',
|
|
268
265
|
callback_data: `${CB_INSTRUCTION}skip`,
|
|
269
266
|
},
|
|
270
267
|
],
|
|
271
|
-
[{ text:
|
|
268
|
+
[{ text: '✕ Cancel', callback_data: CB_CANCEL }],
|
|
272
269
|
],
|
|
273
270
|
};
|
|
274
271
|
}
|
|
275
272
|
return {
|
|
276
273
|
text: [
|
|
277
|
-
|
|
278
|
-
|
|
279
|
-
|
|
280
|
-
].join(
|
|
274
|
+
'*Step 1 of 3 · Task instructions*',
|
|
275
|
+
'',
|
|
276
|
+
'Pick a saved template to prepend to your prompt, or skip.',
|
|
277
|
+
].join('\n'),
|
|
281
278
|
keyboard: buildInstructionKeyboard(state.templates),
|
|
282
279
|
};
|
|
283
280
|
}
|
|
284
281
|
function renderProjectStep(state) {
|
|
285
282
|
const heading = state.chosenInstructionsHeading
|
|
286
283
|
? `✓ Instructions: *${state.chosenInstructionsHeading}*`
|
|
287
|
-
:
|
|
284
|
+
: '✓ Instructions: _skipped_';
|
|
288
285
|
if (state.groups.length === 0) {
|
|
289
286
|
return {
|
|
290
|
-
text: [
|
|
291
|
-
"*Step 2 of 3 · Project*",
|
|
292
|
-
heading,
|
|
293
|
-
"",
|
|
294
|
-
"_No projects yet. Skip to continue._",
|
|
295
|
-
].join("\n"),
|
|
287
|
+
text: ['*Step 2 of 3 · Project*', heading, '', '_No projects yet. Skip to continue._'].join('\n'),
|
|
296
288
|
keyboard: [
|
|
297
|
-
[{ text:
|
|
298
|
-
[{ text:
|
|
289
|
+
[{ text: '⏭ Skip project', callback_data: `${CB_PROJECT}skip` }],
|
|
290
|
+
[{ text: '✕ Cancel', callback_data: CB_CANCEL }],
|
|
299
291
|
],
|
|
300
292
|
};
|
|
301
293
|
}
|
|
302
294
|
return {
|
|
303
|
-
text: [
|
|
304
|
-
"*Step 2 of 3 · Project*",
|
|
305
|
-
heading,
|
|
306
|
-
"",
|
|
307
|
-
"Pick a project for context, or skip.",
|
|
308
|
-
].join("\n"),
|
|
295
|
+
text: ['*Step 2 of 3 · Project*', heading, '', 'Pick a project for context, or skip.'].join('\n'),
|
|
309
296
|
keyboard: buildProjectKeyboard(state.groups),
|
|
310
297
|
};
|
|
311
298
|
}
|
|
312
299
|
function renderPromptStep(state) {
|
|
313
300
|
const lines = [
|
|
314
|
-
|
|
301
|
+
'*Step 3 of 3 · Prompt*',
|
|
315
302
|
state.chosenInstructionsHeading
|
|
316
303
|
? `✓ Instructions: *${state.chosenInstructionsHeading}*`
|
|
317
|
-
:
|
|
318
|
-
state.chosenGroupName
|
|
319
|
-
|
|
320
|
-
|
|
321
|
-
"",
|
|
322
|
-
"Send your prompt as the next message.",
|
|
304
|
+
: '✓ Instructions: _skipped_',
|
|
305
|
+
state.chosenGroupName ? `✓ Project: *${state.chosenGroupName}*` : '✓ Project: _skipped_',
|
|
306
|
+
'',
|
|
307
|
+
'Send your prompt as the next message.',
|
|
323
308
|
];
|
|
324
309
|
return {
|
|
325
|
-
text: lines.join(
|
|
326
|
-
keyboard: [[{ text:
|
|
310
|
+
text: lines.join('\n'),
|
|
311
|
+
keyboard: [[{ text: '✕ Cancel', callback_data: CB_CANCEL }]],
|
|
327
312
|
};
|
|
328
313
|
}
|
|
329
314
|
async function showStep(logger, chatId, state) {
|
|
330
315
|
if (!bot)
|
|
331
316
|
return;
|
|
332
|
-
const copy = state.phase ===
|
|
317
|
+
const copy = state.phase === 'selectInstruction'
|
|
333
318
|
? renderInstructionStep(state)
|
|
334
|
-
: state.phase ===
|
|
319
|
+
: state.phase === 'selectProject'
|
|
335
320
|
? renderProjectStep(state)
|
|
336
321
|
: renderPromptStep(state);
|
|
337
322
|
if (state.wizardMessageId == null) {
|
|
338
323
|
const sent = await bot.sendMessage(chatId, copy.text, {
|
|
339
|
-
parse_mode:
|
|
324
|
+
parse_mode: 'Markdown',
|
|
340
325
|
reply_markup: { inline_keyboard: copy.keyboard },
|
|
341
326
|
});
|
|
342
327
|
state.wizardMessageId = sent.message_id;
|
|
@@ -346,17 +331,17 @@ async function showStep(logger, chatId, state) {
|
|
|
346
331
|
await bot.editMessageText(copy.text, {
|
|
347
332
|
chat_id: chatId,
|
|
348
333
|
message_id: state.wizardMessageId,
|
|
349
|
-
parse_mode:
|
|
334
|
+
parse_mode: 'Markdown',
|
|
350
335
|
reply_markup: { inline_keyboard: copy.keyboard },
|
|
351
336
|
});
|
|
352
337
|
}
|
|
353
338
|
catch (err) {
|
|
354
339
|
// Fall back to a fresh message if the previous one is no longer editable.
|
|
355
|
-
logger.warn(
|
|
340
|
+
logger.warn('Failed to edit wizard message; sending a new one', {
|
|
356
341
|
error: err.message,
|
|
357
342
|
});
|
|
358
343
|
const sent = await bot.sendMessage(chatId, copy.text, {
|
|
359
|
-
parse_mode:
|
|
344
|
+
parse_mode: 'Markdown',
|
|
360
345
|
reply_markup: { inline_keyboard: copy.keyboard },
|
|
361
346
|
});
|
|
362
347
|
state.wizardMessageId = sent.message_id;
|
|
@@ -369,7 +354,7 @@ async function finishWizardMessage(chatId, state, finalText) {
|
|
|
369
354
|
await bot.editMessageText(finalText, {
|
|
370
355
|
chat_id: chatId,
|
|
371
356
|
message_id: state.wizardMessageId,
|
|
372
|
-
parse_mode:
|
|
357
|
+
parse_mode: 'Markdown',
|
|
373
358
|
reply_markup: { inline_keyboard: [] },
|
|
374
359
|
});
|
|
375
360
|
}
|
|
@@ -380,16 +365,16 @@ async function finishWizardMessage(chatId, state, finalText) {
|
|
|
380
365
|
// ─── Pickers / commands ──────────────────────────────────────────────────────
|
|
381
366
|
async function sendSessionPicker(logger, chatId, sessions, verbose) {
|
|
382
367
|
if (!bot)
|
|
383
|
-
throw new Error(
|
|
384
|
-
const verboseTag = verbose ?
|
|
368
|
+
throw new Error('Bot not initialized');
|
|
369
|
+
const verboseTag = verbose ? ' · 🔍 _verbose_' : '';
|
|
385
370
|
const text = sessions.length === 0
|
|
386
371
|
? `*OmniKey Agent*${verboseTag}\n\nNo previous sessions. Start a new one?`
|
|
387
372
|
: `*OmniKey Agent*${verboseTag}\n\nResume a recent session or start fresh:`;
|
|
388
373
|
await bot.sendMessage(chatId, text, {
|
|
389
|
-
parse_mode:
|
|
374
|
+
parse_mode: 'Markdown',
|
|
390
375
|
reply_markup: { inline_keyboard: buildSessionKeyboard(sessions) },
|
|
391
376
|
});
|
|
392
|
-
logger.info(
|
|
377
|
+
logger.info('Sent /cmd session picker', {
|
|
393
378
|
chatId,
|
|
394
379
|
count: sessions.length,
|
|
395
380
|
verbose,
|
|
@@ -405,7 +390,7 @@ async function handleCmdCommand(logger, chatId, verbose) {
|
|
|
405
390
|
await sendSessionPicker(logger, chatId, sessions, verbose);
|
|
406
391
|
}
|
|
407
392
|
catch (err) {
|
|
408
|
-
logger.error(
|
|
393
|
+
logger.error('Failed to list recent sessions', {
|
|
409
394
|
error: err.message,
|
|
410
395
|
});
|
|
411
396
|
await notify(logger, `❌ Failed to load sessions: ${err.message}`, { chatId });
|
|
@@ -428,14 +413,30 @@ async function handleTaskCommand(logger, chatId) {
|
|
|
428
413
|
}
|
|
429
414
|
// 2. Otherwise show final answer from the most recent completed session.
|
|
430
415
|
try {
|
|
431
|
-
const
|
|
416
|
+
const sessions = await (0, agentClient_1.listRecentSessions)(logger, 1);
|
|
417
|
+
const session = sessions[0];
|
|
432
418
|
if (!session) {
|
|
433
|
-
await notify(logger,
|
|
419
|
+
await notify(logger, '🗒️ No sessions found.', { chatId });
|
|
434
420
|
return;
|
|
435
421
|
}
|
|
436
|
-
const
|
|
422
|
+
const messages = await (0, agentClient_1.getSessionMessages)(logger, session.id);
|
|
423
|
+
let finalAnswer = null;
|
|
424
|
+
if (messages) {
|
|
425
|
+
for (let i = messages.length - 1; i >= 0; i--) {
|
|
426
|
+
const msg = messages[i];
|
|
427
|
+
if (msg.role !== 'assistant')
|
|
428
|
+
continue;
|
|
429
|
+
const block = msg.blocks?.find((b) => b.kind === 'finalAnswer');
|
|
430
|
+
if (block) {
|
|
431
|
+
finalAnswer = block.text;
|
|
432
|
+
break;
|
|
433
|
+
}
|
|
434
|
+
}
|
|
435
|
+
}
|
|
437
436
|
if (!finalAnswer) {
|
|
438
|
-
await notify(logger, `🗒️ Most recent session \`${session.id}\` has no final answer yet.`, {
|
|
437
|
+
await notify(logger, `🗒️ Most recent session \`${session.id}\` has no final answer yet.`, {
|
|
438
|
+
chatId,
|
|
439
|
+
});
|
|
439
440
|
return;
|
|
440
441
|
}
|
|
441
442
|
const title = session.title || session.id;
|
|
@@ -447,20 +448,20 @@ async function handleTaskCommand(logger, chatId) {
|
|
|
447
448
|
for (const chunk of chunks) {
|
|
448
449
|
try {
|
|
449
450
|
await bot.sendMessage(chatId, chunk, {
|
|
450
|
-
parse_mode:
|
|
451
|
+
parse_mode: 'HTML',
|
|
451
452
|
disable_web_page_preview: true,
|
|
452
453
|
});
|
|
453
454
|
}
|
|
454
455
|
catch (err) {
|
|
455
|
-
logger.warn(
|
|
456
|
+
logger.warn('HTML render failed for /task; falling back to plain', {
|
|
456
457
|
error: err.message,
|
|
457
458
|
});
|
|
458
|
-
await bot.sendMessage(chatId, chunk.replace(/<[^>]+>/g,
|
|
459
|
+
await bot.sendMessage(chatId, chunk.replace(/<[^>]+>/g, ''));
|
|
459
460
|
}
|
|
460
461
|
}
|
|
461
462
|
}
|
|
462
463
|
catch (err) {
|
|
463
|
-
logger.error(
|
|
464
|
+
logger.error('Failed to handle /task', { error: err.message });
|
|
464
465
|
await notify(logger, `❌ /task failed: ${err.message}`, {
|
|
465
466
|
chatId,
|
|
466
467
|
});
|
|
@@ -474,13 +475,13 @@ async function startNewSessionWizard(logger, chatId) {
|
|
|
474
475
|
try {
|
|
475
476
|
[templates, groups] = await Promise.all([
|
|
476
477
|
(0, agentClient_1.listTaskTemplates)(logger).catch((e) => {
|
|
477
|
-
logger.warn(
|
|
478
|
+
logger.warn('Failed to load task templates', {
|
|
478
479
|
error: e.message,
|
|
479
480
|
});
|
|
480
481
|
return [];
|
|
481
482
|
}),
|
|
482
483
|
(0, agentClient_1.listProjectGroups)(logger).catch((e) => {
|
|
483
|
-
logger.warn(
|
|
484
|
+
logger.warn('Failed to load project groups', {
|
|
484
485
|
error: e.message,
|
|
485
486
|
});
|
|
486
487
|
return [];
|
|
@@ -488,12 +489,12 @@ async function startNewSessionWizard(logger, chatId) {
|
|
|
488
489
|
]);
|
|
489
490
|
}
|
|
490
491
|
catch (err) {
|
|
491
|
-
logger.error(
|
|
492
|
+
logger.error('Failed to load wizard data', {
|
|
492
493
|
error: err.message,
|
|
493
494
|
});
|
|
494
495
|
}
|
|
495
496
|
const state = {
|
|
496
|
-
phase:
|
|
497
|
+
phase: 'selectInstruction',
|
|
497
498
|
sessionId: null,
|
|
498
499
|
wizardMessageId: null,
|
|
499
500
|
templates,
|
|
@@ -511,7 +512,7 @@ async function startResumeSession(logger, chatId, sessionId) {
|
|
|
511
512
|
return;
|
|
512
513
|
const verbose = pendingVerbose.get(chatId) ?? false;
|
|
513
514
|
const state = {
|
|
514
|
-
phase:
|
|
515
|
+
phase: 'awaitPrompt',
|
|
515
516
|
sessionId,
|
|
516
517
|
wizardMessageId: null,
|
|
517
518
|
templates: [],
|
|
@@ -523,23 +524,23 @@ async function startResumeSession(logger, chatId, sessionId) {
|
|
|
523
524
|
};
|
|
524
525
|
pendingPrompts.set(chatId, state);
|
|
525
526
|
await bot.sendMessage(chatId, [
|
|
526
|
-
`*Resuming session* \`${sessionId}\`${verbose ?
|
|
527
|
-
|
|
528
|
-
|
|
529
|
-
].join(
|
|
530
|
-
parse_mode:
|
|
527
|
+
`*Resuming session* \`${sessionId}\`${verbose ? ' · 🔍 _verbose_' : ''}`,
|
|
528
|
+
'',
|
|
529
|
+
'Send your prompt as the next message.',
|
|
530
|
+
].join('\n'), {
|
|
531
|
+
parse_mode: 'Markdown',
|
|
531
532
|
reply_markup: {
|
|
532
|
-
inline_keyboard: [[{ text:
|
|
533
|
+
inline_keyboard: [[{ text: '✕ Cancel', callback_data: CB_CANCEL }]],
|
|
533
534
|
},
|
|
534
535
|
});
|
|
535
536
|
}
|
|
536
537
|
async function handleCallbackQuery(logger, query) {
|
|
537
538
|
if (!bot)
|
|
538
539
|
return;
|
|
539
|
-
const data = query.data ||
|
|
540
|
+
const data = query.data || '';
|
|
540
541
|
const chatId = query.message?.chat.id;
|
|
541
542
|
if (!chatId || !isAuthorizedChat(chatId)) {
|
|
542
|
-
await bot.answerCallbackQuery(query.id, { text:
|
|
543
|
+
await bot.answerCallbackQuery(query.id, { text: 'Unauthorized' });
|
|
543
544
|
return;
|
|
544
545
|
}
|
|
545
546
|
// Cancel from anywhere — clear state and acknowledge.
|
|
@@ -549,15 +550,15 @@ async function handleCallbackQuery(logger, query) {
|
|
|
549
550
|
sessionListCache.delete(chatId);
|
|
550
551
|
pendingVerbose.delete(chatId);
|
|
551
552
|
if (state?.wizardMessageId) {
|
|
552
|
-
await finishWizardMessage(chatId, state,
|
|
553
|
+
await finishWizardMessage(chatId, state, '✕ Cancelled.');
|
|
553
554
|
}
|
|
554
|
-
await bot.answerCallbackQuery(query.id, { text:
|
|
555
|
+
await bot.answerCallbackQuery(query.id, { text: 'Cancelled' });
|
|
555
556
|
return;
|
|
556
557
|
}
|
|
557
558
|
// Step 0 — session picker
|
|
558
559
|
if (data.startsWith(CB_SESSION)) {
|
|
559
560
|
const tok = data.slice(CB_SESSION.length);
|
|
560
|
-
if (tok ===
|
|
561
|
+
if (tok === 'new') {
|
|
561
562
|
await bot.answerCallbackQuery(query.id);
|
|
562
563
|
await startNewSessionWizard(logger, chatId);
|
|
563
564
|
return;
|
|
@@ -567,7 +568,7 @@ async function handleCallbackQuery(logger, query) {
|
|
|
567
568
|
const chosen = Number.isInteger(idx) ? sessions[idx] : undefined;
|
|
568
569
|
if (!chosen) {
|
|
569
570
|
await bot.answerCallbackQuery(query.id, {
|
|
570
|
-
text:
|
|
571
|
+
text: 'Session no longer available',
|
|
571
572
|
});
|
|
572
573
|
return;
|
|
573
574
|
}
|
|
@@ -578,12 +579,12 @@ async function handleCallbackQuery(logger, query) {
|
|
|
578
579
|
// Step 1 — instruction picker
|
|
579
580
|
if (data.startsWith(CB_INSTRUCTION)) {
|
|
580
581
|
const state = pendingPrompts.get(chatId);
|
|
581
|
-
if (!state || state.phase !==
|
|
582
|
-
await bot.answerCallbackQuery(query.id, { text:
|
|
582
|
+
if (!state || state.phase !== 'selectInstruction') {
|
|
583
|
+
await bot.answerCallbackQuery(query.id, { text: 'Step expired' });
|
|
583
584
|
return;
|
|
584
585
|
}
|
|
585
586
|
const tok = data.slice(CB_INSTRUCTION.length);
|
|
586
|
-
if (tok ===
|
|
587
|
+
if (tok === 'skip') {
|
|
587
588
|
state.chosenInstructions = null;
|
|
588
589
|
state.chosenInstructionsHeading = null;
|
|
589
590
|
}
|
|
@@ -591,7 +592,7 @@ async function handleCallbackQuery(logger, query) {
|
|
|
591
592
|
const idx = Number(tok);
|
|
592
593
|
const t = state.templates[idx];
|
|
593
594
|
if (!t) {
|
|
594
|
-
await bot.answerCallbackQuery(query.id, { text:
|
|
595
|
+
await bot.answerCallbackQuery(query.id, { text: 'Template not found' });
|
|
595
596
|
return;
|
|
596
597
|
}
|
|
597
598
|
// Don't prepend the body to the user prompt — instead promote this
|
|
@@ -605,17 +606,17 @@ async function handleCallbackQuery(logger, query) {
|
|
|
605
606
|
state.chosenInstructionsHeading = t.heading;
|
|
606
607
|
}
|
|
607
608
|
catch (err) {
|
|
608
|
-
logger.error(
|
|
609
|
+
logger.error('Failed to set default task template', {
|
|
609
610
|
templateId: t.id,
|
|
610
611
|
error: err.message,
|
|
611
612
|
});
|
|
612
613
|
await bot.answerCallbackQuery(query.id, {
|
|
613
|
-
text:
|
|
614
|
+
text: 'Failed to set default',
|
|
614
615
|
});
|
|
615
616
|
return;
|
|
616
617
|
}
|
|
617
618
|
}
|
|
618
|
-
state.phase =
|
|
619
|
+
state.phase = 'selectProject';
|
|
619
620
|
await bot.answerCallbackQuery(query.id);
|
|
620
621
|
await showStep(logger, chatId, state);
|
|
621
622
|
return;
|
|
@@ -623,24 +624,24 @@ async function handleCallbackQuery(logger, query) {
|
|
|
623
624
|
// Step 2 — project picker
|
|
624
625
|
if (data.startsWith(CB_PROJECT)) {
|
|
625
626
|
const state = pendingPrompts.get(chatId);
|
|
626
|
-
if (!state || state.phase !==
|
|
627
|
-
await bot.answerCallbackQuery(query.id, { text:
|
|
627
|
+
if (!state || state.phase !== 'selectProject') {
|
|
628
|
+
await bot.answerCallbackQuery(query.id, { text: 'Step expired' });
|
|
628
629
|
return;
|
|
629
630
|
}
|
|
630
631
|
const tok = data.slice(CB_PROJECT.length);
|
|
631
|
-
if (tok ===
|
|
632
|
+
if (tok === 'skip') {
|
|
632
633
|
state.chosenGroupName = null;
|
|
633
634
|
}
|
|
634
635
|
else {
|
|
635
636
|
const idx = Number(tok);
|
|
636
637
|
const g = state.groups[idx];
|
|
637
638
|
if (!g) {
|
|
638
|
-
await bot.answerCallbackQuery(query.id, { text:
|
|
639
|
+
await bot.answerCallbackQuery(query.id, { text: 'Project not found' });
|
|
639
640
|
return;
|
|
640
641
|
}
|
|
641
642
|
state.chosenGroupName = g.groupName;
|
|
642
643
|
}
|
|
643
|
-
state.phase =
|
|
644
|
+
state.phase = 'awaitPrompt';
|
|
644
645
|
await bot.answerCallbackQuery(query.id);
|
|
645
646
|
await showStep(logger, chatId, state);
|
|
646
647
|
return;
|
|
@@ -650,23 +651,21 @@ async function handleCallbackQuery(logger, query) {
|
|
|
650
651
|
async function runAgentForChat(logger, chatId, pending, prompt) {
|
|
651
652
|
pendingPrompts.delete(chatId);
|
|
652
653
|
if (runningSessions.has(chatId)) {
|
|
653
|
-
await notify(logger,
|
|
654
|
+
await notify(logger, '⏳ A session is already running. Wait for it to finish.', { chatId });
|
|
654
655
|
return;
|
|
655
656
|
}
|
|
656
657
|
// Mark the wizard as resolved so the user sees a clean trail of choices.
|
|
657
658
|
if (pending.wizardMessageId) {
|
|
658
659
|
const summary = [
|
|
659
|
-
|
|
660
|
+
'✅ *Session started*',
|
|
660
661
|
pending.chosenInstructionsHeading
|
|
661
662
|
? `📝 Instructions: *${pending.chosenInstructionsHeading}*`
|
|
662
|
-
:
|
|
663
|
-
pending.chosenGroupName
|
|
664
|
-
|
|
665
|
-
: "📁 Project: _none_",
|
|
666
|
-
pending.verbose ? "🔍 Verbose: *on*" : "",
|
|
663
|
+
: '📝 Instructions: _none_',
|
|
664
|
+
pending.chosenGroupName ? `📁 Project: *${pending.chosenGroupName}*` : '📁 Project: _none_',
|
|
665
|
+
pending.verbose ? '🔍 Verbose: *on*' : '',
|
|
667
666
|
]
|
|
668
667
|
.filter(Boolean)
|
|
669
|
-
.join(
|
|
668
|
+
.join('\n');
|
|
670
669
|
await finishWizardMessage(chatId, pending, summary);
|
|
671
670
|
}
|
|
672
671
|
// The selected task template is already promoted to default on the
|
|
@@ -674,7 +673,7 @@ async function runAgentForChat(logger, chatId, pending, prompt) {
|
|
|
674
673
|
// stored_instructions on the server side. We send the user's prompt
|
|
675
674
|
// verbatim — no preamble injection here.
|
|
676
675
|
const composedPrompt = prompt;
|
|
677
|
-
const placeholderId = pending.sessionId ??
|
|
676
|
+
const placeholderId = pending.sessionId ?? '(new)';
|
|
678
677
|
const abortController = new AbortController();
|
|
679
678
|
const state = {
|
|
680
679
|
sessionId: placeholderId,
|
|
@@ -684,7 +683,7 @@ async function runAgentForChat(logger, chatId, pending, prompt) {
|
|
|
684
683
|
stoppedByUser: false,
|
|
685
684
|
};
|
|
686
685
|
runningSessions.set(chatId, state);
|
|
687
|
-
await notify(logger,
|
|
686
|
+
await notify(logger, '🚀 Starting agent run… (send /stop to cancel)', {
|
|
688
687
|
chatId,
|
|
689
688
|
});
|
|
690
689
|
const REASONING_MAX = 1200;
|
|
@@ -696,7 +695,7 @@ async function runAgentForChat(logger, chatId, pending, prompt) {
|
|
|
696
695
|
await bot.sendMessage(chatId, body);
|
|
697
696
|
}
|
|
698
697
|
catch (e) {
|
|
699
|
-
logger.warn(
|
|
698
|
+
logger.warn('Failed to forward block to telegram', {
|
|
700
699
|
error: e.message,
|
|
701
700
|
});
|
|
702
701
|
}
|
|
@@ -706,20 +705,20 @@ async function runAgentForChat(logger, chatId, pending, prompt) {
|
|
|
706
705
|
return;
|
|
707
706
|
try {
|
|
708
707
|
await bot.sendMessage(chatId, body, {
|
|
709
|
-
parse_mode:
|
|
708
|
+
parse_mode: 'HTML',
|
|
710
709
|
disable_web_page_preview: true,
|
|
711
710
|
});
|
|
712
711
|
}
|
|
713
712
|
catch (e) {
|
|
714
713
|
// Fall back to plain text if Telegram rejects the HTML payload.
|
|
715
|
-
logger.warn(
|
|
714
|
+
logger.warn('HTML render failed; falling back to plain text', {
|
|
716
715
|
error: e.message,
|
|
717
716
|
});
|
|
718
717
|
try {
|
|
719
|
-
await bot.sendMessage(chatId, body.replace(/<[^>]+>/g,
|
|
718
|
+
await bot.sendMessage(chatId, body.replace(/<[^>]+>/g, ''));
|
|
720
719
|
}
|
|
721
720
|
catch (e2) {
|
|
722
|
-
logger.warn(
|
|
721
|
+
logger.warn('Plain-text fallback also failed', {
|
|
723
722
|
error: e2.message,
|
|
724
723
|
});
|
|
725
724
|
}
|
|
@@ -732,7 +731,7 @@ async function runAgentForChat(logger, chatId, pending, prompt) {
|
|
|
732
731
|
groupName: pending.chosenGroupName ?? undefined,
|
|
733
732
|
signal: abortController.signal,
|
|
734
733
|
onBlock: async (block) => {
|
|
735
|
-
if (block.kind ===
|
|
734
|
+
if (block.kind === 'reasoning') {
|
|
736
735
|
const cleaned = cleanForTelegram(block.text);
|
|
737
736
|
if (!cleaned)
|
|
738
737
|
return;
|
|
@@ -740,11 +739,11 @@ async function runAgentForChat(logger, chatId, pending, prompt) {
|
|
|
740
739
|
if (cleaned === lastSent)
|
|
741
740
|
return;
|
|
742
741
|
lastSent = cleaned;
|
|
743
|
-
const prefix = pending.verbose ?
|
|
742
|
+
const prefix = pending.verbose ? '💭 ' : '';
|
|
744
743
|
await sendPlain(prefix + truncate(cleaned, REASONING_MAX));
|
|
745
744
|
return;
|
|
746
745
|
}
|
|
747
|
-
if (block.kind ===
|
|
746
|
+
if (block.kind === 'finalAnswer') {
|
|
748
747
|
// finalAnswer — render markdown as Telegram HTML and split into
|
|
749
748
|
// 4096-char-safe chunks so long answers are never truncated.
|
|
750
749
|
const html = markdownToTelegramHtml(block.text);
|
|
@@ -763,29 +762,29 @@ async function runAgentForChat(logger, chatId, pending, prompt) {
|
|
|
763
762
|
return;
|
|
764
763
|
const VERBOSE_MAX = 1500;
|
|
765
764
|
switch (block.kind) {
|
|
766
|
-
case
|
|
765
|
+
case 'shellCommand': {
|
|
767
766
|
const body = truncate(block.text.trim(), VERBOSE_MAX);
|
|
768
767
|
await sendHtml(`🛠 <b>Shell command</b>\n<pre><code class="language-bash">${escapeHtml(body)}</code></pre>`);
|
|
769
768
|
return;
|
|
770
769
|
}
|
|
771
|
-
case
|
|
770
|
+
case 'terminalOutput': {
|
|
772
771
|
const body = truncate(block.text.trim(), VERBOSE_MAX);
|
|
773
772
|
if (!body)
|
|
774
773
|
return;
|
|
775
774
|
await sendHtml(`📤 <b>Terminal output</b>\n<pre>${escapeHtml(body)}</pre>`);
|
|
776
775
|
return;
|
|
777
776
|
}
|
|
778
|
-
case
|
|
777
|
+
case 'webCall': {
|
|
779
778
|
const body = truncate(cleanForTelegram(block.text) || block.text, VERBOSE_MAX);
|
|
780
779
|
await sendHtml(`🌐 <b>Web call</b>\n${escapeHtml(body)}`);
|
|
781
780
|
return;
|
|
782
781
|
}
|
|
783
|
-
case
|
|
782
|
+
case 'mcpCall': {
|
|
784
783
|
const body = truncate(cleanForTelegram(block.text) || block.text, VERBOSE_MAX);
|
|
785
784
|
await sendHtml(`🔌 <b>MCP call</b>\n${escapeHtml(body)}`);
|
|
786
785
|
return;
|
|
787
786
|
}
|
|
788
|
-
case
|
|
787
|
+
case 'imageRendering': {
|
|
789
788
|
const body = truncate(cleanForTelegram(block.text) || block.text, VERBOSE_MAX);
|
|
790
789
|
await sendHtml(`🖼 <b>Image</b>\n${escapeHtml(body)}`);
|
|
791
790
|
return;
|
|
@@ -794,21 +793,23 @@ async function runAgentForChat(logger, chatId, pending, prompt) {
|
|
|
794
793
|
},
|
|
795
794
|
});
|
|
796
795
|
state.sessionId = result.sessionId;
|
|
797
|
-
logger.info(
|
|
796
|
+
logger.info('Agent run completed', {
|
|
798
797
|
chatId,
|
|
799
798
|
sessionId: result.sessionId,
|
|
800
799
|
});
|
|
801
800
|
}
|
|
802
801
|
catch (err) {
|
|
803
802
|
if (err instanceof agentClient_1.AgentAbortError || state.stoppedByUser) {
|
|
804
|
-
logger.info(
|
|
803
|
+
logger.info('Agent run stopped by user', {
|
|
805
804
|
chatId,
|
|
806
805
|
sessionId: state.sessionId,
|
|
807
806
|
});
|
|
808
|
-
await notify(logger, `🛑 Agent run stopped by user (session \`${state.sessionId}\`).`, {
|
|
807
|
+
await notify(logger, `🛑 Agent run stopped by user (session \`${state.sessionId}\`).`, {
|
|
808
|
+
chatId,
|
|
809
|
+
});
|
|
809
810
|
}
|
|
810
811
|
else {
|
|
811
|
-
logger.error(
|
|
812
|
+
logger.error('Agent run failed', { error: err.message });
|
|
812
813
|
await notify(logger, `❌ Agent run failed: ${err.message}`, {
|
|
813
814
|
chatId,
|
|
814
815
|
});
|
|
@@ -822,72 +823,70 @@ async function runAgentForChat(logger, chatId, pending, prompt) {
|
|
|
822
823
|
async function handleStopCommand(logger, chatId) {
|
|
823
824
|
const running = runningSessions.get(chatId);
|
|
824
825
|
if (!running) {
|
|
825
|
-
await notify(logger,
|
|
826
|
+
await notify(logger, '🛑 No agent session is currently running.', {
|
|
826
827
|
chatId,
|
|
827
828
|
});
|
|
828
829
|
return;
|
|
829
830
|
}
|
|
830
831
|
if (running.abortController.signal.aborted) {
|
|
831
|
-
await notify(logger,
|
|
832
|
+
await notify(logger, '⏳ Stop already requested — waiting for shutdown…', {
|
|
832
833
|
chatId,
|
|
833
834
|
});
|
|
834
835
|
return;
|
|
835
836
|
}
|
|
836
837
|
running.stoppedByUser = true;
|
|
837
838
|
running.abortController.abort();
|
|
838
|
-
logger.info(
|
|
839
|
+
logger.info('User requested agent stop', {
|
|
839
840
|
chatId,
|
|
840
841
|
sessionId: running.sessionId,
|
|
841
842
|
});
|
|
842
843
|
await notify(logger, `🛑 Stop requested for session \`${running.sessionId}\`.`, { chatId });
|
|
843
844
|
}
|
|
844
845
|
function setupMessageListener(logger, bot) {
|
|
845
|
-
bot.on(
|
|
846
|
+
bot.on('callback_query', (q) => {
|
|
846
847
|
void handleCallbackQuery(logger, q).catch((err) => {
|
|
847
|
-
logger.error(
|
|
848
|
+
logger.error('callback_query handler crashed', {
|
|
848
849
|
error: err.message,
|
|
849
850
|
});
|
|
850
851
|
});
|
|
851
852
|
});
|
|
852
|
-
bot.on(
|
|
853
|
+
bot.on('message', async (msg) => {
|
|
853
854
|
const chatId = msg.chat.id;
|
|
854
855
|
if (!isAuthorizedChat(chatId)) {
|
|
855
|
-
logger.warn(
|
|
856
|
+
logger.warn('Received message from unauthorized chat ID:', chatId);
|
|
856
857
|
return;
|
|
857
858
|
}
|
|
858
|
-
const text = (msg.text ||
|
|
859
|
+
const text = (msg.text || '').trim();
|
|
859
860
|
logger.info(`Received message from chat ID ${chatId}: ${text}`);
|
|
860
861
|
const lower = text.toLowerCase();
|
|
861
|
-
if (lower ===
|
|
862
|
-
const args = text
|
|
863
|
-
|
|
864
|
-
.trim()
|
|
865
|
-
.split(/\s+/)
|
|
866
|
-
.filter(Boolean);
|
|
867
|
-
const verbose = args.some((a) => a === "--verbose" || a === "-v" || a === "--verbos");
|
|
862
|
+
if (lower === '/cmd' || lower.startsWith('/cmd ')) {
|
|
863
|
+
const args = text.slice('/cmd'.length).trim().split(/\s+/).filter(Boolean);
|
|
864
|
+
const verbose = args.some((a) => a === '--verbose' || a === '-v' || a === '--verbos');
|
|
868
865
|
await handleCmdCommand(logger, chatId, verbose);
|
|
869
866
|
return;
|
|
870
867
|
}
|
|
871
|
-
if (lower ===
|
|
868
|
+
if (lower === '/task') {
|
|
872
869
|
await handleTaskCommand(logger, chatId);
|
|
873
870
|
return;
|
|
874
871
|
}
|
|
875
|
-
if (lower ===
|
|
872
|
+
if (lower === '/stop') {
|
|
876
873
|
await handleStopCommand(logger, chatId);
|
|
877
874
|
return;
|
|
878
875
|
}
|
|
879
876
|
// If we are awaiting a prompt for a /cmd selection, treat this message as the prompt.
|
|
880
877
|
const pending = pendingPrompts.get(chatId);
|
|
881
|
-
if (pending && text && !text.startsWith(
|
|
882
|
-
if (pending.phase !==
|
|
883
|
-
await notify(logger,
|
|
878
|
+
if (pending && text && !text.startsWith('/')) {
|
|
879
|
+
if (pending.phase !== 'awaitPrompt') {
|
|
880
|
+
await notify(logger, '👉 Pick the remaining options on the wizard above first.', {
|
|
881
|
+
chatId,
|
|
882
|
+
});
|
|
884
883
|
return;
|
|
885
884
|
}
|
|
886
885
|
if (pending.sessionId) {
|
|
887
|
-
const
|
|
888
|
-
if (
|
|
886
|
+
const messages = await (0, agentClient_1.getSessionMessages)(logger, pending.sessionId);
|
|
887
|
+
if (messages === null) {
|
|
889
888
|
pendingPrompts.delete(chatId);
|
|
890
|
-
await notify(logger,
|
|
889
|
+
await notify(logger, '❌ Selected session no longer exists.', {
|
|
891
890
|
chatId,
|
|
892
891
|
});
|
|
893
892
|
return;
|
|
@@ -896,6 +895,6 @@ function setupMessageListener(logger, bot) {
|
|
|
896
895
|
await runAgentForChat(logger, chatId, pending, text);
|
|
897
896
|
return;
|
|
898
897
|
}
|
|
899
|
-
logger.info(
|
|
898
|
+
logger.info('Ignoring unknown message');
|
|
900
899
|
});
|
|
901
900
|
}
|