omnikey-cli 1.5.8 → 1.6.1

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.
@@ -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("Missing telegram bot token");
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("Telegram bot not initialized. Call initTelegram first.");
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("Missing chat ID");
24
+ throw new Error('Missing chat ID');
25
25
  }
26
- const parseMode = options.parseMode ?? "Markdown";
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("Failed to send Telegram message:", err);
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 = "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";
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 || "0", 10);
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, "$1")
62
- .replace(/\*([^*\n]+)\*/g, "$1")
63
- .replace(/`([^`\n]+)`/g, "$1")
64
- .replace(/[ \t]+\n/g, "\n")
65
- .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')
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, "&amp;").replace(/</g, "&lt;").replace(/>/g, "&gt;");
77
+ return s.replace(/&/g, '&amp;').replace(/</g, '&lt;').replace(/>/g, '&gt;');
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$/, ""))}</code></pre>`);
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("|").map((c) => c.trim());
104
+ return r.split('|').map((c) => c.trim());
105
105
  };
106
106
  const headerCells = splitRow(header);
107
107
  const bodyRows = body
108
- .split("\n")
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, "$1")
123
- .replace(/__([^_]+)__/g, "$1")
124
- .replace(/`([^`]+)`/g, "$1")
125
- .replace(/\[([^\]]+)\]\([^)]+\)/g, "$1");
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], " ")).join("");
135
- 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('─┼─');
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("\n"))}</pre>`)}\n`;
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, "<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>");
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, "<b>$1</b>");
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)((?:&gt; .*(?:\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(/^&gt; ?/, ""))
163
- .join("\n");
162
+ .map((l) => l.replace(/^&gt; ?/, ''))
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, "\n")
169
- .replace(/\n{3,}/g, "\n\n")
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 ? "\n\n" : "") + p;
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: "🆕 New session", callback_data: `${CB_SESSION}new` }],
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: "✕ Cancel", callback_data: CB_CANCEL }]);
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
- { text: "⏭ Skip instructions", callback_data: `${CB_INSTRUCTION}skip` },
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: "⏭ Skip project", callback_data: `${CB_PROJECT}skip` }]);
252
- 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 }]);
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
- "*Step 1 of 3 · Task instructions*",
260
- "",
261
- "_No saved templates. Skip to continue._",
262
- ].join("\n"),
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: "⏭ Skip instructions",
264
+ text: '⏭ Skip instructions',
267
265
  callback_data: `${CB_INSTRUCTION}skip`,
268
266
  },
269
267
  ],
270
- [{ text: "✕ Cancel", callback_data: CB_CANCEL }],
268
+ [{ text: '✕ Cancel', callback_data: CB_CANCEL }],
271
269
  ],
272
270
  };
273
271
  }
274
272
  return {
275
273
  text: [
276
- "*Step 1 of 3 · Task instructions*",
277
- "",
278
- "Pick a saved template to prepend to your prompt, or skip.",
279
- ].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'),
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
- : "✓ Instructions: _skipped_";
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: "⏭ Skip project", callback_data: `${CB_PROJECT}skip` }],
297
- [{ text: "✕ Cancel", callback_data: CB_CANCEL }],
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
- "*Step 3 of 3 · Prompt*",
301
+ '*Step 3 of 3 · Prompt*',
314
302
  state.chosenInstructionsHeading
315
303
  ? `✓ Instructions: *${state.chosenInstructionsHeading}*`
316
- : "✓ Instructions: _skipped_",
317
- state.chosenGroupName
318
- ? `✓ Project: *${state.chosenGroupName}*`
319
- : "✓ Project: _skipped_",
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("\n"),
325
- keyboard: [[{ text: "✕ Cancel", callback_data: CB_CANCEL }]],
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 === "selectInstruction"
317
+ const copy = state.phase === 'selectInstruction'
332
318
  ? renderInstructionStep(state)
333
- : state.phase === "selectProject"
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: "Markdown",
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: "Markdown",
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("Failed to edit wizard message; sending a new one", {
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: "Markdown",
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: "Markdown",
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("Bot not initialized");
383
- const verboseTag = verbose ? " · 🔍 _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: "Markdown",
374
+ parse_mode: 'Markdown',
389
375
  reply_markup: { inline_keyboard: buildSessionKeyboard(sessions) },
390
376
  });
391
- logger.info("Sent /cmd session picker", {
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("Failed to list recent sessions", {
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, "🗒️ No sessions found.", { chatId });
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 !== "assistant")
427
+ if (msg.role !== 'assistant')
442
428
  continue;
443
- const block = msg.blocks?.find((b) => b.kind === "finalAnswer");
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.`, { chatId });
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: "HTML",
451
+ parse_mode: 'HTML',
464
452
  disable_web_page_preview: true,
465
453
  });
466
454
  }
467
455
  catch (err) {
468
- logger.warn("HTML render failed for /task; falling back to plain", {
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("Failed to handle /task", { error: err.message });
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("Failed to load task templates", {
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("Failed to load project groups", {
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("Failed to load wizard data", {
492
+ logger.error('Failed to load wizard data', {
505
493
  error: err.message,
506
494
  });
507
495
  }
508
496
  const state = {
509
- phase: "selectInstruction",
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: "awaitPrompt",
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 ? " · 🔍 _verbose_" : ""}`,
540
- "",
541
- "Send your prompt as the next message.",
542
- ].join("\n"), {
543
- 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',
544
532
  reply_markup: {
545
- inline_keyboard: [[{ text: "✕ Cancel", callback_data: CB_CANCEL }]],
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: "Unauthorized" });
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, "✕ Cancelled.");
553
+ await finishWizardMessage(chatId, state, '✕ Cancelled.');
566
554
  }
567
- await bot.answerCallbackQuery(query.id, { text: "Cancelled" });
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 === "new") {
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: "Session no longer available",
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 !== "selectInstruction") {
595
- await bot.answerCallbackQuery(query.id, { text: "Step expired" });
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 === "skip") {
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: "Template not found" });
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("Failed to set default task template", {
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: "Failed to set default",
614
+ text: 'Failed to set default',
627
615
  });
628
616
  return;
629
617
  }
630
618
  }
631
- state.phase = "selectProject";
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 !== "selectProject") {
640
- await bot.answerCallbackQuery(query.id, { text: "Step expired" });
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 === "skip") {
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: "Project not found" });
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 = "awaitPrompt";
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, "⏳ 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 });
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
- "✅ *Session started*",
660
+ '✅ *Session started*',
673
661
  pending.chosenInstructionsHeading
674
662
  ? `📝 Instructions: *${pending.chosenInstructionsHeading}*`
675
- : "📝 Instructions: _none_",
676
- pending.chosenGroupName
677
- ? `📁 Project: *${pending.chosenGroupName}*`
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("\n");
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 ?? "(new)";
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, "🚀 Starting agent run… (send /stop to cancel)", {
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("Failed to forward block to telegram", {
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: "HTML",
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("HTML render failed; falling back to plain text", {
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("Plain-text fallback also failed", {
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 === "reasoning") {
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 === "finalAnswer") {
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 "shellCommand": {
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 "terminalOutput": {
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 "webCall": {
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 "mcpCall": {
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 "imageRendering": {
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("Agent run completed", {
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("Agent run stopped by user", {
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}\`).`, { chatId });
807
+ await notify(logger, `🛑 Agent run stopped by user (session \`${state.sessionId}\`).`, {
808
+ chatId,
809
+ });
822
810
  }
823
811
  else {
824
- logger.error("Agent run failed", { error: err.message });
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, "🛑 No agent session is currently running.", {
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, "⏳ Stop already requested — waiting for shutdown…", {
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("User requested agent stop", {
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("callback_query", (q) => {
846
+ bot.on('callback_query', (q) => {
859
847
  void handleCallbackQuery(logger, q).catch((err) => {
860
- logger.error("callback_query handler crashed", {
848
+ logger.error('callback_query handler crashed', {
861
849
  error: err.message,
862
850
  });
863
851
  });
864
852
  });
865
- bot.on("message", async (msg) => {
853
+ bot.on('message', async (msg) => {
866
854
  const chatId = msg.chat.id;
867
855
  if (!isAuthorizedChat(chatId)) {
868
- logger.warn("Received message from unauthorized chat ID:", chatId);
856
+ logger.warn('Received message from unauthorized chat ID:', chatId);
869
857
  return;
870
858
  }
871
- const text = (msg.text || "").trim();
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 === "/cmd" || lower.startsWith("/cmd ")) {
875
- const args = text
876
- .slice("/cmd".length)
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 === "/task") {
868
+ if (lower === '/task') {
885
869
  await handleTaskCommand(logger, chatId);
886
870
  return;
887
871
  }
888
- if (lower === "/stop") {
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 !== "awaitPrompt") {
896
- 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
+ });
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, "❌ Selected session no longer exists.", {
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("Ignoring unknown message");
898
+ logger.info('Ignoring unknown message');
913
899
  });
914
900
  }