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