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,84 +1,11 @@
1
- // Admin bot: BotFather-style Telegram control plane for CamelAGI
2
- import { Bot, InlineKeyboard } from "grammy";
3
- import fs from "node:fs";
4
- import path from "node:path";
5
- import { saveConfig, loadConfig } from "../core/config.js";
6
- import { agentMemoryDir } from "../workspace.js";
7
- import { listSessions, deleteSession, loadMessages } from "../session.js";
8
- import { formatTokens, aggregateAgentUsage, formatCost } from "../usage.js";
9
- import { getActiveBotIds, startBot, stopBot } from "../telegram.js";
10
- import { startWizard, advanceWizard, cancelWizard, hasActiveWizard } from "./wizard.js";
11
- import { createSetupWizard, createNewAgentWizard, createMcpAddWizard, createCloneWizard } from "./wizards.js";
12
- import { resolvePreset } from "../core/models.js";
13
- import { createVoiceWizard, createVoiceResetWizard } from "./voice-wizard.js";
14
- import { formatAge } from "./helpers.js";
15
- import { approveRequest, denyRequest, listPendingRequests, hasPendingRequest, createPairingRequest } from "./pairing.js";
16
- import { notifyUserApproved, notifyUserOfDenial } from "./pairing-notify.js";
1
+ // Admin bot: BotFather-style Telegram control plane setup + wiring
2
+ import { Bot } from "grammy";
3
+ import { loadConfig } from "../core/config.js";
4
+ import { advanceWizard, hasActiveWizard, cancelWizard } from "./wizard.js";
17
5
  import { isGroupChat } from "./helpers.js";
18
- import { approveBotApproval, denyBotApproval, } from "./bot-approval.js";
19
- // ─── Helpers ─────────────────────────────────────────────────────────
20
- async function showSoul(chatId, targetId, edit, bot) {
21
- const soulPath = path.join(agentMemoryDir(targetId), "SOUL.md");
22
- if (edit) {
23
- const editWizard = {
24
- id: "soul-edit",
25
- steps: [{
26
- id: "content",
27
- prompt: `Send the new SOUL.md content for "${targetId}".\nCurrent content will be replaced. /cancel to abort.`,
28
- }],
29
- onComplete: async (data) => {
30
- const dir = agentMemoryDir(targetId);
31
- fs.mkdirSync(dir, { recursive: true });
32
- fs.writeFileSync(soulPath, data.content);
33
- return `SOUL.md updated for "${targetId}".\n\n${data.content.slice(0, 200)}${data.content.length > 200 ? "..." : ""}`;
34
- },
35
- };
36
- await startWizard(chatId, editWizard, bot);
37
- return;
38
- }
39
- if (!fs.existsSync(soulPath)) {
40
- await bot.api.sendMessage(chatId, `No SOUL.md for "${targetId}" yet.`);
41
- return;
42
- }
43
- const content = fs.readFileSync(soulPath, "utf-8").trim();
44
- const preview = content.length > 3800 ? content.slice(0, 3800) + "\n\n[truncated]" : content;
45
- const kb = new InlineKeyboard()
46
- .text("Edit", `picksoul:edit:${targetId}`);
47
- await bot.api.sendMessage(chatId, `SOUL.md (${targetId}):\n\n${preview}`, { reply_markup: kb });
48
- }
49
- async function showAgentConfig(chatId, agentId, config, bot) {
50
- const agent = config.agents[agentId];
51
- if (!agent)
52
- return;
53
- const model = agent.model ?? config.model;
54
- const thinking = agent.thinking ?? config.thinking;
55
- const effort = agent.effort ?? config.effort;
56
- const maxTurns = agent.maxTurns ?? config.maxTurns;
57
- const mcpCount = agent.mcp ? Object.keys(agent.mcp.servers).length : 0;
58
- const runningBots = getActiveBotIds();
59
- const running = runningBots.includes(agentId);
60
- const statusIcon = running ? "🟢" : agent.telegram?.botToken ? "🔴" : "⚪";
61
- const briefMode = agent.telegram?.briefMode ?? true;
62
- const lines = [
63
- `${statusIcon} ${agent.name} (${agentId})\n`,
64
- `Model: ${model}`,
65
- `Thinking: ${thinking}`,
66
- `Effort: ${effort}`,
67
- `Max Turns: ${maxTurns}`,
68
- `Brief: ${briefMode ? "on" : "off"}`,
69
- mcpCount > 0 ? `MCP: ${mcpCount} server${mcpCount > 1 ? "s" : ""}` : `MCP: none`,
70
- ];
71
- const kb = new InlineKeyboard()
72
- .text("Model", `ae:model:${agentId}`)
73
- .text("Thinking", `ae:think:${agentId}`)
74
- .text("Effort", `ae:effort:${agentId}`)
75
- .row()
76
- .text("Max Turns", `ae:turns:${agentId}`)
77
- .text(`Brief: ${briefMode ? "on" : "off"}`, `ae:brief:${agentId}`)
78
- .text("Clone", `ae:clone:${agentId}`);
79
- await bot.api.sendMessage(chatId, lines.join("\n"), { reply_markup: kb });
80
- }
81
- // ─── Setup Admin Bot ─────────────────────────────────────────────────
6
+ import { hasPendingRequest, createPairingRequest } from "../extensions/pairing.js";
7
+ import { registerAdminCommands } from "./admin-commands.js";
8
+ import { registerAdminAgents } from "./admin-agents.js";
82
9
  export async function setupAdminBot(agentId, botToken, getConfig, getSystemPrompt, activeBots) {
83
10
  const b = new Bot(botToken);
84
11
  await b.api.setMyCommands([
@@ -99,13 +26,12 @@ export async function setupAdminBot(agentId, botToken, getConfig, getSystemPromp
99
26
  { command: "voice", description: "Configure voice transcription" },
100
27
  { command: "cancel", description: "Cancel active wizard" },
101
28
  ]).catch(() => { });
102
- /** Check if userId is allowed for this agent */
29
+ // ─── Access control ─────────────────────────────────────────────────
103
30
  function isUserAllowed(userId) {
104
31
  const memAgent = getConfig().agents[agentId];
105
32
  const memAllowed = memAgent?.telegram?.allowedUsers ?? [];
106
33
  if (memAllowed.includes(userId))
107
34
  return true;
108
- // Fallback: read config file directly (handles hot-reload delay)
109
35
  try {
110
36
  const freshConfig = loadConfig();
111
37
  const freshAgent = freshConfig.agents[agentId];
@@ -116,7 +42,6 @@ export async function setupAdminBot(agentId, botToken, getConfig, getSystemPromp
116
42
  catch { }
117
43
  return false;
118
44
  }
119
- // Access control — admin approves, user gets instant access
120
45
  b.use(async (ctx, next) => {
121
46
  const userId = ctx.from?.id;
122
47
  if (!userId)
@@ -125,22 +50,19 @@ export async function setupAdminBot(agentId, botToken, getConfig, getSystemPromp
125
50
  await next();
126
51
  return;
127
52
  }
128
- // Groups: silent reject for unauthorized users
129
53
  if (ctx.chat && isGroupChat(ctx.chat.type))
130
54
  return;
131
- // Check if user already has a pending request
132
55
  const pending = hasPendingRequest(userId, agentId);
133
56
  if (pending) {
134
57
  await ctx.reply(`Your access request is pending approval.\nCode: ${pending.code}`);
135
58
  return;
136
59
  }
137
- // Create new pairing request
138
60
  const request = createPairingRequest(userId, agentId, ctx.chat.id, ctx.from?.username, ctx.from?.first_name);
139
61
  const who = ctx.from?.username ? `@${ctx.from.username}` : ctx.from?.first_name ?? String(userId);
140
62
  console.log(`\n \x1b[33mPairing request from ${who}\x1b[0m\n \x1b[90mRun: camel pairing\x1b[0m\n`);
141
63
  await ctx.reply(`Access requested. Waiting for approval...\nCode: ${request.code}`);
142
64
  });
143
- // ─── Callback queries ─────────────────────────────────────────────
65
+ // ─── Wizard callback queries ────────────────────────────────────────
144
66
  b.callbackQuery(/^wizard:(.+):(.+)$/, async (ctx) => {
145
67
  const match = ctx.callbackQuery.data.match(/^wizard:(.+):(.+)$/);
146
68
  if (!match)
@@ -149,7 +71,6 @@ export async function setupAdminBot(agentId, botToken, getConfig, getSystemPromp
149
71
  const chatId = ctx.chat?.id;
150
72
  if (!chatId)
151
73
  return;
152
- // Show the button label (not raw value like __default__)
153
74
  const buttonText = ctx.callbackQuery.message && "reply_markup" in ctx.callbackQuery.message
154
75
  ? ctx.callbackQuery.message.reply_markup?.inline_keyboard
155
76
  ?.flat()
@@ -164,852 +85,10 @@ export async function setupAdminBot(agentId, botToken, getConfig, getSystemPromp
164
85
  await ctx.answerCallbackQuery();
165
86
  await advanceWizard(chatId, value, b);
166
87
  });
167
- b.callbackQuery(/^picksoul:(.+):(.+)$/, async (ctx) => {
168
- const match = ctx.callbackQuery.data.match(/^picksoul:(.+):(.+)$/);
169
- if (!match)
170
- return;
171
- const [, action, id] = match;
172
- await ctx.answerCallbackQuery();
173
- try {
174
- await ctx.editMessageText(`-> ${id}`);
175
- }
176
- catch { }
177
- await showSoul(ctx.chat.id, id, action === "edit", b);
178
- });
179
- b.callbackQuery(/^pickdelete:(.+)$/, async (ctx) => {
180
- const match = ctx.callbackQuery.data.match(/^pickdelete:(.+)$/);
181
- if (!match)
182
- return;
183
- const id = match[1];
184
- await ctx.answerCallbackQuery();
185
- const config = getConfig();
186
- if (!config.agents[id]) {
187
- try {
188
- await ctx.editMessageText("Agent not found.");
189
- }
190
- catch { }
191
- return;
192
- }
193
- const kb = new InlineKeyboard()
194
- .text("Yes, delete", `confirm:delete:${id}`)
195
- .text("Cancel", `confirm:cancel:${id}`);
196
- try {
197
- await ctx.editMessageText(`Delete "${config.agents[id].name}" (${id})?\n\nBot will be stopped. Workspace files are preserved.`, { reply_markup: kb });
198
- }
199
- catch { }
200
- });
201
- b.callbackQuery(/^confirm:(.+):(.+)$/, async (ctx) => {
202
- const match = ctx.callbackQuery.data.match(/^confirm:(.+):(.+)$/);
203
- if (!match)
204
- return;
205
- const [, action, param] = match;
206
- try {
207
- await ctx.editMessageText(`${ctx.callbackQuery.message?.text ?? ""}\n\n-> ${action === "delete" ? "Deleting..." : "Cancelled"}`);
208
- }
209
- catch { }
210
- await ctx.answerCallbackQuery();
211
- if (action === "delete") {
212
- const config = getConfig();
213
- if (config.agents[param]) {
214
- stopBot(param);
215
- const agents = { ...config.agents };
216
- delete agents[param];
217
- saveConfig({ agents });
218
- await ctx.reply(`Agent "${param}" deleted.\nWorkspace files preserved at ${agentMemoryDir(param)}`);
219
- }
220
- }
221
- });
222
- b.callbackQuery(/^clearsessions:(.+)$/, async (ctx) => {
223
- const match = ctx.callbackQuery.data.match(/^clearsessions:(.+)$/);
224
- if (!match)
225
- return;
226
- const [, period] = match;
227
- const cutoff = { "1d": 86400000, "1w": 604800000, "1m": 2592000000 }[period] ?? 604800000;
228
- const now = Date.now();
229
- const sessions = listSessions();
230
- let deleted = 0;
231
- for (const s of sessions) {
232
- if (now - s.createdAt > cutoff) {
233
- deleteSession(s.id);
234
- deleted++;
235
- }
236
- }
237
- try {
238
- await ctx.editMessageText(`${ctx.callbackQuery.message?.text ?? ""}\n\n-> Deleted ${deleted} sessions`);
239
- }
240
- catch { }
241
- await ctx.answerCallbackQuery();
242
- });
243
- // ─── Pairing callbacks ───────────────────────────────────────────
244
- b.callbackQuery(/^pairing:(approve|deny):(.+)$/, async (ctx) => {
245
- const match = ctx.callbackQuery.data.match(/^pairing:(approve|deny):(.+)$/);
246
- if (!match)
247
- return;
248
- const [, action, code] = match;
249
- if (action === "approve") {
250
- const request = approveRequest(code);
251
- if (request) {
252
- try {
253
- await ctx.editMessageText(`${ctx.callbackQuery.message?.text ?? ""}\n\n-> Approved`);
254
- }
255
- catch { }
256
- await notifyUserApproved(request, activeBots);
257
- }
258
- else {
259
- try {
260
- await ctx.editMessageText("Request expired or already handled.");
261
- }
262
- catch { }
263
- }
264
- }
265
- else {
266
- const request = denyRequest(code);
267
- if (request) {
268
- try {
269
- await ctx.editMessageText(`${ctx.callbackQuery.message?.text ?? ""}\n\n-> Denied`);
270
- }
271
- catch { }
272
- await notifyUserOfDenial(request, activeBots);
273
- }
274
- else {
275
- try {
276
- await ctx.editMessageText("Request expired or already handled.");
277
- }
278
- catch { }
279
- }
280
- }
281
- await ctx.answerCallbackQuery();
282
- });
283
- // ─── Bot approval callbacks ─────────────────────────────────────
284
- b.callbackQuery(/^botapproval:(approve|deny):(.+)$/, async (ctx) => {
285
- const match = ctx.callbackQuery.data.match(/^botapproval:(approve|deny):(.+)$/);
286
- if (!match)
287
- return;
288
- const [, action, agentIdParam] = match;
289
- if (action === "approve") {
290
- const approval = approveBotApproval(agentIdParam);
291
- if (approval) {
292
- const botLabel = approval.botUsername ? `@${approval.botUsername}` : agentIdParam;
293
- try {
294
- const sysPrompt = getSystemPrompt();
295
- await startBot(agentIdParam, approval.botToken, getConfig, () => sysPrompt);
296
- }
297
- catch (err) {
298
- // "already running" is fine — config hot-reload may have started it
299
- const errMsg = err instanceof Error ? err.message : String(err);
300
- if (!errMsg.includes("already running") && !errMsg.includes("already starting")) {
301
- try {
302
- await ctx.editMessageText(`Failed to start bot: ${errMsg}`);
303
- }
304
- catch { }
305
- await ctx.answerCallbackQuery();
306
- return;
307
- }
308
- }
309
- try {
310
- await ctx.editMessageText(`${ctx.callbackQuery.message?.text ?? ""}\n\n-> Approved. ${botLabel} is now running.`);
311
- }
312
- catch { }
313
- }
314
- else {
315
- try {
316
- await ctx.editMessageText("Approval not found or already handled.");
317
- }
318
- catch { }
319
- }
320
- }
321
- else {
322
- const approval = denyBotApproval(agentIdParam);
323
- if (approval) {
324
- try {
325
- await ctx.editMessageText(`${ctx.callbackQuery.message?.text ?? ""}\n\n-> Denied. Bot will not start.`);
326
- }
327
- catch { }
328
- }
329
- else {
330
- try {
331
- await ctx.editMessageText("Approval not found or already handled.");
332
- }
333
- catch { }
334
- }
335
- }
336
- await ctx.answerCallbackQuery();
337
- });
338
- // ─── Commands ─────────────────────────────────────────────────────
339
- b.command("start", async (ctx) => {
340
- const config = getConfig();
341
- const hasApiKey = !!config.apiKey;
342
- const agentCount = Object.keys(config.agents).length;
343
- await ctx.reply([
344
- "CamelAGI Admin\n",
345
- "I manage your AI agents from here.",
346
- "",
347
- hasApiKey ? `API configured (${config.provider}, ${config.model})` : "No API key — run /setup first",
348
- `${agentCount} agent(s) configured`,
349
- "",
350
- "Commands:",
351
- "/setup — configure API provider & key",
352
- "/newagent — create an agent",
353
- "/agents — list agents",
354
- "/help — all commands",
355
- ].join("\n"));
356
- });
357
- b.command("help", async (ctx) => {
358
- await ctx.reply([
359
- "CamelAGI Admin Commands\n",
360
- "Setup & Config:",
361
- " /setup — configure API provider, key, model",
362
- " /config — view current config",
363
- " /config <key> <value> — update config",
364
- "",
365
- "Agents:",
366
- " /newagent — create a new agent",
367
- " /agents — list all agents",
368
- " /agent <id> — view/edit agent config",
369
- " /deleteagent — pick & delete an agent",
370
- " /soul — view/edit agent personality",
371
- "",
372
- "MCP:",
373
- " /mcp — manage MCP tool servers",
374
- "",
375
- "Access:",
376
- " /pairing — list pending access requests",
377
- "",
378
- "Sessions & Status:",
379
- " /sessions — list sessions",
380
- " /usage — per-agent usage & costs",
381
- " /status — system health",
382
- " /restart — restart all bots",
383
- "",
384
- " /cancel — cancel active wizard",
385
- ].join("\n"));
386
- });
387
- b.command("cancel", async (ctx) => {
388
- if (cancelWizard(ctx.chat.id)) {
389
- await ctx.reply("Wizard cancelled.");
390
- }
391
- else {
392
- await ctx.reply("No active wizard.");
393
- }
394
- });
395
- b.command("setup", async (ctx) => {
396
- await startWizard(ctx.chat.id, createSetupWizard(getConfig), b);
397
- });
398
- b.command("newagent", async (ctx) => {
399
- const config = getConfig();
400
- if (!config.apiKey) {
401
- await ctx.reply("No API key configured. Run /setup first.");
402
- return;
403
- }
404
- await startWizard(ctx.chat.id, createNewAgentWizard(getConfig, getSystemPrompt), b);
405
- });
406
- b.command("agents", async (ctx) => {
407
- const config = getConfig();
408
- const entries = Object.entries(config.agents);
409
- if (entries.length === 0) {
410
- await ctx.reply("No agents configured.\n\nUse /newagent to create one.");
411
- return;
412
- }
413
- const runningBots = getActiveBotIds();
414
- const lines = ["Your agents:\n"];
415
- const kb = new InlineKeyboard();
416
- let hasButtons = false;
417
- for (const [id, a] of entries) {
418
- const running = runningBots.includes(id);
419
- const botState = activeBots.get(id);
420
- const username = botState?.botInfo?.username;
421
- const statusIcon = running ? "🟢" : a.telegram?.botToken ? "🔴" : "⚪";
422
- lines.push(`${statusIcon} ${a.name} (${id})`);
423
- lines.push(` Model: ${a.model ?? config.model}`);
424
- if (running && username) {
425
- lines.push(` @${username}`);
426
- }
427
- else if (a.telegram?.botToken && !running) {
428
- lines.push(` Telegram: stopped`);
429
- }
430
- else if (!a.telegram?.botToken && !a.admin) {
431
- lines.push(` No channel configured`);
432
- }
433
- lines.push("");
434
- // Add chat link button for running bots (except admin)
435
- if (running && username && !a.admin) {
436
- kb.url(`💬 ${a.name}`, `https://t.me/${username}`);
437
- hasButtons = true;
438
- }
439
- }
440
- lines.push("Use /agent <id> to view/edit config");
441
- // Management buttons
442
- if (hasButtons)
443
- kb.row();
444
- kb.text("➕ New agent", "agents:newagent");
445
- const stoppedAgents = entries.filter(([id, a]) => !runningBots.includes(id) && a.telegram?.botToken && !a.admin);
446
- if (stoppedAgents.length > 0) {
447
- kb.text("🔄 Restart stopped", "agents:restart");
448
- }
449
- await ctx.reply(lines.join("\n"), { reply_markup: kb });
450
- });
451
- b.callbackQuery("agents:newagent", async (ctx) => {
452
- await ctx.answerCallbackQuery();
453
- const config = getConfig();
454
- if (!config.apiKey) {
455
- await ctx.reply("No API key configured. Run /setup first.");
456
- return;
457
- }
458
- await startWizard(ctx.chat.id, createNewAgentWizard(getConfig, getSystemPrompt), b);
459
- });
460
- b.callbackQuery("agents:restart", async (ctx) => {
461
- await ctx.answerCallbackQuery();
462
- const config = getConfig();
463
- const restarted = [];
464
- for (const [id, a] of Object.entries(config.agents)) {
465
- if (a.admin)
466
- continue;
467
- if (!a.telegram?.botToken)
468
- continue;
469
- if (getActiveBotIds().includes(id))
470
- continue;
471
- try {
472
- const sysPrompt = getSystemPrompt();
473
- await startBot(id, a.telegram.botToken, getConfig, () => sysPrompt);
474
- restarted.push(id);
475
- }
476
- catch { /* skip */ }
477
- }
478
- await ctx.reply(restarted.length > 0
479
- ? `Restarted: ${restarted.join(", ")}`
480
- : "No stopped bots to restart.");
481
- });
482
- b.command("deleteagent", async (ctx) => {
483
- const config = getConfig();
484
- const deletable = Object.entries(config.agents).filter(([, a]) => !a.admin);
485
- if (deletable.length === 0) {
486
- await ctx.reply("No agents to delete.");
487
- return;
488
- }
489
- const kb = new InlineKeyboard();
490
- for (const [id, a] of deletable) {
491
- kb.text(`${a.name}`, `pickdelete:${id}`).row();
492
- }
493
- await ctx.reply("Which agent do you want to delete?", { reply_markup: kb });
494
- });
495
- b.command("soul", async (ctx) => {
496
- const args = (ctx.match ?? "").trim().split(/\s+/);
497
- const targetId = args[0];
498
- const action = args[1];
499
- if (!targetId) {
500
- const config = getConfig();
501
- const entries = Object.entries(config.agents);
502
- if (entries.length === 0) {
503
- await ctx.reply("No agents. Use /newagent first.");
504
- return;
505
- }
506
- const kb = new InlineKeyboard();
507
- for (const [id, a] of entries) {
508
- kb.text(`${a.name}`, `picksoul:view:${id}`).row();
509
- }
510
- await ctx.reply("Which agent's SOUL.md?", { reply_markup: kb });
511
- return;
512
- }
513
- const config = getConfig();
514
- if (!config.agents[targetId]) {
515
- await ctx.reply(`Agent "${targetId}" not found.`);
516
- return;
517
- }
518
- await showSoul(ctx.chat.id, targetId, action === "edit", b);
519
- });
520
- b.command("config", async (ctx) => {
521
- const args = (ctx.match ?? "").trim();
522
- if (!args) {
523
- const config = getConfig();
524
- const lines = [
525
- "Current configuration:\n",
526
- `Provider: ${config.provider}`,
527
- `Model: ${config.model}`,
528
- config.baseUrl ? `Base URL: ${config.baseUrl}` : null,
529
- `API Key: ${config.apiKey ? "***" + config.apiKey.slice(-4) : "not set"}`,
530
- `Thinking: ${config.thinking}`,
531
- `Effort: ${config.effort}`,
532
- `Max Turns: ${config.maxTurns}`,
533
- `Timeout: ${config.timeoutSeconds}s`,
534
- `Approvals: ${config.approvals.mode}`,
535
- "",
536
- "Use /config <key> <value> to change",
537
- "Examples:",
538
- " /config model gpt-4o",
539
- " /config thinking high",
540
- " /config approvals.mode smart",
541
- ];
542
- await ctx.reply(lines.filter(Boolean).join("\n"));
543
- return;
544
- }
545
- const spaceIdx = args.indexOf(" ");
546
- if (spaceIdx === -1) {
547
- await ctx.reply(`Usage: /config <key> <value>\n\nExample: /config model gpt-4o`);
548
- return;
549
- }
550
- const key = args.slice(0, spaceIdx).trim();
551
- const value = args.slice(spaceIdx + 1).trim();
552
- const parts = key.split(".");
553
- let update;
554
- if (parts.length === 1) {
555
- let parsed = value;
556
- if (value === "true")
557
- parsed = true;
558
- else if (value === "false")
559
- parsed = false;
560
- else if (/^\d+$/.test(value))
561
- parsed = parseInt(value, 10);
562
- update = { [key]: parsed };
563
- }
564
- else {
565
- const config = getConfig();
566
- const topKey = parts[0];
567
- const subKey = parts.slice(1).join(".");
568
- const existing = config[topKey];
569
- if (typeof existing === "object" && existing !== null) {
570
- let parsed = value;
571
- if (value === "true")
572
- parsed = true;
573
- else if (value === "false")
574
- parsed = false;
575
- else if (/^\d+$/.test(value))
576
- parsed = parseInt(value, 10);
577
- update = { [topKey]: { ...existing, [subKey]: parsed } };
578
- }
579
- else {
580
- await ctx.reply(`Unknown config section: ${topKey}`);
581
- return;
582
- }
583
- }
584
- try {
585
- saveConfig(update);
586
- await ctx.reply(`${key} = ${value}`);
587
- }
588
- catch (err) {
589
- await ctx.reply(`Error: ${err instanceof Error ? err.message : String(err)}`);
590
- }
591
- });
592
- // ─── MCP Management ──────────────────────────────────────────────────
593
- b.command("mcp", async (ctx) => {
594
- const kb = new InlineKeyboard()
595
- .text("➕ Add Server", "mcp:add")
596
- .text("📋 List", "mcp:list")
597
- .text("🗑 Remove", "mcp:remove");
598
- await ctx.reply("MCP Servers", { reply_markup: kb });
599
- });
600
- b.callbackQuery("mcp:add", async (ctx) => {
601
- await ctx.answerCallbackQuery();
602
- await startWizard(ctx.chat.id, createMcpAddWizard(getConfig), b);
603
- });
604
- b.callbackQuery("mcp:list", async (ctx) => {
605
- await ctx.answerCallbackQuery();
606
- const config = getConfig();
607
- const entries = Object.entries(config.mcp.servers);
608
- if (entries.length === 0) {
609
- await ctx.reply("No MCP servers configured.\n\nUse /mcp → Add to add one.");
610
- return;
611
- }
612
- const lines = entries.map(([name, s]) => {
613
- const cfg = s;
614
- if (cfg.type === "stdio") {
615
- const args = Array.isArray(cfg.args) ? cfg.args.join(" ") : "";
616
- return `⚙️ ${name} (stdio)\n ${cfg.command} ${args}`.trimEnd();
617
- }
618
- return `${cfg.type === "sse" ? "📡" : "🌐"} ${name} (${cfg.type})\n ${cfg.url}`;
619
- });
620
- await ctx.reply(`MCP Servers:\n\n${lines.join("\n\n")}`);
621
- });
622
- b.callbackQuery("mcp:remove", async (ctx) => {
623
- await ctx.answerCallbackQuery();
624
- const config = getConfig();
625
- const names = Object.keys(config.mcp.servers);
626
- if (names.length === 0) {
627
- await ctx.reply("No MCP servers to remove.");
628
- return;
629
- }
630
- const kb = new InlineKeyboard();
631
- for (const name of names) {
632
- kb.text(`✕ ${name}`, `mcp:rm:${name}`).row();
633
- }
634
- await ctx.reply("Remove which server?", { reply_markup: kb });
635
- });
636
- b.callbackQuery(/^mcp:rm:/, async (ctx) => {
637
- await ctx.answerCallbackQuery();
638
- const name = ctx.callbackQuery.data.replace("mcp:rm:", "");
639
- const config = getConfig();
640
- const servers = { ...config.mcp.servers };
641
- if (!(name in servers)) {
642
- await ctx.reply(`Server "${name}" not found.`);
643
- return;
644
- }
645
- delete servers[name];
646
- saveConfig({ mcp: { servers } });
647
- await ctx.reply(`Removed MCP server: ${name}`);
648
- });
649
- b.command("sessions", async (ctx) => {
650
- const sessions = listSessions();
651
- if (sessions.length === 0) {
652
- await ctx.reply("No sessions.");
653
- return;
654
- }
655
- const recent = sessions.slice(0, 15);
656
- const lines = [`Sessions (${sessions.length} total):\n`];
657
- for (const s of recent) {
658
- const age = formatAge(s.createdAt);
659
- const msgs = loadMessages(s.id).length;
660
- const label = s.label ? ` (${s.label})` : "";
661
- lines.push(`${s.id}${label}`);
662
- lines.push(` ${s.model} · ${msgs} msgs · ${age}`);
663
- }
664
- if (sessions.length > 15) {
665
- lines.push(`\n... and ${sessions.length - 15} more`);
666
- }
667
- const kb = new InlineKeyboard()
668
- .text("Clear > 1 day", "clearsessions:1d")
669
- .text("Clear > 1 week", "clearsessions:1w")
670
- .text("Clear > 1 month", "clearsessions:1m");
671
- await ctx.reply(lines.join("\n"), { reply_markup: kb });
672
- });
673
- b.command("status", async (ctx) => {
674
- const config = getConfig();
675
- const runningBots = getActiveBotIds();
676
- const sessions = listSessions();
677
- const lines = [
678
- "CamelAGI Status\n",
679
- `Provider: ${config.provider}`,
680
- `Model: ${config.model}`,
681
- `API Key: ${config.apiKey ? "set" : "not set"}`,
682
- "",
683
- `Bots: ${runningBots.length} running`,
684
- ];
685
- for (const id of runningBots) {
686
- lines.push(` running: ${id}`);
687
- }
688
- const allIds = Object.keys(config.agents);
689
- const stoppedIds = allIds.filter((id) => !runningBots.includes(id));
690
- for (const id of stoppedIds) {
691
- lines.push(` stopped: ${id}`);
692
- }
693
- lines.push("");
694
- lines.push(`Sessions: ${sessions.length}`);
695
- lines.push(`Approvals: ${config.approvals.mode}`);
696
- await ctx.reply(lines.join("\n"));
697
- });
698
- b.command("restart", async (ctx) => {
699
- const targetId = (ctx.match ?? "").trim();
700
- const config = getConfig();
701
- if (targetId) {
702
- if (!config.agents[targetId]) {
703
- await ctx.reply(`Agent "${targetId}" not found.`);
704
- return;
705
- }
706
- const token = config.agents[targetId].telegram?.botToken;
707
- if (!token) {
708
- await ctx.reply(`Agent "${targetId}" has no Telegram bot.`);
709
- return;
710
- }
711
- stopBot(targetId);
712
- try {
713
- const sysPrompt = getSystemPrompt();
714
- await startBot(targetId, token, getConfig, () => sysPrompt);
715
- await ctx.reply(`Restarted ${targetId}`);
716
- }
717
- catch (err) {
718
- await ctx.reply(`Error restarting: ${err instanceof Error ? err.message : String(err)}`);
719
- }
720
- return;
721
- }
722
- const restarted = [];
723
- for (const [id, a] of Object.entries(config.agents)) {
724
- if (a.admin)
725
- continue;
726
- if (!a.telegram?.botToken)
727
- continue;
728
- stopBot(id);
729
- try {
730
- const sysPrompt = getSystemPrompt();
731
- await startBot(id, a.telegram.botToken, getConfig, () => sysPrompt);
732
- restarted.push(id);
733
- }
734
- catch { /* skip */ }
735
- }
736
- await ctx.reply(restarted.length > 0
737
- ? `Restarted: ${restarted.join(", ")}`
738
- : "No bots to restart.");
739
- });
740
- b.command("pairing", async (ctx) => {
741
- const requests = listPendingRequests();
742
- if (requests.length === 0) {
743
- await ctx.reply("No pending access requests.");
744
- return;
745
- }
746
- for (const r of requests) {
747
- const userLabel = r.username ? `@${r.username}` : r.firstName ?? String(r.userId);
748
- const age = formatAge(r.requestedAt);
749
- const text = `Pending request\n\nUser: ${userLabel} (${r.userId})\nAgent: ${r.agentId}\nCode: ${r.code}\nRequested: ${age}`;
750
- const kb = new InlineKeyboard()
751
- .text("Approve", `pairing:approve:${r.code}`)
752
- .text("Deny", `pairing:deny:${r.code}`);
753
- await ctx.reply(text, { reply_markup: kb });
754
- }
755
- });
756
- // ─── Voice configuration ─────────────────────────────────────────
757
- b.command("voice", async (ctx) => {
758
- const config = getConfig();
759
- if (config.voice.enabled && config.voice.apiKey) {
760
- const masked = "***" + config.voice.apiKey.slice(-4);
761
- const kb = new InlineKeyboard()
762
- .text("Reconfigure", "voice:reconfigure")
763
- .text("Reset", "voice:reset");
764
- await ctx.reply([
765
- "Voice transcription: enabled\n",
766
- `Provider: ${config.voice.provider}`,
767
- `Model: ${config.voice.model ?? "default"}`,
768
- `API Key: ${masked}`,
769
- config.voice.language ? `Language: ${config.voice.language}` : null,
770
- ].filter(Boolean).join("\n"), { reply_markup: kb });
771
- }
772
- else {
773
- await startWizard(ctx.chat.id, createVoiceWizard(getConfig), b);
774
- }
775
- });
776
- b.callbackQuery(/^voice:(reconfigure|reset)$/, async (ctx) => {
777
- const action = ctx.callbackQuery.data.match(/^voice:(reconfigure|reset)$/)?.[1];
778
- await ctx.answerCallbackQuery();
779
- try {
780
- await ctx.editMessageText(`${ctx.callbackQuery.message?.text ?? ""}\n\n-> ${action}`);
781
- }
782
- catch { }
783
- if (action === "reconfigure") {
784
- await startWizard(ctx.chat.id, createVoiceWizard(getConfig), b);
785
- }
786
- else {
787
- await startWizard(ctx.chat.id, createVoiceResetWizard(), b);
788
- }
789
- });
790
- // ─── Agent config editing ───────────────────────────────────────────
791
- b.command("agent", async (ctx) => {
792
- const targetId = (ctx.match ?? "").trim();
793
- const config = getConfig();
794
- if (!targetId) {
795
- const entries = Object.entries(config.agents);
796
- if (entries.length === 0) {
797
- await ctx.reply("No agents. Use /newagent first.");
798
- return;
799
- }
800
- const kb = new InlineKeyboard();
801
- for (const [id, a] of entries) {
802
- kb.text(a.name, `ae:show:${id}`).row();
803
- }
804
- await ctx.reply("Which agent to view/edit?", { reply_markup: kb });
805
- return;
806
- }
807
- if (!config.agents[targetId]) {
808
- await ctx.reply(`Agent "${targetId}" not found.`);
809
- return;
810
- }
811
- await showAgentConfig(ctx.chat.id, targetId, config, b);
812
- });
813
- b.callbackQuery(/^ae:show:(.+)$/, async (ctx) => {
814
- const agentIdParam = ctx.callbackQuery.data.match(/^ae:show:(.+)$/)?.[1];
815
- if (!agentIdParam)
816
- return;
817
- await ctx.answerCallbackQuery();
818
- const config = getConfig();
819
- await showAgentConfig(ctx.chat.id, agentIdParam, config, b);
820
- });
821
- b.callbackQuery(/^ae:think:(.+)$/, async (ctx) => {
822
- const agentIdParam = ctx.callbackQuery.data.match(/^ae:think:(.+)$/)?.[1];
823
- if (!agentIdParam)
824
- return;
825
- await ctx.answerCallbackQuery();
826
- const config = getConfig();
827
- const current = config.agents[agentIdParam]?.thinking ?? config.thinking;
828
- const levels = ["off", "low", "medium", "high"];
829
- const kb = new InlineKeyboard();
830
- for (const l of levels) {
831
- kb.text(l === current ? `✓ ${l}` : l, `as:${agentIdParam}:think:${l}`);
832
- }
833
- await ctx.reply(`Set thinking for ${agentIdParam}:`, { reply_markup: kb });
834
- });
835
- b.callbackQuery(/^ae:effort:(.+)$/, async (ctx) => {
836
- const agentIdParam = ctx.callbackQuery.data.match(/^ae:effort:(.+)$/)?.[1];
837
- if (!agentIdParam)
838
- return;
839
- await ctx.answerCallbackQuery();
840
- const config = getConfig();
841
- const current = config.agents[agentIdParam]?.effort ?? config.effort;
842
- const levels = ["low", "medium", "high", "max"];
843
- const kb = new InlineKeyboard();
844
- for (const l of levels) {
845
- kb.text(l === current ? `✓ ${l}` : l, `as:${agentIdParam}:effort:${l}`);
846
- }
847
- await ctx.reply(`Set effort for ${agentIdParam}:`, { reply_markup: kb });
848
- });
849
- b.callbackQuery(/^ae:turns:(.+)$/, async (ctx) => {
850
- const agentIdParam = ctx.callbackQuery.data.match(/^ae:turns:(.+)$/)?.[1];
851
- if (!agentIdParam)
852
- return;
853
- await ctx.answerCallbackQuery();
854
- const config = getConfig();
855
- const current = config.agents[agentIdParam]?.maxTurns ?? config.maxTurns;
856
- const options = [10, 25, 50, 100];
857
- const kb = new InlineKeyboard();
858
- for (const n of options) {
859
- kb.text(n === current ? `✓ ${n}` : String(n), `as:${agentIdParam}:turns:${n}`);
860
- }
861
- await ctx.reply(`Set max turns for ${agentIdParam}:`, { reply_markup: kb });
862
- });
863
- b.callbackQuery(/^ae:model:(.+)$/, async (ctx) => {
864
- const agentIdParam = ctx.callbackQuery.data.match(/^ae:model:(.+)$/)?.[1];
865
- if (!agentIdParam)
866
- return;
867
- await ctx.answerCallbackQuery();
868
- const config = getConfig();
869
- const current = config.agents[agentIdParam]?.model ?? config.model;
870
- const preset = resolvePreset(config.provider, config.baseUrl);
871
- const models = preset.models.slice(0, 6);
872
- const kb = new InlineKeyboard();
873
- for (const m of models) {
874
- const slash = m.indexOf("/");
875
- const label = slash > 0 ? m.slice(slash + 1) : m;
876
- const check = m === current ? "✓ " : "";
877
- kb.text(`${check}${label}`, `as:${agentIdParam}:model:${m}`).row();
878
- }
879
- kb.text("✏️ Type custom", `ae:modelcustom:${agentIdParam}`);
880
- await ctx.reply(`Set model for ${agentIdParam}:`, { reply_markup: kb });
881
- });
882
- b.callbackQuery(/^ae:modelcustom:(.+)$/, async (ctx) => {
883
- const agentIdParam = ctx.callbackQuery.data.match(/^ae:modelcustom:(.+)$/)?.[1];
884
- if (!agentIdParam)
885
- return;
886
- await ctx.answerCallbackQuery();
887
- await startWizard(ctx.chat.id, {
888
- id: "agent-model-edit",
889
- steps: [{
890
- id: "model",
891
- prompt: `Type the model name for "${agentIdParam}":`,
892
- }],
893
- onComplete: async (data) => {
894
- const cfg = getConfig();
895
- const agents = { ...cfg.agents };
896
- agents[agentIdParam] = { ...agents[agentIdParam], model: data.model };
897
- saveConfig({ agents });
898
- return `Model set to: ${data.model}`;
899
- },
900
- }, b);
901
- });
902
- b.callbackQuery(/^ae:brief:(.+)$/, async (ctx) => {
903
- const agentIdParam = ctx.callbackQuery.data.match(/^ae:brief:(.+)$/)?.[1];
904
- if (!agentIdParam)
905
- return;
906
- await ctx.answerCallbackQuery();
907
- const config = getConfig();
908
- const agent = config.agents[agentIdParam];
909
- if (!agent?.telegram) {
910
- await ctx.reply("Agent has no Telegram config.");
911
- return;
912
- }
913
- const current = agent.telegram.briefMode ?? true;
914
- const next = !current;
915
- const agents = { ...config.agents };
916
- agents[agentIdParam] = {
917
- ...agents[agentIdParam],
918
- telegram: { ...agents[agentIdParam].telegram, briefMode: next },
919
- };
920
- saveConfig({ agents });
921
- try {
922
- await ctx.editMessageText(`Brief mode: ${next ? "on" : "off"} ✓`);
923
- }
924
- catch {
925
- await ctx.reply(`Brief mode: ${next ? "on" : "off"} ✓`);
926
- }
927
- });
928
- b.callbackQuery(/^ae:mcp:(.+)$/, async (ctx) => {
929
- await ctx.answerCallbackQuery();
930
- await ctx.reply("Use /mcp to manage MCP servers.");
931
- });
932
- b.callbackQuery(/^ae:clone:(.+)$/, async (ctx) => {
933
- const agentIdParam = ctx.callbackQuery.data.match(/^ae:clone:(.+)$/)?.[1];
934
- if (!agentIdParam)
935
- return;
936
- await ctx.answerCallbackQuery();
937
- await startWizard(ctx.chat.id, createCloneWizard(agentIdParam, getConfig, getSystemPrompt), b);
938
- });
939
- b.callbackQuery(/^as:(.+?):(.+?):(.+)$/, async (ctx) => {
940
- const match = ctx.callbackQuery.data.match(/^as:(.+?):(.+?):(.+)$/);
941
- if (!match)
942
- return;
943
- const [, agentIdParam, field, value] = match;
944
- await ctx.answerCallbackQuery();
945
- const config = getConfig();
946
- if (!config.agents[agentIdParam]) {
947
- await ctx.reply("Agent not found.");
948
- return;
949
- }
950
- const agents = { ...config.agents };
951
- const agent = { ...agents[agentIdParam] };
952
- const fieldLabels = {
953
- think: "Thinking", effort: "Effort", turns: "Max Turns", model: "Model",
954
- };
955
- if (field === "think") {
956
- agent.thinking = value;
957
- }
958
- else if (field === "effort") {
959
- agent.effort = value;
960
- }
961
- else if (field === "turns") {
962
- agent.maxTurns = parseInt(value, 10);
963
- }
964
- else if (field === "model") {
965
- agent.model = value;
966
- }
967
- agents[agentIdParam] = agent;
968
- saveConfig({ agents });
969
- const label = fieldLabels[field] ?? field;
970
- try {
971
- await ctx.editMessageText(`${label} set to: ${value} ✓`);
972
- }
973
- catch {
974
- await ctx.reply(`${label} set to: ${value} ✓`);
975
- }
976
- });
977
- // ─── Usage & cost summary ────────────────────────────────────────
978
- b.command("usage", async (ctx) => {
979
- const config = getConfig();
980
- const entries = Object.entries(config.agents).filter(([, a]) => !a.admin);
981
- if (entries.length === 0) {
982
- await ctx.reply("No agents configured.");
983
- return;
984
- }
985
- const lines = ["Usage Summary\n"];
986
- let totalCost = 0;
987
- let hasData = false;
988
- for (const [id, a] of entries) {
989
- const model = a.model ?? config.model;
990
- const summary = aggregateAgentUsage(id, a.name, model);
991
- const total = summary.totalInput + summary.totalOutput;
992
- if (total === 0 && summary.calls === 0)
993
- continue;
994
- hasData = true;
995
- lines.push(`${a.name} (${model})`);
996
- lines.push(` ${formatTokens(summary.totalInput)} in | ${formatTokens(summary.totalOutput)} out | ${summary.calls} calls`);
997
- if (summary.estimatedCost !== undefined) {
998
- lines.push(` Cost: ~${formatCost(summary.estimatedCost)}`);
999
- totalCost += summary.estimatedCost;
1000
- }
1001
- lines.push("");
1002
- }
1003
- if (!hasData) {
1004
- await ctx.reply("No usage data yet.");
1005
- return;
1006
- }
1007
- if (totalCost > 0) {
1008
- lines.push(`Total estimated cost: ~${formatCost(totalCost)}`);
1009
- }
1010
- await ctx.reply(lines.join("\n"));
1011
- });
1012
- // ─── Message handler: wizard intercept ─────────────────────────────
88
+ // ─── Register modules ───────────────────────────────────────────────
89
+ registerAdminCommands(b, agentId, getConfig, getSystemPrompt, activeBots);
90
+ registerAdminAgents(b, agentId, getConfig, getSystemPrompt, activeBots);
91
+ // ─── Message handler: wizard text intercept ─────────────────────────
1013
92
  b.on("message:text", async (ctx) => {
1014
93
  const chatId = ctx.chat.id;
1015
94
  const text = ctx.message.text;