camelagi 0.5.39 → 0.5.41

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 (104) hide show
  1. package/README.md +58 -17
  2. package/dashboard/_next/static/chunks/172-a2787ae03f0db2b4.js +12 -0
  3. package/dashboard/_next/static/chunks/239-49a9ac3789f8c41c.js +1 -0
  4. package/dashboard/_next/static/chunks/255-102f2e5b2e3dc2ef.js +1 -0
  5. package/dashboard/_next/static/chunks/413-89d8e4554f461999.js +1 -0
  6. package/dashboard/_next/static/chunks/4bd1b696-c023c6e3521b1417.js +1 -0
  7. package/dashboard/_next/static/chunks/619-ba102abea3e3d0e4.js +1 -0
  8. package/dashboard/_next/static/chunks/909-aed3aa549e59d0fb.js +1 -0
  9. package/dashboard/_next/static/chunks/app/_not-found/page-121b856ce9a0ddcd.js +1 -0
  10. package/dashboard/_next/static/chunks/app/dashboard/agents/page-aa6e6fb2a1df7d63.js +1 -0
  11. package/dashboard/_next/static/chunks/app/dashboard/chat/page-feeb17fdc08b91e5.js +1 -0
  12. package/dashboard/_next/static/chunks/app/dashboard/config/page-afad9f4da82a343e.js +1 -0
  13. package/dashboard/_next/static/chunks/app/dashboard/layout-853ce5dfe3461735.js +1 -0
  14. package/dashboard/_next/static/chunks/app/dashboard/monitor/page-1b3d112c49b3a383.js +1 -0
  15. package/dashboard/_next/static/chunks/app/dashboard/page-b15605f3b21f7467.js +1 -0
  16. package/dashboard/_next/static/chunks/app/dashboard/sessions/page-e11d3f3e6ad99067.js +1 -0
  17. package/dashboard/_next/static/chunks/app/docs/[slug]/page-baf0632d98082d10.js +1 -0
  18. package/dashboard/_next/static/chunks/app/docs/page-5933496f46ff00ec.js +1 -0
  19. package/dashboard/_next/static/chunks/app/download/page-da533b5f543dfd66.js +1 -0
  20. package/dashboard/_next/static/chunks/app/layout-1112267c08c875b5.js +1 -0
  21. package/dashboard/_next/static/chunks/app/page-d69c17b39431e2c5.js +1 -0
  22. package/dashboard/_next/static/chunks/framework-de98b93a850cfc71.js +1 -0
  23. package/dashboard/_next/static/chunks/main-8d9a106db393efcf.js +1 -0
  24. package/dashboard/_next/static/chunks/main-app-e38cb42677e65e59.js +1 -0
  25. package/dashboard/_next/static/chunks/pages/_app-7d307437aca18ad4.js +1 -0
  26. package/dashboard/_next/static/chunks/pages/_error-cb2a52f75f2162e2.js +1 -0
  27. package/dashboard/_next/static/chunks/polyfills-42372ed130431b0a.js +1 -0
  28. package/dashboard/_next/static/chunks/webpack-b2507a6fba7be451.js +1 -0
  29. package/dashboard/_next/static/css/c60b60dc84892a8b.css +5 -0
  30. package/dashboard/_next/static/css/d4ea3c83e49eb5e1.css +4 -0
  31. package/dashboard/_next/static/jjqY4ybx1hNTX-bnJfS2f/_buildManifest.js +1 -0
  32. package/dashboard/_next/static/jjqY4ybx1hNTX-bnJfS2f/_ssgManifest.js +1 -0
  33. package/dashboard/_next/static/media/4cf2300e9c8272f7-s.p.woff2 +0 -0
  34. package/dashboard/_next/static/media/4da3161b738b07dd-s.woff2 +0 -0
  35. package/dashboard/_next/static/media/8d697b304b401681-s.woff2 +0 -0
  36. package/dashboard/_next/static/media/af4bf8399d1aacdf-s.p.woff2 +0 -0
  37. package/dashboard/_next/static/media/ba015fad6dcf6784-s.woff2 +0 -0
  38. package/dashboard/_next/static/media/fb526027db1fc1ae-s.woff2 +0 -0
  39. package/dashboard/agents/index.html +1 -0
  40. package/dashboard/agents/index.txt +28 -0
  41. package/dashboard/chat/index.html +1 -0
  42. package/dashboard/chat/index.txt +28 -0
  43. package/dashboard/config/index.html +1 -0
  44. package/dashboard/config/index.txt +28 -0
  45. package/dashboard/index.html +1 -0
  46. package/dashboard/index.txt +28 -0
  47. package/dashboard/monitor/index.html +1 -0
  48. package/dashboard/monitor/index.txt +28 -0
  49. package/dashboard/sessions/index.html +1 -0
  50. package/dashboard/sessions/index.txt +28 -0
  51. package/dist/agent/agent-sdk.js +66 -56
  52. package/dist/agent/agent-sdk.js.map +1 -1
  53. package/dist/bootstrap.js +1 -1
  54. package/dist/bootstrap.js.map +1 -1
  55. package/dist/cli/cmd-config.js +1 -1
  56. package/dist/cli/cmd-config.js.map +1 -1
  57. package/dist/cli/cmd-pairing.js +1 -1
  58. package/dist/cli/cmd-pairing.js.map +1 -1
  59. package/dist/core/config.js +29 -48
  60. package/dist/core/config.js.map +1 -1
  61. package/dist/core/constants.js +2 -0
  62. package/dist/core/constants.js.map +1 -1
  63. package/dist/core/version.js +1 -1
  64. package/dist/extensions/bot-approval.js +63 -0
  65. package/dist/extensions/bot-approval.js.map +1 -0
  66. package/dist/extensions/pairing.js +138 -0
  67. package/dist/extensions/pairing.js.map +1 -0
  68. package/dist/gateway/csrf.js +1 -1
  69. package/dist/gateway/csrf.js.map +1 -1
  70. package/dist/gateway/routes.js +31 -92
  71. package/dist/gateway/routes.js.map +1 -1
  72. package/dist/gateway/ws-handler.js +191 -167
  73. package/dist/gateway/ws-handler.js.map +1 -1
  74. package/dist/gateway-entry.js +8 -2
  75. package/dist/gateway-entry.js.map +1 -1
  76. package/dist/model.js +5 -4
  77. package/dist/model.js.map +1 -1
  78. package/dist/serve.js +20 -3
  79. package/dist/serve.js.map +1 -1
  80. package/dist/setup.js +17 -2
  81. package/dist/setup.js.map +1 -1
  82. package/dist/telegram/admin-agents.js +498 -0
  83. package/dist/telegram/admin-agents.js.map +1 -0
  84. package/dist/telegram/admin-bot.js +13 -934
  85. package/dist/telegram/admin-bot.js.map +1 -1
  86. package/dist/telegram/admin-commands.js +403 -0
  87. package/dist/telegram/admin-commands.js.map +1 -0
  88. package/dist/telegram/agent-bot.js +45 -1338
  89. package/dist/telegram/agent-bot.js.map +1 -1
  90. package/dist/telegram/agent-claude-code.js +555 -0
  91. package/dist/telegram/agent-claude-code.js.map +1 -0
  92. package/dist/telegram/agent-commands.js +396 -0
  93. package/dist/telegram/agent-commands.js.map +1 -0
  94. package/dist/telegram/agent-context.js +136 -0
  95. package/dist/telegram/agent-context.js.map +1 -0
  96. package/dist/telegram/agent-messages.js +249 -0
  97. package/dist/telegram/agent-messages.js.map +1 -0
  98. package/dist/telegram/resolve.js +15 -28
  99. package/dist/telegram/resolve.js.map +1 -1
  100. package/dist/telegram/terminal.js +75 -2
  101. package/dist/telegram/terminal.js.map +1 -1
  102. package/dist/telegram/wizards.js +1 -1
  103. package/dist/telegram/wizards.js.map +1 -1
  104. package/package.json +2 -1
@@ -1,1376 +1,83 @@
1
- // Agent bot: per-agent Telegram bot with commands and message handling
2
- import { Bot, InlineKeyboard, InputFile } from "grammy";
3
- import { loadConfig, saveConfig } from "../core/config.js";
4
- import { startWizard, advanceWizard, hasActiveWizard } from "./wizard.js";
5
- import { createMcpAddWizard } from "./wizards.js";
6
- import { createClient } from "../model.js";
7
- import { loadMessages, deleteSession, listSessions } from "../session.js";
8
- import { isRunActive } from "../runtime/runs.js";
9
- import { queueOrProcess } from "../runtime/queue.js";
10
- import { compactHistory } from "../runtime/compact.js";
11
- import { orchestrate } from "../runtime/orchestrate.js";
12
- import { getSessionUsage, formatUsageSummary, formatTokens } from "../usage.js";
13
- import { CHARS_PER_TOKEN } from "../core/constants.js";
14
- import { submitDecision } from "../extensions/approvals.js";
1
+ // Agent bot: per-agent Telegram bot setup, access control, wiring
2
+ import { Bot } from "grammy";
15
3
  import { registerForwardBot } from "../extensions/approval-forward.js";
16
- import { resolveAgent } from "./resolve.js";
17
- import { createDraftStream } from "./draft-stream.js";
18
- import { isGroupChat, shouldRespondInGroup, stripMention, sendChunked, startPolling } from "./helpers.js";
19
- import { hasPendingRequest, createPairingRequest } from "./pairing.js";
4
+ import { hasPendingRequest, createPairingRequest } from "../extensions/pairing.js";
20
5
  import { notifyAdminOfPairing } from "./pairing-notify.js";
21
- import { listSkillNames } from "../extensions/skills.js";
22
- import { log as slog } from "../core/log.js";
23
- import { transcribe } from "./transcribe.js";
24
- import { detectClaudeCode, startTerminal, endTerminal, hasTerminal, isTerminalBusy, handleTerminalMessage, expandHome, updateWorkDir, getTerminalSessionId, getTerminalWorkDir, getTerminalModel, setTerminalModel, getTerminalSetting, setTerminalSetting, setPinnedMessageId, getPinnedMessageId, listClaudeSessions } from "./terminal.js";
25
- import { startBrowse, handleBrowseCallback } from "./dir-browser.js";
26
- import os from "node:os";
6
+ import { isGroupChat, startPolling } from "./helpers.js";
7
+ import { isUserAllowed, setCommandMenu } from "./agent-context.js";
8
+ import { registerCommands } from "./agent-commands.js";
9
+ import { registerClaudeCode } from "./agent-claude-code.js";
10
+ import { registerMessageHandlers } from "./agent-messages.js";
27
11
  export async function setupAgentBot(agentId, botToken, getConfig, getSystemPrompt, activeBots) {
28
12
  const b = new Bot(botToken);
29
13
  const me = await b.api.getMe();
30
- const state = {
14
+ const ctx = {
15
+ agentId,
16
+ botToken,
31
17
  bot: b,
32
18
  botInfo: { id: me.id, username: me.username ?? "" },
19
+ getConfig,
20
+ getSystemPrompt,
21
+ activeBots,
33
22
  runtimeModels: new Map(),
34
23
  runtimeThinking: new Map(),
35
24
  runtimeEffort: new Map(),
36
25
  runtimeBriefMode: new Map(),
26
+ ccPaused: new Set(),
27
+ };
28
+ const state = {
29
+ bot: b,
30
+ botInfo: ctx.botInfo,
31
+ runtimeModels: ctx.runtimeModels,
32
+ runtimeThinking: ctx.runtimeThinking,
33
+ runtimeEffort: ctx.runtimeEffort,
34
+ runtimeBriefMode: ctx.runtimeBriefMode,
37
35
  };
38
36
  activeBots.set(agentId, state);
39
37
  // Register first bot for approval forwarding (headless -> Telegram)
40
38
  if (activeBots.size === 1) {
41
39
  registerForwardBot(b);
42
40
  }
43
- const { botInfo, runtimeModels, runtimeThinking, runtimeEffort, runtimeBriefMode } = state;
44
- // Error alert throttling — max 1 per agent per 5 minutes
45
- const errorAlertTimes = new Map();
46
- const ALERT_COOLDOWN_MS = 5 * 60 * 1000;
47
- async function alertAdmin(message) {
48
- const lastAlert = errorAlertTimes.get(agentId) ?? 0;
49
- if (Date.now() - lastAlert < ALERT_COOLDOWN_MS)
50
- return;
51
- errorAlertTimes.set(agentId, Date.now());
52
- const config = getConfig();
53
- const adminEntry = Object.entries(config.agents).find(([, a]) => a.admin);
54
- if (!adminEntry)
55
- return;
56
- const [adminId] = adminEntry;
57
- const adminState = activeBots.get(adminId);
58
- if (!adminState)
59
- return;
60
- const adminUsers = adminEntry[1].telegram?.allowedUsers ?? [];
61
- for (const userId of adminUsers) {
62
- try {
63
- await adminState.bot.api.sendMessage(userId, message);
64
- }
65
- catch { /* best effort */ }
66
- }
67
- }
68
- const sid = (chatId) => agentId === "telegram" ? `telegram-${chatId}` : `${agentId}-${chatId}`;
69
- const getAgent = (chatId) => resolveAgent(agentId, getConfig(), getSystemPrompt(), {
70
- model: runtimeModels.get(chatId),
71
- thinking: runtimeThinking.get(chatId),
72
- effort: runtimeEffort.get(chatId),
73
- briefMode: runtimeBriefMode.get(chatId),
74
- });
75
- // ─── Command menus (swap between normal and Claude Code mode) ─────
76
- const NORMAL_COMMANDS = [
77
- { command: "help", description: "List commands and current config" },
78
- { command: "clear", description: "Clear this chat's history" },
79
- { command: "status", description: "Show model, message count, token usage" },
80
- { command: "model", description: "Switch model for this chat" },
81
- { command: "think", description: "Set thinking level" },
82
- { command: "effort", description: "Set effort level" },
83
- { command: "usage", description: "Token usage for this session" },
84
- { command: "skills", description: "List active skills" },
85
- { command: "export", description: "Export session as markdown file" },
86
- { command: "session", description: "Show or switch session" },
87
- { command: "mcp", description: "Manage MCP tool servers" },
88
- { command: "brief", description: "Toggle brief response mode" },
89
- { command: "compact", description: "Force compaction of chat history" },
90
- { command: "voice", description: "Voice transcription info" },
91
- { command: "claudecode", description: "Claude Code — start, stop, sessions" },
92
- ];
93
- const CLAUDECODE_COMMANDS = [
94
- // Core
95
- { command: "claudecode", description: "Menu — sessions, model, settings" },
96
- { command: "exit", description: "Exit Claude Code mode" },
97
- { command: "model", description: "Switch model (sonnet, opus, haiku)" },
98
- { command: "workdir", description: "Change working directory" },
99
- // Code actions
100
- { command: "review", description: "Review code changes" },
101
- { command: "fix", description: "Find and fix bugs" },
102
- { command: "test", description: "Write or run tests" },
103
- { command: "commit", description: "Commit changes" },
104
- { command: "pr", description: "Write PR description" },
105
- { command: "refactor", description: "Suggest refactoring" },
106
- { command: "security", description: "Security review" },
107
- { command: "explain", description: "Explain the codebase" },
108
- { command: "init", description: "Create CLAUDE.md" },
109
- { command: "doc", description: "Generate documentation" },
110
- // Settings
111
- { command: "effort", description: "Effort level (low/medium/high/max)" },
112
- { command: "tools", description: "Allow/deny tools" },
113
- { command: "approvals", description: "Approval mode (skip/acceptEdits)" },
114
- { command: "prompt", description: "Custom system prompt" },
115
- { command: "budget", description: "Max budget in USD" },
116
- { command: "adddir", description: "Add extra directory" },
117
- { command: "worktree", description: "Git worktree isolation" },
118
- { command: "cost", description: "Session cost" },
119
- ];
120
- async function setCommandMenu(ccMode, chatId) {
121
- const commands = ccMode ? CLAUDECODE_COMMANDS : NORMAL_COMMANDS;
122
- if (chatId) {
123
- // Per-chat menu
124
- await b.api.setMyCommands(commands, { scope: { type: "chat", chat_id: chatId } }).catch(() => { });
125
- }
126
- else {
127
- // Global default
128
- await b.api.setMyCommands(commands).catch(() => { });
129
- }
130
- }
131
41
  // Set global default commands
132
- await setCommandMenu(false);
133
- /** Check if userId is allowed for this agent (in-memory + file fallback) */
134
- function isUserAllowed(userId) {
135
- const agent = getAgent(0);
136
- if (agent.allowedUsers.includes(userId))
137
- return true;
138
- // Fallback: read config file directly (handles hot-reload delay)
139
- try {
140
- const fresh = loadConfig();
141
- const freshAgent = agentId === "telegram"
142
- ? fresh.telegram
143
- : fresh.agents[agentId]?.telegram;
144
- const freshAllowed = freshAgent?.allowedUsers ?? [];
145
- if (freshAllowed.includes(userId))
146
- return true;
147
- }
148
- catch { }
149
- return false;
150
- }
151
- // Access control — admin approves, user gets instant access
152
- b.use(async (ctx, next) => {
153
- const agent = getAgent(ctx.chat?.id ?? 0);
154
- if (agent.allowedUsers.length === 0) {
42
+ await setCommandMenu(ctx, false);
43
+ // ─── Access control middleware ─────────────────────────────────────
44
+ b.use(async (gc, next) => {
45
+ const agent = ctx.getConfig().agents[agentId];
46
+ const allowedUsers = agentId === "telegram"
47
+ ? ctx.getConfig().telegram.allowedUsers
48
+ : (agent?.telegram?.allowedUsers ?? []);
49
+ if (allowedUsers.length === 0) {
155
50
  await next();
156
51
  return;
157
52
  }
158
- const userId = ctx.from?.id;
53
+ const userId = gc.from?.id;
159
54
  if (!userId)
160
55
  return;
161
- if (isUserAllowed(userId)) {
56
+ if (isUserAllowed(ctx, userId)) {
162
57
  await next();
163
58
  return;
164
59
  }
165
60
  // Unauthorized user in group — silent reject
166
- if (ctx.chat && isGroupChat(ctx.chat.type))
61
+ if (gc.chat && isGroupChat(gc.chat.type))
167
62
  return;
168
63
  // Check if user already has a pending request
169
64
  const pending = hasPendingRequest(userId, agentId);
170
65
  if (pending) {
171
- await ctx.reply(`Your access request is pending approval.\nCode: ${pending.code}`);
66
+ await gc.reply(`Your access request is pending approval.\nCode: ${pending.code}`);
172
67
  return;
173
68
  }
174
69
  // Create new pairing request
175
- const request = createPairingRequest(userId, agentId, ctx.chat.id, ctx.from?.username, ctx.from?.first_name);
176
- const who = ctx.from?.username ? `@${ctx.from.username}` : ctx.from?.first_name ?? String(userId);
70
+ const request = createPairingRequest(userId, agentId, gc.chat.id, gc.from?.username, gc.from?.first_name);
71
+ const who = gc.from?.username ? `@${gc.from.username}` : gc.from?.first_name ?? String(userId);
177
72
  console.log(`\n \x1b[33mPairing request from ${who} for agent "${agentId}"\x1b[0m\n \x1b[90mRun: camel pairing\x1b[0m\n`);
178
- await ctx.reply(`Access requested. Waiting for admin approval.\nCode: ${request.code}`);
179
- notifyAdminOfPairing(request, getConfig(), activeBots);
180
- });
181
- // ─── Command mode guard ──────────────────────────────────────────
182
- // Normal commands blocked in Claude Code mode, Claude Code commands blocked in normal mode
183
- const CC_ONLY_COMMANDS = new Set([
184
- "exit", "review", "fix", "test", "refactor", "security",
185
- "pr", "commit", "init", "explain", "doc", "cost",
186
- "prompt", "budget", "adddir", "tools", "worktree", "approvals",
187
- ]);
188
- const SHARED_COMMANDS = new Set([
189
- "claudecode", "start", "model", "effort", "workdir", "help",
190
- ]);
191
- b.on("message", async (ctx, next) => {
192
- const text = ctx.message?.text;
193
- if (!text?.startsWith("/")) {
194
- await next();
195
- return;
196
- }
197
- const cmd = text.split(/[\s@]/)[0].slice(1).toLowerCase();
198
- if (SHARED_COMMANDS.has(cmd)) {
199
- await next();
200
- return;
201
- }
202
- const inCC = hasTerminal(ctx.chat.id);
203
- if (inCC && !CC_ONLY_COMMANDS.has(cmd)) {
204
- await ctx.reply("Exit Claude Code first (/exit), then use this command.");
205
- return;
206
- }
207
- if (!inCC && CC_ONLY_COMMANDS.has(cmd)) {
208
- await ctx.reply("Start Claude Code first (/claudecode).");
209
- return;
210
- }
211
- await next();
212
- });
213
- // ─── Commands ─────────────────────────────────────────────────────
214
- b.command("start", async (ctx) => {
215
- const agent = getAgent(ctx.chat.id);
216
- if (isGroupChat(ctx.chat.type)) {
217
- await ctx.reply(`${agent.name} added. Mention me with @${botInfo.username} to chat.`);
218
- }
219
- else {
220
- await ctx.reply(`${agent.name} is ready.\n\nModel: ${agent.model}\nSend me a message or type /help for commands.`);
221
- }
222
- });
223
- b.command("help", async (ctx) => {
224
- const agent = getAgent(ctx.chat.id);
225
- const lines = [
226
- `${agent.name} Commands:\n`,
227
- "/help — List commands and current config",
228
- "/clear — Clear this chat's history",
229
- "/status — Show model, message count, token usage",
230
- "/model <name> — Switch model for this chat",
231
- "/think <level> — Set thinking (off|low|medium|high)",
232
- "/effort <level> — Set effort (low|medium|high|max)",
233
- "/usage — Token usage for this session",
234
- "/brief — Toggle brief response mode",
235
- "/skills — List active skills",
236
- "/mcp — Manage MCP tool servers",
237
- "/export — Export session as markdown file",
238
- "/session — Show or switch session",
239
- "/compact — Force compaction of chat history",
240
- "",
241
- `Model: ${agent.model}`,
242
- `Thinking: ${agent.thinking}`,
243
- `Effort: ${agent.effort}`,
244
- `Max turns: ${agent.maxTurns}`,
245
- `Brief mode: ${agent.briefMode ? "on" : "off"}`,
246
- ];
247
- await ctx.reply(lines.join("\n"));
248
- });
249
- b.command("clear", async (ctx) => {
250
- deleteSession(sid(ctx.chat.id));
251
- runtimeModels.delete(ctx.chat.id);
252
- runtimeThinking.delete(ctx.chat.id);
253
- runtimeEffort.delete(ctx.chat.id);
254
- runtimeBriefMode.delete(ctx.chat.id);
255
- await ctx.reply("Session cleared.");
256
- });
257
- b.command("status", async (ctx) => {
258
- const agent = getAgent(ctx.chat.id);
259
- const sessionId = sid(ctx.chat.id);
260
- const messages = loadMessages(sessionId);
261
- const usage = getSessionUsage(sessionId);
262
- const historyChars = messages.reduce((sum, m) => sum + m.content.length, 0);
263
- const historyTokens = Math.ceil(historyChars / CHARS_PER_TOKEN);
264
- const lines = [
265
- `Agent: ${agent.name}`,
266
- `Model: ${agent.model}`,
267
- `Thinking: ${agent.thinking}`,
268
- `Effort: ${agent.effort}`,
269
- `Messages: ${messages.length}`,
270
- `History: ~${historyTokens} tokens`,
271
- ];
272
- if (usage.calls > 0)
273
- lines.push(`Usage: ${formatUsageSummary(usage)}`);
274
- if (runtimeModels.has(ctx.chat.id))
275
- lines.push(`(runtime override, resets on /clear or restart)`);
276
- await ctx.reply(lines.join("\n"));
277
- });
278
- b.command("model", async (ctx) => {
279
- // Claude Code mode: only Claude models (sonnet, opus, haiku)
280
- if (hasTerminal(ctx.chat.id)) {
281
- const arg = ctx.match?.trim();
282
- if (arg) {
283
- setTerminalModel(ctx.chat.id, arg);
284
- await ctx.reply(`Model set to: ${arg}`);
285
- }
286
- else {
287
- const current = getTerminalModel(ctx.chat.id) ?? "default";
288
- const kb = new InlineKeyboard()
289
- .text("Sonnet 4.6", "cc:setmodel:claude-sonnet-4-6").text("Opus 4.6", "cc:setmodel:claude-opus-4-6").row()
290
- .text("Haiku 4.5", "cc:setmodel:claude-haiku-4-5-20251001").row()
291
- .text("Default", "cc:setmodel:__default__");
292
- await ctx.reply(`Claude Code model: ${current}`, { reply_markup: kb });
293
- }
294
- return;
295
- }
296
- const newModel = ctx.match?.trim();
297
- if (!newModel) {
298
- const agent = getAgent(ctx.chat.id);
299
- await ctx.reply(`Current model: ${agent.model}\n\nUsage: /model <name>`);
300
- return;
301
- }
302
- runtimeModels.set(ctx.chat.id, newModel);
303
- await ctx.reply(`Model switched to: ${newModel}\n(runtime only, resets on /clear or restart)`);
304
- });
305
- b.command("think", async (ctx) => {
306
- const levels = ["off", "low", "medium", "high"];
307
- const arg = ctx.match?.trim();
308
- const agent = getAgent(ctx.chat.id);
309
- if (!arg) {
310
- const kb = new InlineKeyboard();
311
- for (const l of levels) {
312
- kb.text(l === agent.thinking ? `✓ ${l}` : l, `think:${l}`);
313
- }
314
- await ctx.reply(`Thinking: ${agent.thinking}`, { reply_markup: kb });
315
- return;
316
- }
317
- if (!levels.includes(arg)) {
318
- await ctx.reply("Invalid level. Use: off, low, medium, high");
319
- return;
320
- }
321
- runtimeThinking.set(ctx.chat.id, arg);
322
- await ctx.reply(`Thinking set to: ${arg}`);
323
- });
324
- b.callbackQuery(/^think:(.+)$/, async (ctx) => {
325
- const level = ctx.callbackQuery.data.split(":")[1];
326
- runtimeThinking.set(ctx.chat.id, level);
327
- try {
328
- await ctx.editMessageText(`Thinking: ${level} ✓`);
329
- }
330
- catch { }
331
- await ctx.answerCallbackQuery();
332
- });
333
- b.command("effort", async (ctx) => {
334
- // Claude Code mode: set effort for claude subprocess
335
- if (hasTerminal(ctx.chat.id)) {
336
- const levels = ["low", "medium", "high", "max"];
337
- const arg = ctx.match?.trim();
338
- if (arg && levels.includes(arg)) {
339
- setTerminalSetting(ctx.chat.id, "effort", arg);
340
- await ctx.reply(`Effort set to: ${arg}`);
341
- }
342
- else {
343
- const current = getTerminalSetting(ctx.chat.id, "effort") ?? "default";
344
- const kb = new InlineKeyboard()
345
- .text("Low", "cc:effort:low").text("Medium", "cc:effort:medium").row()
346
- .text("High", "cc:effort:high").text("Max", "cc:effort:max");
347
- await ctx.reply(`Effort: ${current}`, { reply_markup: kb });
348
- }
349
- return;
350
- }
351
- const levels = ["low", "medium", "high", "max"];
352
- const arg = ctx.match?.trim();
353
- const agent = getAgent(ctx.chat.id);
354
- if (!arg) {
355
- const kb = new InlineKeyboard();
356
- for (const l of levels) {
357
- kb.text(l === agent.effort ? `✓ ${l}` : l, `effort:${l}`);
358
- }
359
- await ctx.reply(`Effort: ${agent.effort}`, { reply_markup: kb });
360
- return;
361
- }
362
- if (!levels.includes(arg)) {
363
- await ctx.reply("Invalid level. Use: low, medium, high, max");
364
- return;
365
- }
366
- runtimeEffort.set(ctx.chat.id, arg);
367
- await ctx.reply(`Effort set to: ${arg}`);
368
- });
369
- b.callbackQuery(/^effort:(.+)$/, async (ctx) => {
370
- const level = ctx.callbackQuery.data.split(":")[1];
371
- runtimeEffort.set(ctx.chat.id, level);
372
- try {
373
- await ctx.editMessageText(`Effort: ${level} ✓`);
374
- }
375
- catch { }
376
- await ctx.answerCallbackQuery();
377
- });
378
- b.command("brief", async (ctx) => {
379
- const agent = getAgent(ctx.chat.id);
380
- const current = runtimeBriefMode.get(ctx.chat.id) ?? agent.briefMode;
381
- const next = !current;
382
- runtimeBriefMode.set(ctx.chat.id, next);
383
- await ctx.reply(`Brief mode: ${next ? "on — short replies" : "off — detailed replies"}`);
384
- });
385
- b.command("usage", async (ctx) => {
386
- const sessionId = sid(ctx.chat.id);
387
- const usage = getSessionUsage(sessionId);
388
- const messages = loadMessages(sessionId);
389
- if (usage.calls === 0) {
390
- await ctx.reply("No usage yet in this session.");
391
- return;
392
- }
393
- const total = usage.totalInput + usage.totalOutput;
394
- const lines = [
395
- `Token usage this session:\n`,
396
- `Total: ${formatTokens(total)} tokens`,
397
- ` Input: ${formatTokens(usage.totalInput)}`,
398
- ` Output: ${formatTokens(usage.totalOutput)}`,
399
- ];
400
- if (usage.totalCacheRead > 0)
401
- lines.push(` Cache read: ${formatTokens(usage.totalCacheRead)}`);
402
- if (usage.totalCacheWrite > 0)
403
- lines.push(` Cache write: ${formatTokens(usage.totalCacheWrite)}`);
404
- lines.push("", `API calls: ${usage.calls}`, `Messages: ${messages.length}`);
405
- await ctx.reply(lines.join("\n"));
406
- });
407
- b.command("skills", async (ctx) => {
408
- const skills = listSkillNames();
409
- if (skills.length === 0) {
410
- await ctx.reply("No skills installed.\n\nAdd skills to ~/.camelagi/skills/");
411
- }
412
- else {
413
- await ctx.reply(`Active skills: ${skills.join(", ")}`);
414
- }
415
- });
416
- b.command("export", async (ctx) => {
417
- const sessionId = sid(ctx.chat.id);
418
- const messages = loadMessages(sessionId);
419
- if (messages.length === 0) {
420
- await ctx.reply("No messages to export.");
421
- return;
422
- }
423
- const md = messages.map(m => m.role === "user" ? `## You\n\n${m.content}` : `## Assistant\n\n${m.content}`).join("\n\n---\n\n");
424
- const buf = Buffer.from(md, "utf-8");
425
- await ctx.replyWithDocument(new InputFile(buf, `${sessionId}.md`));
426
- });
427
- b.command("session", async (ctx) => {
428
- const arg = (ctx.match ?? "").trim();
429
- const sessionId = sid(ctx.chat.id);
430
- if (!arg) {
431
- await ctx.reply(`Current session: ${sessionId}`);
432
- return;
433
- }
434
- if (arg === "list") {
435
- const sessions = listSessions();
436
- if (sessions.length === 0) {
437
- await ctx.reply("No sessions.");
438
- return;
439
- }
440
- const lines = sessions.slice(0, 20).map(s => {
441
- const msgs = loadMessages(s.id).length;
442
- return `${s.id} (${msgs} msgs)`;
443
- });
444
- await ctx.reply(lines.join("\n"));
445
- return;
446
- }
447
- // TODO: session switching for Telegram requires runtime state
448
- await ctx.reply(`Session switching coming soon. Current: ${sessionId}`);
449
- });
450
- b.command("compact", async (ctx) => {
451
- const config = getConfig();
452
- const agent = getAgent(ctx.chat.id);
453
- const sessionId = sid(ctx.chat.id);
454
- const history = loadMessages(sessionId);
455
- if (history.length === 0) {
456
- await ctx.reply("No history to compact.");
457
- return;
458
- }
459
- const client = createClient(config);
460
- const result = await compactHistory(client, agent.model, history, { ...config.compaction, enabled: true, agentId: agentId === "telegram" ? undefined : agentId });
461
- if (result) {
462
- await ctx.reply(`Compacted: ${history.length} -> ${result.length} messages`);
463
- }
464
- else {
465
- await ctx.reply(`History is already compact (${history.length} messages).`);
466
- }
467
- });
468
- b.command("mcp", async (ctx) => {
469
- const kb = new InlineKeyboard()
470
- .text("➕ Add Server", "mcp:add")
471
- .text("📋 List", "mcp:list")
472
- .text("🗑 Remove", "mcp:remove");
473
- await ctx.reply("MCP Servers", { reply_markup: kb });
474
- });
475
- b.callbackQuery("mcp:add", async (ctx) => {
476
- await ctx.answerCallbackQuery();
477
- await startWizard(ctx.chat.id, createMcpAddWizard(getConfig, agentId), b);
478
- });
479
- b.callbackQuery("mcp:list", async (ctx) => {
480
- await ctx.answerCallbackQuery();
481
- const config = getConfig();
482
- const isAgent = agentId && agentId !== "default" && config.agents[agentId];
483
- const scope = isAgent ? `agent "${config.agents[agentId].name}"` : "global";
484
- const servers = isAgent
485
- ? config.agents[agentId]?.mcp?.servers ?? {}
486
- : config.mcp.servers;
487
- const entries = Object.entries(servers);
488
- if (entries.length === 0) {
489
- await ctx.reply(`No MCP servers (${scope}).`);
490
- return;
491
- }
492
- const lines = entries.map(([name, s]) => {
493
- const cfg = s;
494
- if (cfg.type === "stdio") {
495
- const args = Array.isArray(cfg.args) ? cfg.args.join(" ") : "";
496
- return `⚙️ ${name} (stdio)\n ${cfg.command} ${args}`.trimEnd();
497
- }
498
- return `${cfg.type === "sse" ? "📡" : "🌐"} ${name} (${cfg.type})\n ${cfg.url}`;
499
- });
500
- await ctx.reply(`MCP Servers (${scope}):\n\n${lines.join("\n\n")}`);
501
- });
502
- b.callbackQuery("mcp:remove", async (ctx) => {
503
- await ctx.answerCallbackQuery();
504
- const config = getConfig();
505
- const isAgent = agentId && agentId !== "default" && config.agents[agentId];
506
- const servers = isAgent
507
- ? config.agents[agentId]?.mcp?.servers ?? {}
508
- : config.mcp.servers;
509
- const names = Object.keys(servers);
510
- if (names.length === 0) {
511
- await ctx.reply("No MCP servers to remove.");
512
- return;
513
- }
514
- const kb = new InlineKeyboard();
515
- for (const name of names) {
516
- kb.text(`✕ ${name}`, `mcp:rm:${name}`).row();
517
- }
518
- await ctx.reply("Remove which server?", { reply_markup: kb });
519
- });
520
- b.callbackQuery(/^mcp:rm:/, async (ctx) => {
521
- await ctx.answerCallbackQuery();
522
- const name = ctx.callbackQuery.data.replace("mcp:rm:", "");
523
- const config = getConfig();
524
- const isAgent = agentId && agentId !== "default" && config.agents[agentId];
525
- const servers = isAgent
526
- ? { ...(config.agents[agentId]?.mcp?.servers ?? {}) }
527
- : { ...config.mcp.servers };
528
- if (!(name in servers)) {
529
- await ctx.reply(`Server "${name}" not found.`);
530
- return;
531
- }
532
- delete servers[name];
533
- if (isAgent) {
534
- const agents = { ...config.agents };
535
- agents[agentId] = { ...agents[agentId], mcp: { servers } };
536
- saveConfig({ agents });
537
- }
538
- else {
539
- saveConfig({ mcp: { servers } });
540
- }
541
- await ctx.reply(`Removed MCP server: ${name}`);
542
- });
543
- // ─── Terminal mode: Claude Code via CLI ──────────────────────────
544
- // Track chats that manually exited Claude Code (prevents auto-restart for mode: claude-code agents)
545
- const ccPaused = new Set();
546
- // ─── /cc — Claude Code menu ────────────────────────────────────────
547
- async function ccPinStatus(chatId, api, on) {
548
- // Always clean up old pin first
549
- const oldPin = getPinnedMessageId(chatId);
550
- if (oldPin) {
551
- try {
552
- await api.unpinChatMessage(chatId, oldPin);
553
- }
554
- catch { }
555
- try {
556
- await api.deleteMessage(chatId, oldPin);
557
- }
558
- catch { }
559
- }
560
- setPinnedMessageId(chatId, undefined);
561
- if (on) {
562
- try {
563
- const msg = await api.sendMessage(chatId, "Claude Code ON");
564
- await api.pinChatMessage(chatId, msg.message_id, { disable_notification: true });
565
- setPinnedMessageId(chatId, msg.message_id);
566
- }
567
- catch { }
568
- }
569
- }
570
- function ccResolveWorkDir() {
571
- const config = getConfig();
572
- const agentConfig = config.agents[agentId];
573
- return agentConfig?.workDir ? expandHome(agentConfig.workDir) : os.homedir() + "/Desktop";
574
- }
575
- b.command("claudecode", async (ctx) => {
576
- const detection = detectClaudeCode();
577
- if (!detection.found) {
578
- await ctx.reply("Claude Code not found. Install: npm i -g @anthropic-ai/claude-code");
579
- return;
580
- }
581
- if (hasTerminal(ctx.chat.id)) {
582
- // Active session — show status + options
583
- const sessionId = getTerminalSessionId(ctx.chat.id) ?? "none";
584
- const workDir = getTerminalWorkDir(ctx.chat.id) ?? "?";
585
- const model = getTerminalModel(ctx.chat.id) ?? "default";
586
- const home = os.homedir();
587
- const displayDir = workDir.startsWith(home) ? "~" + workDir.slice(home.length) : workDir;
588
- const effort = getTerminalSetting(ctx.chat.id, "effort") ?? "default";
589
- const budget = getTerminalSetting(ctx.chat.id, "maxBudgetUsd");
590
- const worktree = getTerminalSetting(ctx.chat.id, "worktree");
591
- const approvals = getTerminalSetting(ctx.chat.id, "permissionMode") ?? "skip";
592
- const kb = new InlineKeyboard()
593
- .text("New Session", "cc:new").text("Stop", "cc:stop").row()
594
- .text("Model", "cc:model").text("Effort", "cc:effortmenu").row()
595
- .text("Approvals", "cc:approvalsmenu").text("Sessions", "cc:sessions").row()
596
- .text("Work Dir", "cc:workdir");
597
- const approvalsLabel = approvals === "acceptEdits" ? "Accept Edits" : "Skip All";
598
- const lines = [
599
- `Claude Code active`,
600
- `Session: ${sessionId.slice(0, 8)}...`,
601
- `Model: ${model} | Effort: ${effort}`,
602
- `Approvals: ${approvalsLabel}`,
603
- `Dir: ${displayDir}`,
604
- ];
605
- if (budget)
606
- lines.push(`Budget: $${budget}`);
607
- if (worktree)
608
- lines.push(`Worktree: ON`);
609
- await ctx.reply(lines.join("\n"), { reply_markup: kb });
610
- }
611
- else {
612
- // Not active — show start options
613
- const kb = new InlineKeyboard()
614
- .text("Start", "cc:start").text("Resume Session", "cc:sessions").row()
615
- .text("Work Dir", "cc:workdir");
616
- await ctx.reply(`Claude Code (${detection.version ?? ""})\nDir: ${ccResolveWorkDir().replace(os.homedir(), "~")}`, { reply_markup: kb });
617
- }
618
- });
619
- b.command("exit", async (ctx) => {
620
- if (!hasTerminal(ctx.chat.id)) {
621
- await ctx.reply("No active Claude Code session.");
622
- return;
623
- }
624
- await ccPinStatus(ctx.chat.id, ctx.api, false);
625
- endTerminal(ctx.chat.id);
626
- ccPaused.add(ctx.chat.id);
627
- await setCommandMenu(false, ctx.chat.id);
628
- await ctx.reply("Claude Code stopped. Use /claudecode to start again.");
629
- });
630
- b.command("workdir", async (ctx) => {
631
- const config = getConfig();
632
- const agentConfig = config.agents[agentId];
633
- const currentDir = agentConfig?.workDir
634
- ? expandHome(agentConfig.workDir)
635
- : os.homedir();
636
- await startBrowse(ctx.chat.id, ctx.api, currentDir, (selectedDir) => {
637
- // Save to config
638
- const agents = { ...config.agents };
639
- agents[agentId] = { ...agents[agentId], workDir: selectedDir };
640
- saveConfig({ agents });
641
- // Update active terminal session if any
642
- if (hasTerminal(ctx.chat.id)) {
643
- updateWorkDir(ctx.chat.id, selectedDir);
644
- }
645
- });
646
- });
647
- // Claude Code shortcut commands — map Telegram commands to natural language prompts
648
- const CC_SHORTCUTS = {
649
- "/review": (args) => args
650
- ? `Review this code and provide feedback: ${args}`
651
- : "Review all the code changes in the current working directory. Look at git diff if available, otherwise review the main files. Provide actionable feedback on bugs, improvements, and best practices.",
652
- "/init": "Create a CLAUDE.md file for this project. Analyze the codebase structure, tech stack, build commands, and key patterns. Write a concise CLAUDE.md that helps future Claude Code sessions understand this project.",
653
- "/fix": (args) => args
654
- ? `Find and fix this issue: ${args}`
655
- : "Look at the code in the current directory. Find any bugs, errors, or issues and fix them.",
656
- "/test": (args) => args
657
- ? `Write tests for: ${args}`
658
- : "Look at the code in the current directory and write or run the appropriate tests.",
659
- "/explain": (args) => args
660
- ? `Explain this: ${args}`
661
- : "Explain the architecture and key patterns of this codebase. What does it do, how is it structured, and what are the main entry points?",
662
- "/refactor": (args) => args
663
- ? `Refactor this: ${args}`
664
- : "Review the code in the current directory and suggest refactoring improvements. Focus on readability, maintainability, and removing duplication.",
665
- "/security": "Perform a security review of this codebase. Look for common vulnerabilities: injection, XSS, auth issues, hardcoded secrets, insecure dependencies. Report findings with severity and fix suggestions.",
666
- "/pr": "Look at the current git changes (staged and unstaged). Write a pull request description with a summary of changes, what was changed and why, and any testing notes.",
667
- "/commit": "Look at the current git changes. Create a well-formatted commit message that describes what changed and why. Then commit the changes.",
668
- "/doc": (args) => args
669
- ? `Write documentation for: ${args}`
670
- : "Generate documentation for the key modules in this codebase. Focus on public APIs, configuration options, and usage examples.",
671
- "/cost": "Show the current Claude Code session cost and token usage.",
672
- };
673
- // Claude Code settings commands — handled before sending to subprocess
674
- const CC_SETTINGS = {
675
- "/model": async (ctx, args) => {
676
- const chatId = ctx.chat.id;
677
- if (!hasTerminal(chatId)) {
678
- await ctx.reply("No active Claude Code session.");
679
- return true;
680
- }
681
- const models = ["sonnet", "opus", "haiku"];
682
- if (args && models.includes(args.toLowerCase())) {
683
- setTerminalModel(chatId, args.toLowerCase());
684
- await ctx.reply(`Model set to: ${args.toLowerCase()}`);
685
- }
686
- else if (args) {
687
- // Allow any model name (e.g. claude-sonnet-4-20250514)
688
- setTerminalModel(chatId, args);
689
- await ctx.reply(`Model set to: ${args}`);
690
- }
691
- else {
692
- const current = getTerminalModel(chatId) ?? "default";
693
- const kb = new InlineKeyboard()
694
- .text("Sonnet 4.6", "cc:setmodel:claude-sonnet-4-6").text("Opus 4.6", "cc:setmodel:claude-opus-4-6").row()
695
- .text("Haiku 4.5", "cc:setmodel:claude-haiku-4-5-20251001").row()
696
- .text("Default", "cc:setmodel:__default__");
697
- await ctx.reply(`Model: ${current}`, { reply_markup: kb });
698
- }
699
- return true;
700
- },
701
- "/effort": async (ctx, args) => {
702
- const chatId = ctx.chat.id;
703
- if (!hasTerminal(chatId)) {
704
- await ctx.reply("No active Claude Code session.");
705
- return true;
706
- }
707
- const levels = ["low", "medium", "high", "max"];
708
- if (args && levels.includes(args)) {
709
- setTerminalSetting(chatId, "effort", args);
710
- await ctx.reply(`Effort set to: ${args}`);
711
- }
712
- else {
713
- const current = getTerminalSetting(chatId, "effort") ?? "default";
714
- const kb = new InlineKeyboard()
715
- .text("Low", "cc:effort:low").text("Medium", "cc:effort:medium").row()
716
- .text("High", "cc:effort:high").text("Max", "cc:effort:max");
717
- await ctx.reply(`Effort: ${current}`, { reply_markup: kb });
718
- }
719
- return true;
720
- },
721
- "/prompt": async (ctx, args) => {
722
- const chatId = ctx.chat.id;
723
- if (!hasTerminal(chatId)) {
724
- await ctx.reply("No active Claude Code session.");
725
- return true;
726
- }
727
- if (args) {
728
- setTerminalSetting(chatId, "systemPrompt", args);
729
- await ctx.reply(`System prompt set.`);
730
- }
731
- else {
732
- const current = getTerminalSetting(chatId, "systemPrompt");
733
- await ctx.reply(current ? `Current prompt: ${current}\n\nSend /prompt <text> to change.` : "No custom prompt. Send /prompt <text> to set one.");
734
- }
735
- return true;
736
- },
737
- "/budget": async (ctx, args) => {
738
- const chatId = ctx.chat.id;
739
- if (!hasTerminal(chatId)) {
740
- await ctx.reply("No active Claude Code session.");
741
- return true;
742
- }
743
- const amount = parseFloat(args);
744
- if (args && !isNaN(amount) && amount > 0) {
745
- setTerminalSetting(chatId, "maxBudgetUsd", amount);
746
- await ctx.reply(`Budget limit set to: $${amount}`);
747
- }
748
- else {
749
- const current = getTerminalSetting(chatId, "maxBudgetUsd");
750
- await ctx.reply(current ? `Budget: $${current}\n\nSend /budget <amount> to change.` : "No budget limit. Send /budget 5.00 to set one.");
751
- }
752
- return true;
753
- },
754
- "/adddir": async (ctx, args) => {
755
- const chatId = ctx.chat.id;
756
- if (!hasTerminal(chatId)) {
757
- await ctx.reply("No active Claude Code session.");
758
- return true;
759
- }
760
- if (args) {
761
- const current = getTerminalSetting(chatId, "addDirs") ?? [];
762
- setTerminalSetting(chatId, "addDirs", [...current, args]);
763
- await ctx.reply(`Added directory: ${args}`);
764
- }
765
- else {
766
- const current = getTerminalSetting(chatId, "addDirs") ?? [];
767
- await ctx.reply(current.length ? `Extra dirs: ${current.join(", ")}\n\nSend /adddir <path> to add more.` : "No extra directories. Send /adddir ~/other-project to add one.");
768
- }
769
- return true;
770
- },
771
- "/tools": async (ctx, _args) => {
772
- const chatId = ctx.chat.id;
773
- if (!hasTerminal(chatId)) {
774
- await ctx.reply("No active Claude Code session.");
775
- return true;
776
- }
777
- const denied = new Set(getTerminalSetting(chatId, "disallowedTools") ?? []);
778
- const CC_TOOLS = ["Bash", "Read", "Write", "Edit", "Glob", "Grep", "WebSearch", "WebFetch", "Agent"];
779
- const kb = new InlineKeyboard();
780
- for (let i = 0; i < CC_TOOLS.length; i++) {
781
- const t = CC_TOOLS[i];
782
- const icon = denied.has(t) ? "🚫" : "✅";
783
- kb.text(`${icon} ${t}`, `cc:tool:${t}`);
784
- if ((i + 1) % 3 === 0)
785
- kb.row();
786
- }
787
- kb.row().text("Reset All", "cc:tool:__reset__");
788
- const status = denied.size ? `Blocked: ${[...denied].join(", ")}` : "All tools enabled";
789
- await ctx.reply(`${status}\n\nTap to toggle:`, { reply_markup: kb });
790
- return true;
791
- },
792
- "/worktree": async (ctx, _args) => {
793
- const chatId = ctx.chat.id;
794
- if (!hasTerminal(chatId)) {
795
- await ctx.reply("No active Claude Code session.");
796
- return true;
797
- }
798
- const current = getTerminalSetting(chatId, "worktree") ?? false;
799
- setTerminalSetting(chatId, "worktree", !current);
800
- await ctx.reply(`Git worktree: ${!current ? "ON" : "OFF"}`);
801
- return true;
802
- },
803
- "/approvals": async (ctx, args) => {
804
- const chatId = ctx.chat.id;
805
- if (!hasTerminal(chatId)) {
806
- await ctx.reply("No active Claude Code session.");
807
- return true;
808
- }
809
- const modes = ["skip", "acceptEdits"];
810
- if (args && modes.includes(args)) {
811
- setTerminalSetting(chatId, "permissionMode", args);
812
- const label = args === "acceptEdits" ? "Accept Edits" : "Skip All";
813
- await ctx.reply(`Approvals set to: ${label}`);
814
- }
815
- else {
816
- const current = getTerminalSetting(chatId, "permissionMode") ?? "skip";
817
- const label = current === "acceptEdits" ? "Accept Edits" : "Skip All";
818
- const kb = new InlineKeyboard()
819
- .text("Skip All", "cc:setapprovals:skip").text("Accept Edits", "cc:setapprovals:acceptEdits");
820
- await ctx.reply(`Approvals: ${label}`, { reply_markup: kb });
821
- }
822
- return true;
823
- },
824
- };
825
- // Terminal log helpers
826
- const cclog = (icon, msg) => {
827
- const time = new Date().toLocaleTimeString("en-GB", { hour12: false });
828
- console.log(` ${time} ${icon} ${msg}`);
829
- };
830
- async function handleTerminalIncoming(ctx) {
831
- const chatId = ctx.chat.id;
832
- let text = stripMention(ctx.message.text, botInfo.username);
833
- if (!text)
834
- return;
835
- // Check for Claude Code settings commands (don't send to subprocess)
836
- const firstWord = text.split(/\s+/)[0].toLowerCase();
837
- const settingHandler = CC_SETTINGS[firstWord];
838
- if (settingHandler) {
839
- const args = text.slice(firstWord.length).trim();
840
- await settingHandler(ctx, args);
841
- return;
842
- }
843
- // Check for Claude Code shortcut commands (convert to prompts)
844
- const shortcut = CC_SHORTCUTS[firstWord];
845
- if (shortcut) {
846
- const args = text.slice(firstWord.length).trim();
847
- text = typeof shortcut === "function" ? shortcut(args) : shortcut;
848
- }
849
- if (isTerminalBusy(chatId)) {
850
- await ctx.reply("Claude Code is busy. Wait for the current response.");
851
- return;
852
- }
853
- const who = ctx.from?.username ? `@${ctx.from.username}` : ctx.from?.first_name ?? "user";
854
- cclog("→", `[${who}] ${text.slice(0, 120)}`);
855
- const draft = createDraftStream(chatId, ctx.api);
856
- let pendingText = "";
857
- const setReaction = async (emoji) => {
858
- try {
859
- const reactions = emoji ? [{ type: "emoji", emoji: emoji }] : [];
860
- await ctx.api.setMessageReaction(chatId, ctx.message.message_id, reactions);
861
- }
862
- catch { }
863
- };
864
- try {
865
- await setReaction("eyes");
866
- await ctx.replyWithChatAction("typing");
867
- // Keep "typing" alive every 4s while Claude Code runs
868
- const typingInterval = setInterval(() => {
869
- ctx.replyWithChatAction("typing").catch(() => { });
870
- }, 4000);
871
- let result;
872
- try {
873
- result = await handleTerminalMessage(chatId, text, (event) => {
874
- if (event.type === "text_delta" && event.text) {
875
- pendingText += event.text;
876
- draft.update(pendingText);
877
- }
878
- else if (event.type === "thinking_start") {
879
- cclog("..", "Thinking...");
880
- setReaction("thought_balloon").catch(() => { });
881
- }
882
- else if (event.type === "tool_use") {
883
- cclog("⚡", `Tool: ${event.toolName ?? "unknown"}`);
884
- setReaction("wrench").catch(() => { });
885
- }
886
- });
887
- }
888
- finally {
889
- clearInterval(typingInterval);
890
- }
891
- // Use streamed text if available, fall back to result.response
892
- const response = pendingText || result.response || "(no response)";
893
- draft.update(response);
894
- await draft.flush();
895
- const streamMsgId = draft.getMessageId();
896
- if (streamMsgId && response.length > 4096) {
897
- try {
898
- await ctx.api.deleteMessage(chatId, streamMsgId);
899
- }
900
- catch { }
901
- await sendChunked(ctx, response);
902
- }
903
- else if (!streamMsgId) {
904
- await sendChunked(ctx, response);
905
- }
906
- cclog("←", `[claude] ${response.replace(/\n/g, " ").slice(0, 120)}`);
907
- await setReaction("");
908
- }
909
- catch (err) {
910
- const errMsg = err instanceof Error ? err.message : String(err);
911
- cclog("✗", `Error: ${errMsg}`);
912
- slog.error("terminal", "Claude Code failed", { chatId, error: errMsg });
913
- const streamMsgId = draft.getMessageId();
914
- if (streamMsgId) {
915
- try {
916
- await ctx.api.editMessageText(chatId, streamMsgId, `Error: ${errMsg}`);
917
- }
918
- catch {
919
- await ctx.reply(`Error: ${errMsg}`);
920
- }
921
- }
922
- else {
923
- await ctx.reply(`Error: ${errMsg}`);
924
- }
925
- await setReaction("");
926
- }
927
- }
928
- // ─── Admin-only commands: redirect to admin bot ──────────────────
929
- const adminRedirect = async (ctx) => {
930
- const config = getConfig();
931
- const adminEntry = Object.entries(config.agents).find(([, a]) => a.admin);
932
- const adminState = adminEntry ? activeBots.get(adminEntry[0]) : undefined;
933
- const adminUsername = adminState?.botInfo?.username;
934
- if (adminUsername) {
935
- await ctx.reply(`This is an admin command. Use it in @${adminUsername}`);
936
- }
937
- else {
938
- await ctx.reply("This command is only available in the admin bot.");
939
- }
940
- };
941
- b.command("agents", adminRedirect);
942
- b.command("soul", adminRedirect);
943
- b.command("sessions", adminRedirect);
944
- b.command("config", adminRedirect);
945
- b.command("setup", adminRedirect);
946
- b.command("newagent", adminRedirect);
947
- b.command("deleteagent", adminRedirect);
948
- b.command("pairing", adminRedirect);
949
- b.command("restart", adminRedirect);
950
- // ─── Approval callback queries ────────────────────────────────────
951
- b.callbackQuery(/^approve:(.+):(.+)$/, async (ctx) => {
952
- const match = ctx.callbackQuery.data.match(/^approve:(.+):(.+)$/);
953
- if (!match)
954
- return;
955
- const [, approvalId, decision] = match;
956
- const resolved = submitDecision(approvalId, decision);
957
- if (resolved) {
958
- const label = decision === "allow-once" ? "Allowed" : decision === "allow-always" ? "Always allowed" : "Denied";
959
- await ctx.editMessageText(`${ctx.callbackQuery.message?.text ?? ""}\n\n-> ${label}`);
960
- }
961
- await ctx.answerCallbackQuery();
962
- });
963
- // ─── Shared message handler ─────────────────────────────────────────
964
- async function handleIncoming(ctx, cleanText) {
965
- const config = getConfig();
966
- const agent = getAgent(ctx.chat.id);
967
- const sessionId = sid(ctx.chat.id);
968
- const chatContext = isGroupChat(ctx.chat.type)
969
- ? ctx.chat.title
970
- : ctx.from?.first_name;
971
- const label = chatContext ? `${agent.name}: ${chatContext}` : agent.name;
972
- slog.info("telegram", "Incoming message", { agent: agent.name, sessionId, text: cleanText.slice(0, 160) });
973
- if (isRunActive(sessionId)) {
974
- await queueOrProcess(sessionId, cleanText);
975
- return;
976
- }
977
- const abortController = new AbortController();
978
- const client = createClient(config);
979
- const setReaction = async (emoji) => {
980
- try {
981
- const reactions = emoji ? [{ type: "emoji", emoji: emoji }] : [];
982
- await ctx.api.setMessageReaction(ctx.chat.id, ctx.message.message_id, reactions);
983
- }
984
- catch { }
985
- };
986
- const draft = createDraftStream(ctx.chat.id, ctx.api);
987
- let pendingText = "";
988
- try {
989
- await setReaction("eyes");
990
- await ctx.replyWithChatAction("typing");
991
- await setReaction("thinking_face");
992
- const result = await orchestrate({
993
- sessionId,
994
- message: cleanText,
995
- config,
996
- systemPrompt: agent.systemPrompt,
997
- client,
998
- signal: abortController.signal,
999
- agentId: agentId === "telegram" ? undefined : agentId,
1000
- label,
1001
- model: agent.model,
1002
- agentSystemPrompt: agent.systemPrompt,
1003
- thinking: agent.thinking,
1004
- effort: agent.effort,
1005
- onRetry: async (attempt, kind) => {
1006
- await alertAdmin(`⚠️ ${agent.name}: ${kind} (attempt ${attempt + 1}/${config.retry.maxRetries})`);
1007
- },
1008
- onError: async (err, kind) => {
1009
- const isFatal = kind === "auth" || kind === "billing";
1010
- const icon = isFatal ? "🚨" : "⚠️";
1011
- await alertAdmin(`${icon} ${agent.name}: ${kind} — ${err.message.slice(0, 200)}`);
1012
- },
1013
- onEvent: async (event) => {
1014
- if (event.type === "stream_text") {
1015
- pendingText += event.text;
1016
- draft.update(pendingText);
1017
- }
1018
- else if (event.type === "chunk") {
1019
- pendingText = event.text;
1020
- draft.update(pendingText);
1021
- }
1022
- else if (event.type === "tool_call") {
1023
- await setReaction("wrench");
1024
- }
1025
- else if (event.type === "thinking") {
1026
- if (event.state === "start")
1027
- await setReaction("thought_balloon");
1028
- }
1029
- else if (event.type === "subagent_start") {
1030
- await setReaction("wrench");
1031
- }
1032
- else if (event.type === "approval_request") {
1033
- await setReaction("lock");
1034
- const keyboard = new InlineKeyboard()
1035
- .text("Allow", `approve:${event.id}:allow-once`)
1036
- .text("Always", `approve:${event.id}:allow-always`)
1037
- .text("Deny", `approve:${event.id}:deny`);
1038
- try {
1039
- await ctx.api.sendMessage(ctx.chat.id, `${event.toolName}\n${event.preview}`, {
1040
- reply_markup: keyboard,
1041
- });
1042
- }
1043
- catch { /* best effort */ }
1044
- }
1045
- },
1046
- });
1047
- const response = result.response || "(no response)";
1048
- slog.info("telegram", "Response sent", { agent: agent.name, sessionId, text: response.slice(0, 160) });
1049
- pendingText = response;
1050
- draft.update(response);
1051
- await draft.flush();
1052
- const streamMsgId = draft.getMessageId();
1053
- if (streamMsgId && response.length > 4096) {
1054
- try {
1055
- await ctx.api.deleteMessage(ctx.chat.id, streamMsgId);
1056
- }
1057
- catch { }
1058
- await sendChunked(ctx, response);
1059
- }
1060
- else if (!streamMsgId) {
1061
- await sendChunked(ctx, response);
1062
- }
1063
- await setReaction("");
1064
- }
1065
- catch (err) {
1066
- const errMsg = err instanceof Error ? err.message : String(err);
1067
- slog.error("telegram", "Agent run failed", { agent: agent.name, sessionId, error: errMsg });
1068
- const streamMsgId = draft.getMessageId();
1069
- if (streamMsgId) {
1070
- try {
1071
- await ctx.api.editMessageText(ctx.chat.id, streamMsgId, `Error: ${errMsg}`);
1072
- }
1073
- catch {
1074
- await ctx.reply(`Error: ${errMsg}`);
1075
- }
1076
- }
1077
- else {
1078
- await ctx.reply(`Error: ${errMsg}`);
1079
- }
1080
- await setReaction("");
1081
- }
1082
- }
1083
- // ─── /voice command (redirect to admin) ────────────────────────────
1084
- b.command("voice", async (ctx) => {
1085
- const config = getConfig();
1086
- if (config.voice.enabled) {
1087
- await ctx.reply("Voice is enabled. Send a voice message and I'll transcribe it.");
1088
- }
1089
- else {
1090
- const adminEntry = Object.entries(config.agents).find(([, a]) => a.admin);
1091
- const adminState = adminEntry ? activeBots.get(adminEntry[0]) : undefined;
1092
- const adminUsername = adminState?.botInfo?.username;
1093
- const hint = adminUsername
1094
- ? `Voice not configured. Set it up in @${adminUsername} with /voice`
1095
- : "Voice transcription is not configured.";
1096
- await ctx.reply(hint);
1097
- }
1098
- });
1099
- // ─── Text message handler ─────────────────────────────────────────
1100
- // Handle wizard callback queries (inline button selections)
1101
- b.on("callback_query:data", async (ctx) => {
1102
- const data = ctx.callbackQuery.data;
1103
- if (data.startsWith("wizard:")) {
1104
- await ctx.answerCallbackQuery();
1105
- const value = data.split(":").slice(2).join(":");
1106
- await advanceWizard(ctx.chat.id, value, b);
1107
- }
1108
- else if (data.startsWith("browse:")) {
1109
- await ctx.answerCallbackQuery();
1110
- const value = data.slice("browse:".length);
1111
- await handleBrowseCallback(ctx.chat.id, value, ctx.api);
1112
- }
1113
- else if (data.startsWith("cc:")) {
1114
- await ctx.answerCallbackQuery();
1115
- const action = data.slice("cc:".length);
1116
- const chatId = ctx.chat.id;
1117
- if (action === "start") {
1118
- ccPaused.delete(chatId);
1119
- startTerminal(chatId, ccResolveWorkDir());
1120
- await setCommandMenu(true, chatId);
1121
- await ccPinStatus(chatId, ctx.api, true);
1122
- await ctx.editMessageText("Claude Code started. Send messages.");
1123
- }
1124
- else if (action === "stop") {
1125
- await ccPinStatus(chatId, ctx.api, false);
1126
- endTerminal(chatId);
1127
- ccPaused.add(chatId);
1128
- await setCommandMenu(false, chatId);
1129
- await ctx.editMessageText("Claude Code stopped. Use /claudecode to start again.");
1130
- }
1131
- else if (action === "new") {
1132
- // Start fresh session (clear old sessionId)
1133
- ccPaused.delete(chatId);
1134
- startTerminal(chatId, ccResolveWorkDir());
1135
- await setCommandMenu(true, chatId);
1136
- await ccPinStatus(chatId, ctx.api, true);
1137
- await ctx.editMessageText("New Claude Code session started.");
1138
- }
1139
- else if (action === "sessions") {
1140
- const ccSessions = listClaudeSessions(ccResolveWorkDir());
1141
- if (ccSessions.length === 0) {
1142
- await ctx.editMessageText("No previous Claude Code sessions found.");
1143
- return;
1144
- }
1145
- const home = os.homedir();
1146
- const kb = new InlineKeyboard();
1147
- for (const s of ccSessions) {
1148
- const dir = s.cwd ? s.cwd.replace(home, "~") : "";
1149
- const label = s.name ?? `${s.id.slice(0, 8)} ${dir}`;
1150
- kb.text(label, `cc:resume:${s.id}`).row();
1151
- }
1152
- kb.text("⬅ Back", "cc:back");
1153
- await ctx.editMessageText("Resume a session:", { reply_markup: kb });
1154
- }
1155
- else if (action === "workdir") {
1156
- const config = getConfig();
1157
- const agentConfig = config.agents[agentId];
1158
- const currentDir = agentConfig?.workDir
1159
- ? expandHome(agentConfig.workDir)
1160
- : os.homedir();
1161
- await startBrowse(chatId, ctx.api, currentDir, (selectedDir) => {
1162
- const agents = { ...config.agents };
1163
- agents[agentId] = { ...agents[agentId], workDir: selectedDir };
1164
- saveConfig({ agents });
1165
- if (hasTerminal(chatId)) {
1166
- updateWorkDir(chatId, selectedDir);
1167
- }
1168
- });
1169
- }
1170
- else if (action === "effortmenu") {
1171
- const current = getTerminalSetting(chatId, "effort") ?? "default";
1172
- const kb = new InlineKeyboard()
1173
- .text("Low", "cc:effort:low").text("Medium", "cc:effort:medium").row()
1174
- .text("High", "cc:effort:high").text("Max", "cc:effort:max").row()
1175
- .text("⬅ Back", "cc:back");
1176
- await ctx.editMessageText(`Effort: ${current}\n\nSelect level:`, { reply_markup: kb });
1177
- }
1178
- else if (action === "approvalsmenu") {
1179
- const current = getTerminalSetting(chatId, "permissionMode") ?? "skip";
1180
- const label = current === "acceptEdits" ? "Accept Edits" : "Skip All";
1181
- const kb = new InlineKeyboard()
1182
- .text("Skip All", "cc:setapprovals:skip").text("Accept Edits", "cc:setapprovals:acceptEdits").row()
1183
- .text("⬅ Back", "cc:back");
1184
- await ctx.editMessageText(`Approvals: ${label}\n\nSkip All — no checks, everything auto-approved\nAccept Edits — auto-accepts reads/edits, blocks dangerous commands`, { reply_markup: kb });
1185
- }
1186
- else if (action.startsWith("setapprovals:")) {
1187
- const mode = action.slice("setapprovals:".length);
1188
- setTerminalSetting(chatId, "permissionMode", mode);
1189
- const label = mode === "acceptEdits" ? "Accept Edits" : "Skip All";
1190
- await ctx.editMessageText(`Approvals set to: ${label}`);
1191
- }
1192
- else if (action.startsWith("tool:")) {
1193
- const tool = action.slice("tool:".length);
1194
- const CC_TOOLS = ["Bash", "Read", "Write", "Edit", "Glob", "Grep", "WebSearch", "WebFetch", "Agent"];
1195
- if (tool === "__reset__") {
1196
- setTerminalSetting(chatId, "disallowedTools", undefined);
1197
- setTerminalSetting(chatId, "allowedTools", undefined);
1198
- const kb = new InlineKeyboard();
1199
- for (let i = 0; i < CC_TOOLS.length; i++) {
1200
- kb.text(`✅ ${CC_TOOLS[i]}`, `cc:tool:${CC_TOOLS[i]}`);
1201
- if ((i + 1) % 3 === 0)
1202
- kb.row();
1203
- }
1204
- kb.row().text("Reset All", "cc:tool:__reset__");
1205
- await ctx.editMessageText("All tools enabled\n\nTap to toggle:", { reply_markup: kb });
1206
- }
1207
- else {
1208
- const denied = new Set(getTerminalSetting(chatId, "disallowedTools") ?? []);
1209
- if (denied.has(tool)) {
1210
- denied.delete(tool);
1211
- }
1212
- else {
1213
- denied.add(tool);
1214
- }
1215
- setTerminalSetting(chatId, "disallowedTools", denied.size ? [...denied] : undefined);
1216
- const kb = new InlineKeyboard();
1217
- for (let i = 0; i < CC_TOOLS.length; i++) {
1218
- const t = CC_TOOLS[i];
1219
- const icon = denied.has(t) ? "🚫" : "✅";
1220
- kb.text(`${icon} ${t}`, `cc:tool:${t}`);
1221
- if ((i + 1) % 3 === 0)
1222
- kb.row();
1223
- }
1224
- kb.row().text("Reset All", "cc:tool:__reset__");
1225
- const status = denied.size ? `Blocked: ${[...denied].join(", ")}` : "All tools enabled";
1226
- await ctx.editMessageText(`${status}\n\nTap to toggle:`, { reply_markup: kb });
1227
- }
1228
- }
1229
- else if (action === "model") {
1230
- const current = getTerminalModel(chatId) ?? "default";
1231
- const kb = new InlineKeyboard()
1232
- .text("Sonnet 4.6", "cc:setmodel:claude-sonnet-4-6").text("Opus 4.6", "cc:setmodel:claude-opus-4-6").row()
1233
- .text("Haiku 4.5", "cc:setmodel:claude-haiku-4-5-20251001").row()
1234
- .text("Default", "cc:setmodel:__default__").row()
1235
- .text("⬅ Back", "cc:back");
1236
- await ctx.editMessageText(`Current model: ${current}\n\nSelect model:`, { reply_markup: kb });
1237
- }
1238
- else if (action.startsWith("setmodel:")) {
1239
- const model = action.slice("setmodel:".length);
1240
- if (model === "__default__") {
1241
- setTerminalModel(chatId, undefined);
1242
- await ctx.editMessageText("Model reset to default.");
1243
- }
1244
- else {
1245
- setTerminalModel(chatId, model);
1246
- await ctx.editMessageText(`Model set to: ${model}`);
1247
- }
1248
- }
1249
- else if (action.startsWith("effort:")) {
1250
- const level = action.slice("effort:".length);
1251
- setTerminalSetting(chatId, "effort", level);
1252
- await ctx.editMessageText(`Effort set to: ${level}`);
1253
- }
1254
- else if (action === "back") {
1255
- // Re-show main menu
1256
- if (hasTerminal(chatId)) {
1257
- const model = getTerminalModel(chatId) ?? "default";
1258
- const kb = new InlineKeyboard()
1259
- .text("New Session", "cc:new").text("Stop", "cc:stop").row()
1260
- .text("Model", "cc:model").text("Sessions", "cc:sessions").row()
1261
- .text("Work Dir", "cc:workdir");
1262
- await ctx.editMessageText(`Claude Code active (${model})`, { reply_markup: kb });
1263
- }
1264
- else {
1265
- const kb = new InlineKeyboard()
1266
- .text("Start", "cc:start").text("Resume Session", "cc:sessions").row()
1267
- .text("Work Dir", "cc:workdir");
1268
- await ctx.editMessageText("Claude Code", { reply_markup: kb });
1269
- }
1270
- }
1271
- else if (action.startsWith("resume:")) {
1272
- const sessionId = action.slice("resume:".length);
1273
- ccPaused.delete(chatId);
1274
- startTerminal(chatId, ccResolveWorkDir(), sessionId);
1275
- await setCommandMenu(true, chatId);
1276
- await ccPinStatus(chatId, ctx.api, true);
1277
- await ctx.editMessageText(`Resumed session ${sessionId.slice(0, 8)}... Send messages.`);
1278
- }
1279
- }
1280
- });
1281
- b.on("message:text", async (ctx) => {
1282
- // Claude Code mode: manual (/cc) or config-driven (mode: claude-code)
1283
- if (hasTerminal(ctx.chat.id)) {
1284
- await handleTerminalIncoming(ctx);
1285
- return;
1286
- }
1287
- const agentCfg = getConfig().agents[agentId];
1288
- if (agentCfg?.mode === "claude-code" && !ccPaused.has(ctx.chat.id)) {
1289
- // Auto-start terminal session for claude-code agents (unless manually exited)
1290
- const workDir = agentCfg.workDir ? expandHome(agentCfg.workDir) : os.homedir() + "/Desktop";
1291
- startTerminal(ctx.chat.id, workDir);
1292
- if (agentCfg.ccApprovals === "acceptEdits") {
1293
- setTerminalSetting(ctx.chat.id, "permissionMode", "acceptEdits");
1294
- }
1295
- await setCommandMenu(true, ctx.chat.id);
1296
- await handleTerminalIncoming(ctx);
1297
- return;
1298
- }
1299
- // Advance active wizard with text input
1300
- if (hasActiveWizard(ctx.chat.id)) {
1301
- const consumed = await advanceWizard(ctx.chat.id, ctx.message.text, b);
1302
- if (consumed)
1303
- return;
1304
- }
1305
- const agent = getAgent(ctx.chat.id);
1306
- const text = ctx.message.text;
1307
- if (isGroupChat(ctx.chat.type) && agent.mentionOnly) {
1308
- const replyToBotId = ctx.message.reply_to_message?.from?.id;
1309
- if (!shouldRespondInGroup(text, replyToBotId, botInfo.id, botInfo.username)) {
1310
- return;
1311
- }
1312
- }
1313
- const cleanText = stripMention(text, botInfo.username);
1314
- if (!cleanText)
1315
- return;
1316
- await handleIncoming(ctx, cleanText);
1317
- });
1318
- // ─── Voice/audio message handler ──────────────────────────────────
1319
- b.on(["message:voice", "message:audio"], async (ctx) => {
1320
- const config = getConfig();
1321
- if (!config.voice.enabled || !config.voice.apiKey) {
1322
- const adminEntry = Object.entries(config.agents).find(([, a]) => a.admin);
1323
- const hint = adminEntry
1324
- ? "Voice transcription is not configured. Use /voice in the admin bot to enable it."
1325
- : "Voice transcription is not configured.";
1326
- await ctx.reply(hint);
1327
- return;
1328
- }
1329
- try {
1330
- await ctx.replyWithChatAction("typing");
1331
- const fileId = ctx.message.voice?.file_id ?? ctx.message.audio?.file_id;
1332
- if (!fileId) {
1333
- await ctx.reply("Could not read audio file.");
1334
- return;
1335
- }
1336
- const file = await ctx.api.getFile(fileId);
1337
- const fileUrl = `https://api.telegram.org/file/bot${botToken}/${file.file_path}`;
1338
- const resp = await fetch(fileUrl);
1339
- if (!resp.ok) {
1340
- await ctx.reply("Failed to download voice file.");
1341
- return;
1342
- }
1343
- const buffer = Buffer.from(await resp.arrayBuffer());
1344
- if (buffer.length > 20 * 1024 * 1024) {
1345
- await ctx.reply("Audio file too large (max 20 MB).");
1346
- return;
1347
- }
1348
- const result = await transcribe(buffer, {
1349
- enabled: true,
1350
- provider: config.voice.provider,
1351
- apiKey: config.voice.apiKey,
1352
- model: config.voice.model,
1353
- language: config.voice.language,
1354
- });
1355
- if (!result.text?.trim()) {
1356
- await ctx.reply("(could not transcribe — empty result)");
1357
- return;
1358
- }
1359
- const cleanText = `[Voice] ${result.text.trim()}`;
1360
- slog.info("telegram", "Voice transcribed", {
1361
- agent: agentId, text: cleanText.slice(0, 160), provider: config.voice.provider,
1362
- });
1363
- await handleIncoming(ctx, cleanText);
1364
- }
1365
- catch (err) {
1366
- const errMsg = err instanceof Error ? err.message : String(err);
1367
- slog.error("telegram", "Voice processing failed", { agent: agentId, error: errMsg });
1368
- await ctx.reply(`Voice error: ${errMsg}`);
1369
- }
1370
- });
1371
- b.catch((err) => {
1372
- slog.error("telegram", "Bot error", { agent: agentId, error: err.message ?? String(err) });
1373
- });
73
+ await gc.reply(`Access requested. Waiting for admin approval.\nCode: ${request.code}`);
74
+ notifyAdminOfPairing(request, ctx.getConfig(), activeBots);
75
+ });
76
+ // ─── Register modules (order matters for grammy middleware chain) ──
77
+ registerClaudeCode(ctx); // command mode guard + CC commands/callbacks
78
+ registerCommands(ctx); // standard commands
79
+ registerMessageHandlers(ctx); // text/voice/callback handlers (must be last)
80
+ // ─── Start polling ────────────────────────────────────────────────
1374
81
  startPolling(b, agentId);
1375
82
  }
1376
83
  //# sourceMappingURL=agent-bot.js.map