talon-agent 1.0.0 → 1.2.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.
Files changed (88) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +1 -0
  3. package/package.json +15 -11
  4. package/prompts/dream.md +7 -3
  5. package/prompts/heartbeat.md +30 -0
  6. package/prompts/identity.md +1 -0
  7. package/prompts/teams.md +3 -0
  8. package/prompts/telegram.md +1 -0
  9. package/src/__tests__/chat-settings.test.ts +108 -2
  10. package/src/__tests__/cleanup-registry.test.ts +58 -0
  11. package/src/__tests__/config.test.ts +118 -52
  12. package/src/__tests__/cron-store-extended.test.ts +661 -0
  13. package/src/__tests__/cron-store.test.ts +145 -11
  14. package/src/__tests__/daily-log.test.ts +224 -13
  15. package/src/__tests__/dispatcher.test.ts +424 -23
  16. package/src/__tests__/dream.test.ts +1028 -0
  17. package/src/__tests__/errors-extended.test.ts +428 -0
  18. package/src/__tests__/errors.test.ts +95 -3
  19. package/src/__tests__/fuzz.test.ts +87 -15
  20. package/src/__tests__/gateway-actions.test.ts +1174 -433
  21. package/src/__tests__/gateway-http.test.ts +210 -19
  22. package/src/__tests__/gateway-retry.test.ts +359 -0
  23. package/src/__tests__/gateway-withRetry-extended.test.ts +343 -0
  24. package/src/__tests__/graph.test.ts +830 -0
  25. package/src/__tests__/handlers-stream.test.ts +208 -0
  26. package/src/__tests__/handlers.test.ts +2539 -70
  27. package/src/__tests__/heartbeat.test.ts +364 -0
  28. package/src/__tests__/history-extended.test.ts +775 -0
  29. package/src/__tests__/history-persistence.test.ts +74 -19
  30. package/src/__tests__/history.test.ts +113 -79
  31. package/src/__tests__/integration.test.ts +43 -8
  32. package/src/__tests__/log-init.test.ts +129 -0
  33. package/src/__tests__/log.test.ts +23 -5
  34. package/src/__tests__/media-index.test.ts +317 -35
  35. package/src/__tests__/plugin.test.ts +314 -0
  36. package/src/__tests__/prompt-builder-extended.test.ts +296 -0
  37. package/src/__tests__/prompt-builder.test.ts +44 -9
  38. package/src/__tests__/sessions.test.ts +258 -4
  39. package/src/__tests__/storage-save-errors.test.ts +342 -0
  40. package/src/__tests__/teams-frontend.test.ts +526 -31
  41. package/src/__tests__/telegram-formatting.test.ts +82 -0
  42. package/src/__tests__/terminal-commands.test.ts +208 -1
  43. package/src/__tests__/terminal-renderer.test.ts +223 -0
  44. package/src/__tests__/time.test.ts +107 -0
  45. package/src/__tests__/workspace-migrate.test.ts +256 -0
  46. package/src/__tests__/workspace.test.ts +63 -1
  47. package/src/backend/claude-sdk/tools.ts +64 -18
  48. package/src/bootstrap.ts +14 -14
  49. package/src/cli.ts +440 -125
  50. package/src/core/cron.ts +20 -5
  51. package/src/core/dispatcher.ts +27 -9
  52. package/src/core/dream.ts +79 -24
  53. package/src/core/errors.ts +12 -2
  54. package/src/core/gateway-actions.ts +182 -46
  55. package/src/core/gateway.ts +93 -41
  56. package/src/core/heartbeat.ts +515 -0
  57. package/src/core/plugin.ts +1 -1
  58. package/src/core/prompt-builder.ts +1 -4
  59. package/src/core/pulse.ts +4 -3
  60. package/src/frontend/teams/actions.ts +3 -1
  61. package/src/frontend/teams/formatting.ts +47 -8
  62. package/src/frontend/teams/graph.ts +35 -11
  63. package/src/frontend/teams/index.ts +155 -57
  64. package/src/frontend/teams/tools.ts +4 -6
  65. package/src/frontend/telegram/actions.ts +358 -82
  66. package/src/frontend/telegram/admin.ts +162 -72
  67. package/src/frontend/telegram/callbacks.ts +16 -10
  68. package/src/frontend/telegram/commands.ts +37 -21
  69. package/src/frontend/telegram/formatting.ts +2 -4
  70. package/src/frontend/telegram/handlers.ts +262 -66
  71. package/src/frontend/telegram/index.ts +39 -14
  72. package/src/frontend/telegram/middleware.ts +14 -4
  73. package/src/frontend/telegram/userbot.ts +16 -4
  74. package/src/frontend/terminal/renderer.ts +1 -4
  75. package/src/index.ts +28 -4
  76. package/src/storage/chat-settings.ts +32 -9
  77. package/src/storage/cron-store.ts +53 -11
  78. package/src/storage/daily-log.ts +72 -19
  79. package/src/storage/history.ts +39 -21
  80. package/src/storage/media-index.ts +37 -12
  81. package/src/storage/sessions.ts +3 -2
  82. package/src/util/cleanup-registry.ts +34 -0
  83. package/src/util/config.ts +85 -23
  84. package/src/util/log.ts +47 -17
  85. package/src/util/paths.ts +10 -0
  86. package/src/util/time.ts +29 -6
  87. package/src/util/watchdog.ts +5 -1
  88. package/src/util/workspace.ts +51 -10
@@ -74,7 +74,9 @@ function loadTokens(): StoredTokens | null {
74
74
  if (existsSync(TOKEN_FILE)) {
75
75
  return JSON.parse(readFileSync(TOKEN_FILE, "utf-8"));
76
76
  }
77
- } catch { /* corrupt */ }
77
+ } catch {
78
+ /* corrupt */
79
+ }
78
80
  return null;
79
81
  }
80
82
 
@@ -85,7 +87,10 @@ function saveTokens(tokens: StoredTokens): void {
85
87
 
86
88
  // ── OAuth helpers ────────────────────────────────────────────────────────────
87
89
 
88
- async function postForm(url: string, params: Record<string, string>): Promise<Record<string, unknown>> {
90
+ async function postForm(
91
+ url: string,
92
+ params: Record<string, string>,
93
+ ): Promise<Record<string, unknown>> {
89
94
  const body = new URLSearchParams(params).toString();
90
95
  const resp = await proxyFetch(url, {
91
96
  method: "POST",
@@ -96,7 +101,9 @@ async function postForm(url: string, params: Record<string, string>): Promise<Re
96
101
  return (await resp.json()) as Record<string, unknown>;
97
102
  }
98
103
 
99
- async function refreshAccessToken(refreshToken: string): Promise<StoredTokens | null> {
104
+ async function refreshAccessToken(
105
+ refreshToken: string,
106
+ ): Promise<StoredTokens | null> {
100
107
  const data = (await postForm(`${AUTH_BASE}/token`, {
101
108
  grant_type: "refresh_token",
102
109
  client_id: CLIENT_ID,
@@ -105,7 +112,10 @@ async function refreshAccessToken(refreshToken: string): Promise<StoredTokens |
105
112
  })) as TokenResponse;
106
113
 
107
114
  if (data.error) {
108
- logError("teams", `Token refresh failed: ${data.error_description || data.error}`);
115
+ logError(
116
+ "teams",
117
+ `Token refresh failed: ${data.error_description || data.error}`,
118
+ );
109
119
  return null;
110
120
  }
111
121
 
@@ -129,7 +139,10 @@ export async function deviceCodeAuth(): Promise<StoredTokens> {
129
139
  console.log(` To sign in, open: ${dcResp.verification_uri}`);
130
140
  console.log(` Enter code: ${dcResp.user_code}`);
131
141
  console.log();
132
- log("teams", `Device code auth: go to ${dcResp.verification_uri} and enter ${dcResp.user_code}`);
142
+ log(
143
+ "teams",
144
+ `Device code auth: go to ${dcResp.verification_uri} and enter ${dcResp.user_code}`,
145
+ );
133
146
 
134
147
  // Poll for token
135
148
  const pollInterval = (dcResp.interval || 5) * 1000;
@@ -161,7 +174,9 @@ export async function deviceCodeAuth(): Promise<StoredTokens> {
161
174
  continue;
162
175
  }
163
176
 
164
- throw new Error(`Auth failed: ${tokenResp.error_description || tokenResp.error}`);
177
+ throw new Error(
178
+ `Auth failed: ${tokenResp.error_description || tokenResp.error}`,
179
+ );
165
180
  }
166
181
 
167
182
  throw new Error("Device code auth timed out (15 minutes)");
@@ -183,7 +198,8 @@ export class GraphClient {
183
198
 
184
199
  log("teams", "Refreshing access token...");
185
200
  const refreshed = await refreshAccessToken(this.tokens.refreshToken);
186
- if (!refreshed) throw new Error("Token refresh failed — re-authentication needed");
201
+ if (!refreshed)
202
+ throw new Error("Token refresh failed — re-authentication needed");
187
203
 
188
204
  this.tokens = { ...this.tokens, ...refreshed };
189
205
  saveTokens(this.tokens);
@@ -214,7 +230,9 @@ export class GraphClient {
214
230
 
215
231
  // ── Chat discovery ─────────────────────────────────────────────────────
216
232
 
217
- async listChats(): Promise<Array<{ id: string; topic: string | null; chatType: string }>> {
233
+ async listChats(): Promise<
234
+ Array<{ id: string; topic: string | null; chatType: string }>
235
+ > {
218
236
  const data = await this.graphGet("/me/chats?$top=50");
219
237
  const chats = data.value as Array<Record<string, unknown>>;
220
238
  return chats.map((c) => ({
@@ -259,9 +277,15 @@ export class GraphClient {
259
277
 
260
278
  // ── Stored config ──────────────────────────────────────────────────────
261
279
 
262
- getStoredChatId(): string | undefined { return this.tokens.chatId; }
263
- getStoredChatTopic(): string | undefined { return this.tokens.chatTopic; }
264
- getStoredUserId(): string | undefined { return this.tokens.userId; }
280
+ getStoredChatId(): string | undefined {
281
+ return this.tokens.chatId;
282
+ }
283
+ getStoredChatTopic(): string | undefined {
284
+ return this.tokens.chatTopic;
285
+ }
286
+ getStoredUserId(): string | undefined {
287
+ return this.tokens.userId;
288
+ }
265
289
 
266
290
  saveChatConfig(chatId: string, chatTopic: string, userId: string): void {
267
291
  this.tokens.chatId = chatId;
@@ -15,7 +15,11 @@ import { log, logError } from "../../util/log.js";
15
15
  import { deriveNumericChatId } from "../../util/chat-id.js";
16
16
  import { createTeamsActionHandler } from "./actions.js";
17
17
  import { splitTeamsMessage, buildAdaptiveCard } from "./formatting.js";
18
- import { initGraphClient, type GraphClient, type ChatMessage } from "./graph.js";
18
+ import {
19
+ initGraphClient,
20
+ type GraphClient,
21
+ type ChatMessage,
22
+ } from "./graph.js";
19
23
  import { proxyFetch } from "./proxy-fetch.js";
20
24
 
21
25
  // ── Types ────────────────────────────────────────────────────────────────────
@@ -36,10 +40,14 @@ export function createTeamsFrontend(
36
40
  config: TalonConfig,
37
41
  gateway: Gateway,
38
42
  ): TeamsFrontend {
39
- const webhookUrl = (config as Record<string, unknown>).teamsWebhookUrl as string;
40
- const botDisplayName = ((config as Record<string, unknown>).teamsBotDisplayName as string) || "";
41
- const pollIntervalMs = ((config as Record<string, unknown>).teamsGraphPollMs as number) || 10_000;
42
- const configChatTopic = ((config as Record<string, unknown>).teamsChatTopic as string) || "";
43
+ const webhookUrl = (config as Record<string, unknown>)
44
+ .teamsWebhookUrl as string;
45
+ const botDisplayName =
46
+ ((config as Record<string, unknown>).teamsBotDisplayName as string) || "";
47
+ const pollIntervalMs =
48
+ ((config as Record<string, unknown>).teamsGraphPollMs as number) || 10_000;
49
+ const configChatTopic =
50
+ ((config as Record<string, unknown>).teamsChatTopic as string) || "";
43
51
 
44
52
  let graphClient: GraphClient | null = null;
45
53
  let pollTimer: ReturnType<typeof setInterval> | null = null;
@@ -48,7 +56,8 @@ export function createTeamsFrontend(
48
56
  let polling = false;
49
57
 
50
58
  const context: ContextManager = {
51
- acquire: (chatId: number, stringId?: string) => gateway.setContext(chatId, stringId),
59
+ acquire: (chatId: number, stringId?: string) =>
60
+ gateway.setContext(chatId, stringId),
52
61
  release: (chatId: number) => gateway.clearContext(chatId),
53
62
  getMessageCount: (chatId: number) => gateway.getMessageCount(chatId),
54
63
  };
@@ -73,7 +82,10 @@ export function createTeamsFrontend(
73
82
  });
74
83
  }
75
84
  } catch (err) {
76
- logError("teams", `sendMessage failed: ${err instanceof Error ? err.message : err}`);
85
+ logError(
86
+ "teams",
87
+ `sendMessage failed: ${err instanceof Error ? err.message : err}`,
88
+ );
77
89
  }
78
90
  },
79
91
 
@@ -110,7 +122,11 @@ export function createTeamsFrontend(
110
122
  c.topic?.toLowerCase().includes(configChatTopic.toLowerCase()),
111
123
  );
112
124
  if (match) selectedChat = match;
113
- else log("teams", `No chat matching topic "${configChatTopic}", using most recent`);
125
+ else
126
+ log(
127
+ "teams",
128
+ `No chat matching topic "${configChatTopic}", using most recent`,
129
+ );
114
130
  }
115
131
 
116
132
  chatId = selectedChat.id;
@@ -119,7 +135,10 @@ export function createTeamsFrontend(
119
135
  graphClient.saveChatConfig(chatId, topic, myUserId);
120
136
  log("teams", `Configured chat: ${topic} [${selectedChat.chatType}]`);
121
137
  } else {
122
- log("teams", `Using chat: ${graphClient.getStoredChatTopic() || chatId}`);
138
+ log(
139
+ "teams",
140
+ `Using chat: ${graphClient.getStoredChatTopic() || chatId}`,
141
+ );
123
142
  }
124
143
 
125
144
  // Seed lastSeenMessageId from current messages (don't process old messages)
@@ -130,7 +149,10 @@ export function createTeamsFrontend(
130
149
  log("teams", `Seeded last message ID: ${lastSeenMessageId}`);
131
150
  }
132
151
  } catch (err) {
133
- logError("teams", `Failed to seed messages: ${err instanceof Error ? err.message : err}`);
152
+ logError(
153
+ "teams",
154
+ `Failed to seed messages: ${err instanceof Error ? err.message : err}`,
155
+ );
134
156
  }
135
157
  },
136
158
 
@@ -142,7 +164,10 @@ export function createTeamsFrontend(
142
164
 
143
165
  log("teams", "Teams frontend running");
144
166
  log("teams", `Send: Power Automate webhook`);
145
- log("teams", `Receive: Graph API chat polling every ${pollIntervalMs / 1000}s`);
167
+ log(
168
+ "teams",
169
+ `Receive: Graph API chat polling every ${pollIntervalMs / 1000}s`,
170
+ );
146
171
 
147
172
  // ── Poll loop ──────────────────────────────────────────────────────
148
173
  async function poll(): Promise<void> {
@@ -173,7 +198,10 @@ export function createTeamsFrontend(
173
198
  // Skip bot/workflow messages by display name (echo loop prevention).
174
199
  // We do NOT filter by user ID — the authenticated user also sends
175
200
  // real messages that Talon should respond to.
176
- if (botDisplayName && msg.senderName.toLowerCase() === botDisplayName.toLowerCase()) {
201
+ if (
202
+ botDisplayName &&
203
+ msg.senderName.toLowerCase() === botDisplayName.toLowerCase()
204
+ ) {
177
205
  continue;
178
206
  }
179
207
 
@@ -183,7 +211,8 @@ export function createTeamsFrontend(
183
211
  // ── Slash commands ──
184
212
  const trimmed = msg.text.trim().toLowerCase();
185
213
  if (trimmed === "/reset") {
186
- const { resetSession } = await import("../../storage/sessions.js");
214
+ const { resetSession } =
215
+ await import("../../storage/sessions.js");
187
216
  const { clearHistory } = await import("../../storage/history.js");
188
217
  resetSession(talonChatId);
189
218
  clearHistory(talonChatId);
@@ -198,40 +227,85 @@ export function createTeamsFrontend(
198
227
  continue;
199
228
  }
200
229
  if (trimmed === "/status") {
201
- const { getSessionInfo } = await import("../../storage/sessions.js");
230
+ const { getSessionInfo } =
231
+ await import("../../storage/sessions.js");
202
232
  const info = getSessionInfo(talonChatId);
203
233
  const u = info.usage;
204
- const cacheHit = (u.totalInputTokens + u.totalCacheRead) > 0
205
- ? Math.round((u.totalCacheRead / (u.totalInputTokens + u.totalCacheRead)) * 100) : 0;
206
- const { getChatSettings } = await import("../../storage/chat-settings.js");
207
- const model = (getChatSettings(talonChatId).model ?? config.model as string).replace("claude-", "");
208
- const avgMs = info.turns > 0 ? Math.round(u.totalResponseMs / info.turns) : 0;
234
+ const cacheHit =
235
+ u.totalInputTokens + u.totalCacheRead > 0
236
+ ? Math.round(
237
+ (u.totalCacheRead /
238
+ (u.totalInputTokens + u.totalCacheRead)) *
239
+ 100,
240
+ )
241
+ : 0;
242
+ const { getChatSettings } =
243
+ await import("../../storage/chat-settings.js");
244
+ const model = (
245
+ getChatSettings(talonChatId).model ?? (config.model as string)
246
+ ).replace("claude-", "");
247
+ const avgMs =
248
+ info.turns > 0 ? Math.round(u.totalResponseMs / info.turns) : 0;
209
249
  const contextUsed = u.lastPromptTokens;
210
- const contextMax = model.includes("opus") ? 1_000_000 : model.includes("sonnet") ? 1_000_000 : 200_000;
211
- const contextPct = contextMax > 0 ? Math.round((contextUsed / contextMax) * 100) : 0;
250
+ const contextMax = model.includes("opus")
251
+ ? 1_000_000
252
+ : model.includes("sonnet")
253
+ ? 1_000_000
254
+ : 200_000;
255
+ const contextPct =
256
+ contextMax > 0
257
+ ? Math.round((contextUsed / contextMax) * 100)
258
+ : 0;
212
259
  const card = {
213
260
  type: "message",
214
- attachments: [{
215
- contentType: "application/vnd.microsoft.card.adaptive",
216
- contentUrl: null,
217
- content: {
218
- type: "AdaptiveCard",
219
- $schema: "http://adaptivecards.io/schemas/adaptive-card.json",
220
- version: "1.4",
221
- body: [
222
- { type: "TextBlock", text: "**Session**", wrap: true, size: "Medium", weight: "Bolder" },
223
- { type: "FactSet", facts: [
224
- { title: "Model", value: model },
225
- { title: "Turns", value: String(info.turns) },
226
- { title: "Context", value: `${(contextUsed / 1000).toFixed(0)}K / ${(contextMax / 1000).toFixed(0)}K (${contextPct}%)` },
227
- { title: "Cache", value: `${cacheHit}% hit` },
228
- { title: "Input", value: `${u.totalInputTokens.toLocaleString()} tokens` },
229
- { title: "Output", value: `${u.totalOutputTokens.toLocaleString()} tokens` },
230
- { title: "Avg response", value: avgMs > 0 ? `${(avgMs / 1000).toFixed(1)}s` : "—" },
231
- ]},
232
- ],
261
+ attachments: [
262
+ {
263
+ contentType: "application/vnd.microsoft.card.adaptive",
264
+ contentUrl: null,
265
+ content: {
266
+ type: "AdaptiveCard",
267
+ $schema:
268
+ "http://adaptivecards.io/schemas/adaptive-card.json",
269
+ version: "1.4",
270
+ body: [
271
+ {
272
+ type: "TextBlock",
273
+ text: "**Session**",
274
+ wrap: true,
275
+ size: "Medium",
276
+ weight: "Bolder",
277
+ },
278
+ {
279
+ type: "FactSet",
280
+ facts: [
281
+ { title: "Model", value: model },
282
+ { title: "Turns", value: String(info.turns) },
283
+ {
284
+ title: "Context",
285
+ value: `${(contextUsed / 1000).toFixed(0)}K / ${(contextMax / 1000).toFixed(0)}K (${contextPct}%)`,
286
+ },
287
+ { title: "Cache", value: `${cacheHit}% hit` },
288
+ {
289
+ title: "Input",
290
+ value: `${u.totalInputTokens.toLocaleString()} tokens`,
291
+ },
292
+ {
293
+ title: "Output",
294
+ value: `${u.totalOutputTokens.toLocaleString()} tokens`,
295
+ },
296
+ {
297
+ title: "Avg response",
298
+ value:
299
+ avgMs > 0
300
+ ? `${(avgMs / 1000).toFixed(1)}s`
301
+ : "—",
302
+ },
303
+ ],
304
+ },
305
+ ],
306
+ },
233
307
  },
234
- }],
308
+ ],
235
309
  };
236
310
  await proxyFetch(webhookUrl, {
237
311
  method: "POST",
@@ -242,7 +316,8 @@ export function createTeamsFrontend(
242
316
  continue;
243
317
  }
244
318
  if (trimmed === "/help") {
245
- const helpText = "**Commands:**\n- `/reset` — clear session & history\n- `/status` — session stats\n- `/help` — this message";
319
+ const helpText =
320
+ "**Commands:**\n- `/reset` — clear session & history\n- `/status` — session stats\n- `/help` — this message";
246
321
  const card = buildAdaptiveCard(helpText);
247
322
  await proxyFetch(webhookUrl, {
248
323
  method: "POST",
@@ -253,7 +328,10 @@ export function createTeamsFrontend(
253
328
  continue;
254
329
  }
255
330
 
256
- log("teams", `[${msg.senderName}]: ${msg.text.slice(0, 80)}${msg.text.length > 80 ? "..." : ""}`);
331
+ log(
332
+ "teams",
333
+ `[${msg.senderName}]: ${msg.text.slice(0, 80)}${msg.text.length > 80 ? "..." : ""}`,
334
+ );
257
335
 
258
336
  execute({
259
337
  chatId: talonChatId,
@@ -266,23 +344,43 @@ export function createTeamsFrontend(
266
344
  if (phase) log("teams", ` phase: ${phase}`);
267
345
  },
268
346
  onToolUse: (toolName, input) => {
269
- const detail = (input.description ?? input.command ?? input.action ?? input.query ?? input.url ?? input.name ?? "") as string;
270
- log("teams", ` tool: ${toolName}${detail ? ` — ${String(detail).slice(0, 100)}` : ""}`);
347
+ const detail = (input.description ??
348
+ input.command ??
349
+ input.action ??
350
+ input.query ??
351
+ input.url ??
352
+ input.name ??
353
+ "") as string;
354
+ log(
355
+ "teams",
356
+ ` tool: ${toolName}${detail ? ` — ${String(detail).slice(0, 100)}` : ""}`,
357
+ );
271
358
  },
272
- }).then(async (result) => {
273
- // Only deliver messages sent via the send_message tool.
274
- // Do NOT send fallback text if Claude chose not to use send_message,
275
- // it's either choosing not to respond or outputting internal reasoning
276
- // that shouldn't be shown to users.
277
- if (result.bridgeMessageCount === 0 && result.text?.trim()) {
278
- log("teams", `Suppressed fallback text (${result.text.length} chars) — no send_message tool used`);
279
- }
280
- }).catch((err) => {
281
- logError("teams", `execute failed: ${err instanceof Error ? err.message : err}`);
282
- });
359
+ })
360
+ .then(async (result) => {
361
+ // Only deliver messages sent via the send_message tool.
362
+ // Do NOT send fallback text if Claude chose not to use send_message,
363
+ // it's either choosing not to respond or outputting internal reasoning
364
+ // that shouldn't be shown to users.
365
+ if (result.bridgeMessageCount === 0 && result.text?.trim()) {
366
+ log(
367
+ "teams",
368
+ `Suppressed fallback text (${result.text.length} chars) no send_message tool used`,
369
+ );
370
+ }
371
+ })
372
+ .catch((err) => {
373
+ logError(
374
+ "teams",
375
+ `execute failed: ${err instanceof Error ? err.message : err}`,
376
+ );
377
+ });
283
378
  }
284
379
  } catch (err) {
285
- logError("teams", `Poll error: ${err instanceof Error ? err.message : err}`);
380
+ logError(
381
+ "teams",
382
+ `Poll error: ${err instanceof Error ? err.message : err}`,
383
+ );
286
384
  } finally {
287
385
  polling = false;
288
386
  }
@@ -77,16 +77,14 @@ Example: send_message_with_buttons(text="Choose:", rows=[[{"text":"Docs","url":"
77
77
  )
78
78
  .describe("Button rows"),
79
79
  },
80
- async (params) => textResult(await callBridge("send_message_with_buttons", params)),
80
+ async (params) =>
81
+ textResult(await callBridge("send_message_with_buttons", params)),
81
82
  );
82
83
 
83
84
  // ── Chat info ────────────────────────────────────────────────────────────────
84
85
 
85
- server.tool(
86
- "get_chat_info",
87
- "Get info about the current chat.",
88
- {},
89
- async () => textResult(await callBridge("get_chat_info", {})),
86
+ server.tool("get_chat_info", "Get info about the current chat.", {}, async () =>
87
+ textResult(await callBridge("get_chat_info", {})),
90
88
  );
91
89
 
92
90
  // ── Web tools ────────────────────────────────────────────────────────────────