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.
@@ -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("Missing telegram bot token");
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("Telegram bot not initialized. Call initTelegram first.");
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("Missing chat ID");
24
+ throw new Error('Missing chat ID');
26
25
  }
27
- const parseMode = options.parseMode ?? "Markdown";
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("Failed to send Telegram message:", err);
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 = "s:"; // session picker; "s:new" or "s:<idx>"
41
- const CB_INSTRUCTION = "t:"; // instruction picker; "t:skip" or "t:<idx>"
42
- const CB_PROJECT = "g:"; // project picker; "g:skip" or "g:<idx>"
43
- const CB_CANCEL = "x: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 || "0", 10);
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, "$1")
63
- .replace(/\*([^*\n]+)\*/g, "$1")
64
- .replace(/`([^`\n]+)`/g, "$1")
65
- .replace(/[ \t]+\n/g, "\n")
66
- .replace(/\n{3,}/g, "\n\n")
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, "&amp;").replace(/</g, "&lt;").replace(/>/g, "&gt;");
77
+ return s.replace(/&/g, '&amp;').replace(/</g, '&lt;').replace(/>/g, '&gt;');
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$/, ""))}</code></pre>`);
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("|").map((c) => c.trim());
104
+ return r.split('|').map((c) => c.trim());
106
105
  };
107
106
  const headerCells = splitRow(header);
108
107
  const bodyRows = body
109
- .split("\n")
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, "$1")
124
- .replace(/__([^_]+)__/g, "$1")
125
- .replace(/`([^`]+)`/g, "$1")
126
- .replace(/\[([^\]]+)\]\([^)]+\)/g, "$1");
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], " ")).join("");
136
- const sep = widths.map((w) => "".repeat(w)).join("─┼─");
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("\n"))}</pre>`)}\n`;
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, "<b>$1</b>");
150
- text = text.replace(/__([^\n_]+)__/g, "<b>$1</b>");
151
- text = text.replace(/(^|[^*])\*([^\n*]+)\*(?!\*)/g, "$1<i>$2</i>");
152
- text = text.replace(/(^|[^_])_([^\n_]+)_(?!_)/g, "$1<i>$2</i>");
153
- text = text.replace(/~~([^\n~]+)~~/g, "<s>$1</s>");
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, "<b>$1</b>");
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)((?:&gt; .*(?:\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(/^&gt; ?/, ""))
164
- .join("\n");
162
+ .map((l) => l.replace(/^&gt; ?/, ''))
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, "\n")
170
- .replace(/\n{3,}/g, "\n\n")
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 ? "\n\n" : "") + p;
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: "🆕 New session", callback_data: `${CB_SESSION}new` }],
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: "✕ Cancel", callback_data: CB_CANCEL }]);
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
- { text: "⏭ Skip instructions", callback_data: `${CB_INSTRUCTION}skip` },
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: "⏭ Skip project", callback_data: `${CB_PROJECT}skip` }]);
253
- rows.push([{ text: "✕ Cancel", callback_data: CB_CANCEL }]);
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
- "*Step 1 of 3 · Task instructions*",
261
- "",
262
- "_No saved templates. Skip to continue._",
263
- ].join("\n"),
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: "⏭ Skip instructions",
264
+ text: '⏭ Skip instructions',
268
265
  callback_data: `${CB_INSTRUCTION}skip`,
269
266
  },
270
267
  ],
271
- [{ text: "✕ Cancel", callback_data: CB_CANCEL }],
268
+ [{ text: '✕ Cancel', callback_data: CB_CANCEL }],
272
269
  ],
273
270
  };
274
271
  }
275
272
  return {
276
273
  text: [
277
- "*Step 1 of 3 · Task instructions*",
278
- "",
279
- "Pick a saved template to prepend to your prompt, or skip.",
280
- ].join("\n"),
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
- : "✓ Instructions: _skipped_";
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: "⏭ Skip project", callback_data: `${CB_PROJECT}skip` }],
298
- [{ text: "✕ Cancel", callback_data: CB_CANCEL }],
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
- "*Step 3 of 3 · Prompt*",
301
+ '*Step 3 of 3 · Prompt*',
315
302
  state.chosenInstructionsHeading
316
303
  ? `✓ Instructions: *${state.chosenInstructionsHeading}*`
317
- : "✓ Instructions: _skipped_",
318
- state.chosenGroupName
319
- ? `✓ Project: *${state.chosenGroupName}*`
320
- : "✓ Project: _skipped_",
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("\n"),
326
- keyboard: [[{ text: "✕ Cancel", callback_data: CB_CANCEL }]],
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 === "selectInstruction"
317
+ const copy = state.phase === 'selectInstruction'
333
318
  ? renderInstructionStep(state)
334
- : state.phase === "selectProject"
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: "Markdown",
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: "Markdown",
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("Failed to edit wizard message; sending a new one", {
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: "Markdown",
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: "Markdown",
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("Bot not initialized");
384
- const verboseTag = verbose ? " · 🔍 _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: "Markdown",
374
+ parse_mode: 'Markdown',
390
375
  reply_markup: { inline_keyboard: buildSessionKeyboard(sessions) },
391
376
  });
392
- logger.info("Sent /cmd session picker", {
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("Failed to list recent sessions", {
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 session = (0, db_1.getMostRecentSession)();
416
+ const sessions = await (0, agentClient_1.listRecentSessions)(logger, 1);
417
+ const session = sessions[0];
432
418
  if (!session) {
433
- await notify(logger, "🗒️ No sessions found.", { chatId });
419
+ await notify(logger, '🗒️ No sessions found.', { chatId });
434
420
  return;
435
421
  }
436
- const finalAnswer = (0, agentClient_1.extractFinalAnswerFromHistory)(session.historyJson);
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.`, { chatId });
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: "HTML",
451
+ parse_mode: 'HTML',
451
452
  disable_web_page_preview: true,
452
453
  });
453
454
  }
454
455
  catch (err) {
455
- logger.warn("HTML render failed for /task; falling back to plain", {
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("Failed to handle /task", { error: err.message });
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("Failed to load task templates", {
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("Failed to load project groups", {
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("Failed to load wizard data", {
492
+ logger.error('Failed to load wizard data', {
492
493
  error: err.message,
493
494
  });
494
495
  }
495
496
  const state = {
496
- phase: "selectInstruction",
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: "awaitPrompt",
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 ? " · 🔍 _verbose_" : ""}`,
527
- "",
528
- "Send your prompt as the next message.",
529
- ].join("\n"), {
530
- parse_mode: "Markdown",
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: "✕ Cancel", callback_data: CB_CANCEL }]],
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: "Unauthorized" });
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, "✕ Cancelled.");
553
+ await finishWizardMessage(chatId, state, '✕ Cancelled.');
553
554
  }
554
- await bot.answerCallbackQuery(query.id, { text: "Cancelled" });
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 === "new") {
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: "Session no longer available",
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 !== "selectInstruction") {
582
- await bot.answerCallbackQuery(query.id, { text: "Step expired" });
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 === "skip") {
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: "Template not found" });
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("Failed to set default task template", {
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: "Failed to set default",
614
+ text: 'Failed to set default',
614
615
  });
615
616
  return;
616
617
  }
617
618
  }
618
- state.phase = "selectProject";
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 !== "selectProject") {
627
- await bot.answerCallbackQuery(query.id, { text: "Step expired" });
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 === "skip") {
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: "Project not found" });
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 = "awaitPrompt";
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, "⏳ A session is already running. Wait for it to finish.", { chatId });
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
- "✅ *Session started*",
660
+ '✅ *Session started*',
660
661
  pending.chosenInstructionsHeading
661
662
  ? `📝 Instructions: *${pending.chosenInstructionsHeading}*`
662
- : "📝 Instructions: _none_",
663
- pending.chosenGroupName
664
- ? `📁 Project: *${pending.chosenGroupName}*`
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("\n");
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 ?? "(new)";
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, "🚀 Starting agent run… (send /stop to cancel)", {
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("Failed to forward block to telegram", {
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: "HTML",
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("HTML render failed; falling back to plain text", {
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("Plain-text fallback also failed", {
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 === "reasoning") {
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 === "finalAnswer") {
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 "shellCommand": {
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 "terminalOutput": {
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 "webCall": {
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 "mcpCall": {
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 "imageRendering": {
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("Agent run completed", {
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("Agent run stopped by user", {
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}\`).`, { chatId });
807
+ await notify(logger, `🛑 Agent run stopped by user (session \`${state.sessionId}\`).`, {
808
+ chatId,
809
+ });
809
810
  }
810
811
  else {
811
- logger.error("Agent run failed", { error: err.message });
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, "🛑 No agent session is currently running.", {
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, "⏳ Stop already requested — waiting for shutdown…", {
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("User requested agent stop", {
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("callback_query", (q) => {
846
+ bot.on('callback_query', (q) => {
846
847
  void handleCallbackQuery(logger, q).catch((err) => {
847
- logger.error("callback_query handler crashed", {
848
+ logger.error('callback_query handler crashed', {
848
849
  error: err.message,
849
850
  });
850
851
  });
851
852
  });
852
- bot.on("message", async (msg) => {
853
+ bot.on('message', async (msg) => {
853
854
  const chatId = msg.chat.id;
854
855
  if (!isAuthorizedChat(chatId)) {
855
- logger.warn("Received message from unauthorized chat ID:", chatId);
856
+ logger.warn('Received message from unauthorized chat ID:', chatId);
856
857
  return;
857
858
  }
858
- const text = (msg.text || "").trim();
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 === "/cmd" || lower.startsWith("/cmd ")) {
862
- const args = text
863
- .slice("/cmd".length)
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 === "/task") {
868
+ if (lower === '/task') {
872
869
  await handleTaskCommand(logger, chatId);
873
870
  return;
874
871
  }
875
- if (lower === "/stop") {
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 !== "awaitPrompt") {
883
- await notify(logger, "👉 Pick the remaining options on the wizard above first.", { chatId });
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 exists = (0, db_1.getSessionById)(pending.sessionId);
888
- if (!exists) {
886
+ const messages = await (0, agentClient_1.getSessionMessages)(logger, pending.sessionId);
887
+ if (messages === null) {
889
888
  pendingPrompts.delete(chatId);
890
- await notify(logger, "❌ Selected session no longer exists.", {
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("Ignoring unknown message");
898
+ logger.info('Ignoring unknown message');
900
899
  });
901
900
  }