camelagi 0.5.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (249) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +224 -0
  3. package/camelagi.mjs +2 -0
  4. package/config.example.yaml +107 -0
  5. package/dist/agent/agent-openai.js +206 -0
  6. package/dist/agent/agent-openai.js.map +1 -0
  7. package/dist/agent/agent-sdk.js +209 -0
  8. package/dist/agent/agent-sdk.js.map +1 -0
  9. package/dist/agent/tool-adapter.js +31 -0
  10. package/dist/agent/tool-adapter.js.map +1 -0
  11. package/dist/agent/types.js +3 -0
  12. package/dist/agent/types.js.map +1 -0
  13. package/dist/agent.js +17 -0
  14. package/dist/agent.js.map +1 -0
  15. package/dist/approval-forward.js +42 -0
  16. package/dist/approval-forward.js.map +1 -0
  17. package/dist/approvals.js +151 -0
  18. package/dist/approvals.js.map +1 -0
  19. package/dist/boot.js +34 -0
  20. package/dist/boot.js.map +1 -0
  21. package/dist/bootstrap.js +451 -0
  22. package/dist/bootstrap.js.map +1 -0
  23. package/dist/camelagi-gateway.mjs +93611 -0
  24. package/dist/camelagi-gateway.mjs.map +7 -0
  25. package/dist/channels/adapter.js +10 -0
  26. package/dist/channels/adapter.js.map +1 -0
  27. package/dist/channels/discord.js +232 -0
  28. package/dist/channels/discord.js.map +1 -0
  29. package/dist/channels/handler.js +349 -0
  30. package/dist/channels/handler.js.map +1 -0
  31. package/dist/channels/index.js +19 -0
  32. package/dist/channels/index.js.map +1 -0
  33. package/dist/channels/registry.js +71 -0
  34. package/dist/channels/registry.js.map +1 -0
  35. package/dist/channels/telegram.js +83 -0
  36. package/dist/channels/telegram.js.map +1 -0
  37. package/dist/channels/types.js +3 -0
  38. package/dist/channels/types.js.map +1 -0
  39. package/dist/chunker.js +102 -0
  40. package/dist/chunker.js.map +1 -0
  41. package/dist/cli/cmd-agents.js +65 -0
  42. package/dist/cli/cmd-agents.js.map +1 -0
  43. package/dist/cli/cmd-bootstrap.js +10 -0
  44. package/dist/cli/cmd-bootstrap.js.map +1 -0
  45. package/dist/cli/cmd-chat.js +32 -0
  46. package/dist/cli/cmd-chat.js.map +1 -0
  47. package/dist/cli/cmd-config.js +88 -0
  48. package/dist/cli/cmd-config.js.map +1 -0
  49. package/dist/cli/cmd-cron.js +120 -0
  50. package/dist/cli/cmd-cron.js.map +1 -0
  51. package/dist/cli/cmd-daemon.js +37 -0
  52. package/dist/cli/cmd-daemon.js.map +1 -0
  53. package/dist/cli/cmd-doctor.js +18 -0
  54. package/dist/cli/cmd-doctor.js.map +1 -0
  55. package/dist/cli/cmd-logs.js +30 -0
  56. package/dist/cli/cmd-logs.js.map +1 -0
  57. package/dist/cli/cmd-pairing.js +41 -0
  58. package/dist/cli/cmd-pairing.js.map +1 -0
  59. package/dist/cli/cmd-reset.js +39 -0
  60. package/dist/cli/cmd-reset.js.map +1 -0
  61. package/dist/cli/cmd-serve.js +30 -0
  62. package/dist/cli/cmd-serve.js.map +1 -0
  63. package/dist/cli/cmd-sessions.js +56 -0
  64. package/dist/cli/cmd-sessions.js.map +1 -0
  65. package/dist/cli/cmd-setup.js +11 -0
  66. package/dist/cli/cmd-setup.js.map +1 -0
  67. package/dist/cli/cmd-soul.js +43 -0
  68. package/dist/cli/cmd-soul.js.map +1 -0
  69. package/dist/cli/parse.js +50 -0
  70. package/dist/cli/parse.js.map +1 -0
  71. package/dist/cli/registry.js +15 -0
  72. package/dist/cli/registry.js.map +1 -0
  73. package/dist/cli.js +103 -0
  74. package/dist/cli.js.map +1 -0
  75. package/dist/compact.js +92 -0
  76. package/dist/compact.js.map +1 -0
  77. package/dist/config.js +153 -0
  78. package/dist/config.js.map +1 -0
  79. package/dist/constants.js +21 -0
  80. package/dist/constants.js.map +1 -0
  81. package/dist/core/config.js +212 -0
  82. package/dist/core/config.js.map +1 -0
  83. package/dist/core/constants.js +21 -0
  84. package/dist/core/constants.js.map +1 -0
  85. package/dist/core/errors.js +5 -0
  86. package/dist/core/errors.js.map +1 -0
  87. package/dist/core/log.js +41 -0
  88. package/dist/core/log.js.map +1 -0
  89. package/dist/core/models.js +123 -0
  90. package/dist/core/models.js.map +1 -0
  91. package/dist/core/types.js +3 -0
  92. package/dist/core/types.js.map +1 -0
  93. package/dist/core/update-check.js +51 -0
  94. package/dist/core/update-check.js.map +1 -0
  95. package/dist/cron.js +81 -0
  96. package/dist/cron.js.map +1 -0
  97. package/dist/daemon.js +109 -0
  98. package/dist/daemon.js.map +1 -0
  99. package/dist/doctor.js +194 -0
  100. package/dist/doctor.js.map +1 -0
  101. package/dist/errors.js +5 -0
  102. package/dist/errors.js.map +1 -0
  103. package/dist/extensions/approval-forward.js +42 -0
  104. package/dist/extensions/approval-forward.js.map +1 -0
  105. package/dist/extensions/approvals.js +144 -0
  106. package/dist/extensions/approvals.js.map +1 -0
  107. package/dist/extensions/cron.js +306 -0
  108. package/dist/extensions/cron.js.map +1 -0
  109. package/dist/extensions/hooks.js +72 -0
  110. package/dist/extensions/hooks.js.map +1 -0
  111. package/dist/extensions/skills.js +97 -0
  112. package/dist/extensions/skills.js.map +1 -0
  113. package/dist/gateway/csrf.js +44 -0
  114. package/dist/gateway/csrf.js.map +1 -0
  115. package/dist/gateway/logger.js +81 -0
  116. package/dist/gateway/logger.js.map +1 -0
  117. package/dist/gateway/rate-limit.js +33 -0
  118. package/dist/gateway/rate-limit.js.map +1 -0
  119. package/dist/gateway/routes.js +315 -0
  120. package/dist/gateway/routes.js.map +1 -0
  121. package/dist/gateway/state.js +54 -0
  122. package/dist/gateway/state.js.map +1 -0
  123. package/dist/gateway/ws-handler.js +200 -0
  124. package/dist/gateway/ws-handler.js.map +1 -0
  125. package/dist/gateway-entry.js +16 -0
  126. package/dist/gateway-entry.js.map +1 -0
  127. package/dist/hooks.js +72 -0
  128. package/dist/hooks.js.map +1 -0
  129. package/dist/lanes.js +62 -0
  130. package/dist/lanes.js.map +1 -0
  131. package/dist/model.js +30 -0
  132. package/dist/model.js.map +1 -0
  133. package/dist/policy.js +22 -0
  134. package/dist/policy.js.map +1 -0
  135. package/dist/queue.js +45 -0
  136. package/dist/queue.js.map +1 -0
  137. package/dist/retry.js +96 -0
  138. package/dist/retry.js.map +1 -0
  139. package/dist/runs.js +83 -0
  140. package/dist/runs.js.map +1 -0
  141. package/dist/runtime/compact.js +99 -0
  142. package/dist/runtime/compact.js.map +1 -0
  143. package/dist/runtime/lanes.js +66 -0
  144. package/dist/runtime/lanes.js.map +1 -0
  145. package/dist/runtime/orchestrate.js +121 -0
  146. package/dist/runtime/orchestrate.js.map +1 -0
  147. package/dist/runtime/queue.js +50 -0
  148. package/dist/runtime/queue.js.map +1 -0
  149. package/dist/runtime/retry.js +127 -0
  150. package/dist/runtime/retry.js.map +1 -0
  151. package/dist/runtime/runs.js +105 -0
  152. package/dist/runtime/runs.js.map +1 -0
  153. package/dist/serve.js +209 -0
  154. package/dist/serve.js.map +1 -0
  155. package/dist/session.js +75 -0
  156. package/dist/session.js.map +1 -0
  157. package/dist/setup.js +254 -0
  158. package/dist/setup.js.map +1 -0
  159. package/dist/skills.js +89 -0
  160. package/dist/skills.js.map +1 -0
  161. package/dist/subagent.js +71 -0
  162. package/dist/subagent.js.map +1 -0
  163. package/dist/system-prompt.js +157 -0
  164. package/dist/system-prompt.js.map +1 -0
  165. package/dist/telegram/admin-bot.js +705 -0
  166. package/dist/telegram/admin-bot.js.map +1 -0
  167. package/dist/telegram/agent-bot.js +551 -0
  168. package/dist/telegram/agent-bot.js.map +1 -0
  169. package/dist/telegram/bot-approval.js +63 -0
  170. package/dist/telegram/bot-approval.js.map +1 -0
  171. package/dist/telegram/draft-stream.js +86 -0
  172. package/dist/telegram/draft-stream.js.map +1 -0
  173. package/dist/telegram/format.js +106 -0
  174. package/dist/telegram/format.js.map +1 -0
  175. package/dist/telegram/helpers.js +87 -0
  176. package/dist/telegram/helpers.js.map +1 -0
  177. package/dist/telegram/pairing-notify.js +52 -0
  178. package/dist/telegram/pairing-notify.js.map +1 -0
  179. package/dist/telegram/pairing.js +138 -0
  180. package/dist/telegram/pairing.js.map +1 -0
  181. package/dist/telegram/resolve.js +33 -0
  182. package/dist/telegram/resolve.js.map +1 -0
  183. package/dist/telegram/transcribe.js +77 -0
  184. package/dist/telegram/transcribe.js.map +1 -0
  185. package/dist/telegram/types.js +3 -0
  186. package/dist/telegram/types.js.map +1 -0
  187. package/dist/telegram/voice-wizard.js +84 -0
  188. package/dist/telegram/voice-wizard.js.map +1 -0
  189. package/dist/telegram/wizard.js +89 -0
  190. package/dist/telegram/wizard.js.map +1 -0
  191. package/dist/telegram/wizards.js +297 -0
  192. package/dist/telegram/wizards.js.map +1 -0
  193. package/dist/telegram-admin.js +800 -0
  194. package/dist/telegram-admin.js.map +1 -0
  195. package/dist/telegram.js +118 -0
  196. package/dist/telegram.js.map +1 -0
  197. package/dist/tools/cron.js +94 -0
  198. package/dist/tools/cron.js.map +1 -0
  199. package/dist/tools/edit.js +29 -0
  200. package/dist/tools/edit.js.map +1 -0
  201. package/dist/tools/exec.js +38 -0
  202. package/dist/tools/exec.js.map +1 -0
  203. package/dist/tools/fetch.js +28 -0
  204. package/dist/tools/fetch.js.map +1 -0
  205. package/dist/tools/index.js +16 -0
  206. package/dist/tools/index.js.map +1 -0
  207. package/dist/tools/memory.js +164 -0
  208. package/dist/tools/memory.js.map +1 -0
  209. package/dist/tools/patch.js +284 -0
  210. package/dist/tools/patch.js.map +1 -0
  211. package/dist/tools/read.js +26 -0
  212. package/dist/tools/read.js.map +1 -0
  213. package/dist/tools/search.js +62 -0
  214. package/dist/tools/search.js.map +1 -0
  215. package/dist/tools/subagent.js +48 -0
  216. package/dist/tools/subagent.js.map +1 -0
  217. package/dist/tools/write.js +22 -0
  218. package/dist/tools/write.js.map +1 -0
  219. package/dist/tui/commands.js +450 -0
  220. package/dist/tui/commands.js.map +1 -0
  221. package/dist/tui/components/assistant-message.js +26 -0
  222. package/dist/tui/components/assistant-message.js.map +1 -0
  223. package/dist/tui/components/chat-log.js +94 -0
  224. package/dist/tui/components/chat-log.js.map +1 -0
  225. package/dist/tui/components/custom-editor.js +40 -0
  226. package/dist/tui/components/custom-editor.js.map +1 -0
  227. package/dist/tui/components/hint-bar.js +13 -0
  228. package/dist/tui/components/hint-bar.js.map +1 -0
  229. package/dist/tui/components/tool-execution.js +73 -0
  230. package/dist/tui/components/tool-execution.js.map +1 -0
  231. package/dist/tui/components/user-message.js +19 -0
  232. package/dist/tui/components/user-message.js.map +1 -0
  233. package/dist/tui/components/welcome.js +147 -0
  234. package/dist/tui/components/welcome.js.map +1 -0
  235. package/dist/tui/context.js +3 -0
  236. package/dist/tui/context.js.map +1 -0
  237. package/dist/tui/theme.js +91 -0
  238. package/dist/tui/theme.js.map +1 -0
  239. package/dist/tui/tui.js +389 -0
  240. package/dist/tui/tui.js.map +1 -0
  241. package/dist/tui/ws-handler.js +154 -0
  242. package/dist/tui/ws-handler.js.map +1 -0
  243. package/dist/types.js +3 -0
  244. package/dist/types.js.map +1 -0
  245. package/dist/usage.js +88 -0
  246. package/dist/usage.js.map +1 -0
  247. package/dist/workspace.js +245 -0
  248. package/dist/workspace.js.map +1 -0
  249. package/package.json +74 -0
@@ -0,0 +1,705 @@
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 { getActiveBotIds, startBot, stopBot } from "../telegram.js";
9
+ import { startWizard, advanceWizard, cancelWizard, hasActiveWizard } from "./wizard.js";
10
+ import { createSetupWizard, createNewAgentWizard } from "./wizards.js";
11
+ import { createVoiceWizard, createVoiceResetWizard } from "./voice-wizard.js";
12
+ import { formatAge } from "./helpers.js";
13
+ import { approveRequest, denyRequest, listPendingRequests, hasPendingRequest, createPairingRequest } from "./pairing.js";
14
+ import { notifyUserApproved, notifyUserOfDenial } from "./pairing-notify.js";
15
+ import { isGroupChat } from "./helpers.js";
16
+ import { approveBotApproval, denyBotApproval, } from "./bot-approval.js";
17
+ // ─── Helpers ─────────────────────────────────────────────────────────
18
+ async function showSoul(chatId, targetId, edit, bot) {
19
+ const soulPath = path.join(agentMemoryDir(targetId), "SOUL.md");
20
+ if (edit) {
21
+ const editWizard = {
22
+ id: "soul-edit",
23
+ steps: [{
24
+ id: "content",
25
+ prompt: `Send the new SOUL.md content for "${targetId}".\nCurrent content will be replaced. /cancel to abort.`,
26
+ }],
27
+ onComplete: async (data) => {
28
+ const dir = agentMemoryDir(targetId);
29
+ fs.mkdirSync(dir, { recursive: true });
30
+ fs.writeFileSync(soulPath, data.content);
31
+ return `SOUL.md updated for "${targetId}".\n\n${data.content.slice(0, 200)}${data.content.length > 200 ? "..." : ""}`;
32
+ },
33
+ };
34
+ await startWizard(chatId, editWizard, bot);
35
+ return;
36
+ }
37
+ if (!fs.existsSync(soulPath)) {
38
+ await bot.api.sendMessage(chatId, `No SOUL.md for "${targetId}" yet.`);
39
+ return;
40
+ }
41
+ const content = fs.readFileSync(soulPath, "utf-8").trim();
42
+ const preview = content.length > 3800 ? content.slice(0, 3800) + "\n\n[truncated]" : content;
43
+ const kb = new InlineKeyboard()
44
+ .text("Edit", `picksoul:edit:${targetId}`);
45
+ await bot.api.sendMessage(chatId, `SOUL.md (${targetId}):\n\n${preview}`, { reply_markup: kb });
46
+ }
47
+ // ─── Setup Admin Bot ─────────────────────────────────────────────────
48
+ export async function setupAdminBot(agentId, botToken, getConfig, getSystemPrompt, activeBots) {
49
+ const b = new Bot(botToken);
50
+ await b.api.setMyCommands([
51
+ { command: "help", description: "List all commands" },
52
+ { command: "setup", description: "Configure API provider, key, model" },
53
+ { command: "newagent", description: "Create a new agent" },
54
+ { command: "agents", description: "List all agents" },
55
+ { command: "deleteagent", description: "Delete an agent" },
56
+ { command: "soul", description: "View/edit agent SOUL.md" },
57
+ { command: "config", description: "View/update configuration" },
58
+ { command: "sessions", description: "Manage sessions" },
59
+ { command: "status", description: "System health and stats" },
60
+ { command: "restart", description: "Restart agent bots" },
61
+ { command: "pairing", description: "List pending access requests" },
62
+ { command: "voice", description: "Configure voice transcription" },
63
+ { command: "cancel", description: "Cancel active wizard" },
64
+ ]).catch(() => { });
65
+ /** Check if userId is allowed for this agent */
66
+ function isUserAllowed(userId) {
67
+ const memAgent = getConfig().agents[agentId];
68
+ const memAllowed = memAgent?.telegram?.allowedUsers ?? [];
69
+ if (memAllowed.includes(userId))
70
+ return true;
71
+ // Fallback: read config file directly (handles hot-reload delay)
72
+ try {
73
+ const freshConfig = loadConfig();
74
+ const freshAgent = freshConfig.agents[agentId];
75
+ const freshAllowed = freshAgent?.telegram?.allowedUsers ?? [];
76
+ if (freshAllowed.includes(userId))
77
+ return true;
78
+ }
79
+ catch { }
80
+ return false;
81
+ }
82
+ // Access control — admin approves, user gets instant access
83
+ b.use(async (ctx, next) => {
84
+ const userId = ctx.from?.id;
85
+ if (!userId)
86
+ return;
87
+ if (isUserAllowed(userId)) {
88
+ await next();
89
+ return;
90
+ }
91
+ // Groups: silent reject for unauthorized users
92
+ if (ctx.chat && isGroupChat(ctx.chat.type))
93
+ return;
94
+ // Check if user already has a pending request
95
+ const pending = hasPendingRequest(userId, agentId);
96
+ if (pending) {
97
+ await ctx.reply(`Your access request is pending approval.\nCode: ${pending.code}`);
98
+ return;
99
+ }
100
+ // Create new pairing request
101
+ const request = createPairingRequest(userId, agentId, ctx.chat.id, ctx.from?.username, ctx.from?.first_name);
102
+ await ctx.reply(`Access requested. Waiting for approval...\nCode: ${request.code}`);
103
+ });
104
+ // ─── Callback queries ─────────────────────────────────────────────
105
+ b.callbackQuery(/^wizard:(.+):(.+)$/, async (ctx) => {
106
+ const match = ctx.callbackQuery.data.match(/^wizard:(.+):(.+)$/);
107
+ if (!match)
108
+ return;
109
+ const [, stepId, value] = match;
110
+ const chatId = ctx.chat?.id;
111
+ if (!chatId)
112
+ return;
113
+ // Show the button label (not raw value like __default__)
114
+ const buttonText = ctx.callbackQuery.message && "reply_markup" in ctx.callbackQuery.message
115
+ ? ctx.callbackQuery.message.reply_markup?.inline_keyboard
116
+ ?.flat()
117
+ ?.find((btn) => btn.callback_data === ctx.callbackQuery.data)
118
+ ?.text
119
+ : undefined;
120
+ const displayValue = buttonText ?? value;
121
+ try {
122
+ await ctx.editMessageText(`${ctx.callbackQuery.message?.text ?? ""}\n\n-> ${displayValue}`);
123
+ }
124
+ catch { }
125
+ await ctx.answerCallbackQuery();
126
+ await advanceWizard(chatId, value, b);
127
+ });
128
+ b.callbackQuery(/^picksoul:(.+):(.+)$/, async (ctx) => {
129
+ const match = ctx.callbackQuery.data.match(/^picksoul:(.+):(.+)$/);
130
+ if (!match)
131
+ return;
132
+ const [, action, id] = match;
133
+ await ctx.answerCallbackQuery();
134
+ try {
135
+ await ctx.editMessageText(`-> ${id}`);
136
+ }
137
+ catch { }
138
+ await showSoul(ctx.chat.id, id, action === "edit", b);
139
+ });
140
+ b.callbackQuery(/^pickdelete:(.+)$/, async (ctx) => {
141
+ const match = ctx.callbackQuery.data.match(/^pickdelete:(.+)$/);
142
+ if (!match)
143
+ return;
144
+ const id = match[1];
145
+ await ctx.answerCallbackQuery();
146
+ const config = getConfig();
147
+ if (!config.agents[id]) {
148
+ try {
149
+ await ctx.editMessageText("Agent not found.");
150
+ }
151
+ catch { }
152
+ return;
153
+ }
154
+ const kb = new InlineKeyboard()
155
+ .text("Yes, delete", `confirm:delete:${id}`)
156
+ .text("Cancel", `confirm:cancel:${id}`);
157
+ try {
158
+ await ctx.editMessageText(`Delete "${config.agents[id].name}" (${id})?\n\nBot will be stopped. Workspace files are preserved.`, { reply_markup: kb });
159
+ }
160
+ catch { }
161
+ });
162
+ b.callbackQuery(/^confirm:(.+):(.+)$/, async (ctx) => {
163
+ const match = ctx.callbackQuery.data.match(/^confirm:(.+):(.+)$/);
164
+ if (!match)
165
+ return;
166
+ const [, action, param] = match;
167
+ try {
168
+ await ctx.editMessageText(`${ctx.callbackQuery.message?.text ?? ""}\n\n-> ${action === "delete" ? "Deleting..." : "Cancelled"}`);
169
+ }
170
+ catch { }
171
+ await ctx.answerCallbackQuery();
172
+ if (action === "delete") {
173
+ const config = getConfig();
174
+ if (config.agents[param]) {
175
+ stopBot(param);
176
+ const agents = { ...config.agents };
177
+ delete agents[param];
178
+ saveConfig({ agents });
179
+ await ctx.reply(`Agent "${param}" deleted.\nWorkspace files preserved at ${agentMemoryDir(param)}`);
180
+ }
181
+ }
182
+ });
183
+ b.callbackQuery(/^clearsessions:(.+)$/, async (ctx) => {
184
+ const match = ctx.callbackQuery.data.match(/^clearsessions:(.+)$/);
185
+ if (!match)
186
+ return;
187
+ const [, period] = match;
188
+ const cutoff = { "1d": 86400000, "1w": 604800000, "1m": 2592000000 }[period] ?? 604800000;
189
+ const now = Date.now();
190
+ const sessions = listSessions();
191
+ let deleted = 0;
192
+ for (const s of sessions) {
193
+ if (now - s.createdAt > cutoff) {
194
+ deleteSession(s.id);
195
+ deleted++;
196
+ }
197
+ }
198
+ try {
199
+ await ctx.editMessageText(`${ctx.callbackQuery.message?.text ?? ""}\n\n-> Deleted ${deleted} sessions`);
200
+ }
201
+ catch { }
202
+ await ctx.answerCallbackQuery();
203
+ });
204
+ // ─── Pairing callbacks ───────────────────────────────────────────
205
+ b.callbackQuery(/^pairing:(approve|deny):(.+)$/, async (ctx) => {
206
+ const match = ctx.callbackQuery.data.match(/^pairing:(approve|deny):(.+)$/);
207
+ if (!match)
208
+ return;
209
+ const [, action, code] = match;
210
+ if (action === "approve") {
211
+ const request = approveRequest(code);
212
+ if (request) {
213
+ try {
214
+ await ctx.editMessageText(`${ctx.callbackQuery.message?.text ?? ""}\n\n-> Approved`);
215
+ }
216
+ catch { }
217
+ await notifyUserApproved(request, activeBots);
218
+ }
219
+ else {
220
+ try {
221
+ await ctx.editMessageText("Request expired or already handled.");
222
+ }
223
+ catch { }
224
+ }
225
+ }
226
+ else {
227
+ const request = denyRequest(code);
228
+ if (request) {
229
+ try {
230
+ await ctx.editMessageText(`${ctx.callbackQuery.message?.text ?? ""}\n\n-> Denied`);
231
+ }
232
+ catch { }
233
+ await notifyUserOfDenial(request, activeBots);
234
+ }
235
+ else {
236
+ try {
237
+ await ctx.editMessageText("Request expired or already handled.");
238
+ }
239
+ catch { }
240
+ }
241
+ }
242
+ await ctx.answerCallbackQuery();
243
+ });
244
+ // ─── Bot approval callbacks ─────────────────────────────────────
245
+ b.callbackQuery(/^botapproval:(approve|deny):(.+)$/, async (ctx) => {
246
+ const match = ctx.callbackQuery.data.match(/^botapproval:(approve|deny):(.+)$/);
247
+ if (!match)
248
+ return;
249
+ const [, action, agentIdParam] = match;
250
+ if (action === "approve") {
251
+ const approval = approveBotApproval(agentIdParam);
252
+ if (approval) {
253
+ const botLabel = approval.botUsername ? `@${approval.botUsername}` : agentIdParam;
254
+ try {
255
+ const sysPrompt = getSystemPrompt();
256
+ await startBot(agentIdParam, approval.botToken, getConfig, () => sysPrompt);
257
+ }
258
+ catch (err) {
259
+ // "already running" is fine — config hot-reload may have started it
260
+ const errMsg = err instanceof Error ? err.message : String(err);
261
+ if (!errMsg.includes("already running") && !errMsg.includes("already starting")) {
262
+ try {
263
+ await ctx.editMessageText(`Failed to start bot: ${errMsg}`);
264
+ }
265
+ catch { }
266
+ await ctx.answerCallbackQuery();
267
+ return;
268
+ }
269
+ }
270
+ try {
271
+ await ctx.editMessageText(`${ctx.callbackQuery.message?.text ?? ""}\n\n-> Approved. ${botLabel} is now running.`);
272
+ }
273
+ catch { }
274
+ }
275
+ else {
276
+ try {
277
+ await ctx.editMessageText("Approval not found or already handled.");
278
+ }
279
+ catch { }
280
+ }
281
+ }
282
+ else {
283
+ const approval = denyBotApproval(agentIdParam);
284
+ if (approval) {
285
+ try {
286
+ await ctx.editMessageText(`${ctx.callbackQuery.message?.text ?? ""}\n\n-> Denied. Bot will not start.`);
287
+ }
288
+ catch { }
289
+ }
290
+ else {
291
+ try {
292
+ await ctx.editMessageText("Approval not found or already handled.");
293
+ }
294
+ catch { }
295
+ }
296
+ }
297
+ await ctx.answerCallbackQuery();
298
+ });
299
+ // ─── Commands ─────────────────────────────────────────────────────
300
+ b.command("start", async (ctx) => {
301
+ const config = getConfig();
302
+ const hasApiKey = !!config.apiKey;
303
+ const agentCount = Object.keys(config.agents).length;
304
+ await ctx.reply([
305
+ "CamelAGI Admin\n",
306
+ "I manage your AI agents from here.",
307
+ "",
308
+ hasApiKey ? `API configured (${config.provider}, ${config.model})` : "No API key — run /setup first",
309
+ `${agentCount} agent(s) configured`,
310
+ "",
311
+ "Commands:",
312
+ "/setup — configure API provider & key",
313
+ "/newagent — create an agent",
314
+ "/agents — list agents",
315
+ "/help — all commands",
316
+ ].join("\n"));
317
+ });
318
+ b.command("help", async (ctx) => {
319
+ await ctx.reply([
320
+ "CamelAGI Admin Commands\n",
321
+ "Setup & Config:",
322
+ " /setup — configure API provider, key, model",
323
+ " /config — view current config",
324
+ " /config <key> <value> — update config",
325
+ "",
326
+ "Agents:",
327
+ " /newagent — create a new agent",
328
+ " /agents — list all agents",
329
+ " /deleteagent — pick & delete an agent",
330
+ " /soul — view/edit agent personality",
331
+ "",
332
+ "Access:",
333
+ " /pairing — list pending access requests",
334
+ "",
335
+ "Sessions & Status:",
336
+ " /sessions — list sessions",
337
+ " /status — system health",
338
+ " /restart — restart all bots",
339
+ "",
340
+ " /cancel — cancel active wizard",
341
+ ].join("\n"));
342
+ });
343
+ b.command("cancel", async (ctx) => {
344
+ if (cancelWizard(ctx.chat.id)) {
345
+ await ctx.reply("Wizard cancelled.");
346
+ }
347
+ else {
348
+ await ctx.reply("No active wizard.");
349
+ }
350
+ });
351
+ b.command("setup", async (ctx) => {
352
+ await startWizard(ctx.chat.id, createSetupWizard(getConfig), b);
353
+ });
354
+ b.command("newagent", async (ctx) => {
355
+ const config = getConfig();
356
+ if (!config.apiKey) {
357
+ await ctx.reply("No API key configured. Run /setup first.");
358
+ return;
359
+ }
360
+ await startWizard(ctx.chat.id, createNewAgentWizard(getConfig, getSystemPrompt), b);
361
+ });
362
+ b.command("agents", async (ctx) => {
363
+ const config = getConfig();
364
+ const entries = Object.entries(config.agents);
365
+ if (entries.length === 0) {
366
+ await ctx.reply("No agents configured.\n\nUse /newagent to create one.");
367
+ return;
368
+ }
369
+ const runningBots = getActiveBotIds();
370
+ const lines = ["Your agents:\n"];
371
+ const kb = new InlineKeyboard();
372
+ let hasButtons = false;
373
+ for (const [id, a] of entries) {
374
+ const running = runningBots.includes(id);
375
+ const botState = activeBots.get(id);
376
+ const username = botState?.botInfo?.username;
377
+ const statusIcon = running ? "🟢" : a.telegram?.botToken ? "🔴" : "⚪";
378
+ lines.push(`${statusIcon} ${a.name} (${id})`);
379
+ lines.push(` Model: ${a.model ?? config.model}`);
380
+ if (running && username) {
381
+ lines.push(` @${username}`);
382
+ }
383
+ else if (a.telegram?.botToken && !running) {
384
+ lines.push(` Telegram: stopped`);
385
+ }
386
+ else if (!a.telegram?.botToken && !a.admin) {
387
+ lines.push(` No channel configured`);
388
+ }
389
+ lines.push("");
390
+ // Add chat link button for running bots (except admin)
391
+ if (running && username && !a.admin) {
392
+ kb.url(`💬 ${a.name}`, `https://t.me/${username}`);
393
+ hasButtons = true;
394
+ }
395
+ }
396
+ // Management buttons
397
+ if (hasButtons)
398
+ kb.row();
399
+ kb.text("➕ New agent", "agents:newagent");
400
+ const stoppedAgents = entries.filter(([id, a]) => !runningBots.includes(id) && a.telegram?.botToken && !a.admin);
401
+ if (stoppedAgents.length > 0) {
402
+ kb.text("🔄 Restart stopped", "agents:restart");
403
+ }
404
+ await ctx.reply(lines.join("\n"), { reply_markup: kb });
405
+ });
406
+ b.callbackQuery("agents:newagent", async (ctx) => {
407
+ await ctx.answerCallbackQuery();
408
+ const config = getConfig();
409
+ if (!config.apiKey) {
410
+ await ctx.reply("No API key configured. Run /setup first.");
411
+ return;
412
+ }
413
+ await startWizard(ctx.chat.id, createNewAgentWizard(getConfig, getSystemPrompt), b);
414
+ });
415
+ b.callbackQuery("agents:restart", async (ctx) => {
416
+ await ctx.answerCallbackQuery();
417
+ const config = getConfig();
418
+ const restarted = [];
419
+ for (const [id, a] of Object.entries(config.agents)) {
420
+ if (a.admin)
421
+ continue;
422
+ if (!a.telegram?.botToken)
423
+ continue;
424
+ if (getActiveBotIds().includes(id))
425
+ continue;
426
+ try {
427
+ const sysPrompt = getSystemPrompt();
428
+ await startBot(id, a.telegram.botToken, getConfig, () => sysPrompt);
429
+ restarted.push(id);
430
+ }
431
+ catch { /* skip */ }
432
+ }
433
+ await ctx.reply(restarted.length > 0
434
+ ? `Restarted: ${restarted.join(", ")}`
435
+ : "No stopped bots to restart.");
436
+ });
437
+ b.command("deleteagent", async (ctx) => {
438
+ const config = getConfig();
439
+ const deletable = Object.entries(config.agents).filter(([, a]) => !a.admin);
440
+ if (deletable.length === 0) {
441
+ await ctx.reply("No agents to delete.");
442
+ return;
443
+ }
444
+ const kb = new InlineKeyboard();
445
+ for (const [id, a] of deletable) {
446
+ kb.text(`${a.name}`, `pickdelete:${id}`).row();
447
+ }
448
+ await ctx.reply("Which agent do you want to delete?", { reply_markup: kb });
449
+ });
450
+ b.command("soul", async (ctx) => {
451
+ const args = (ctx.match ?? "").trim().split(/\s+/);
452
+ const targetId = args[0];
453
+ const action = args[1];
454
+ if (!targetId) {
455
+ const config = getConfig();
456
+ const entries = Object.entries(config.agents);
457
+ if (entries.length === 0) {
458
+ await ctx.reply("No agents. Use /newagent first.");
459
+ return;
460
+ }
461
+ const kb = new InlineKeyboard();
462
+ for (const [id, a] of entries) {
463
+ kb.text(`${a.name}`, `picksoul:view:${id}`).row();
464
+ }
465
+ await ctx.reply("Which agent's SOUL.md?", { reply_markup: kb });
466
+ return;
467
+ }
468
+ const config = getConfig();
469
+ if (!config.agents[targetId]) {
470
+ await ctx.reply(`Agent "${targetId}" not found.`);
471
+ return;
472
+ }
473
+ await showSoul(ctx.chat.id, targetId, action === "edit", b);
474
+ });
475
+ b.command("config", async (ctx) => {
476
+ const args = (ctx.match ?? "").trim();
477
+ if (!args) {
478
+ const config = getConfig();
479
+ const lines = [
480
+ "Current configuration:\n",
481
+ `Provider: ${config.provider}`,
482
+ `Model: ${config.model}`,
483
+ config.baseUrl ? `Base URL: ${config.baseUrl}` : null,
484
+ `API Key: ${config.apiKey ? "***" + config.apiKey.slice(-4) : "not set"}`,
485
+ `Thinking: ${config.thinking}`,
486
+ `Effort: ${config.effort}`,
487
+ `Max Turns: ${config.maxTurns}`,
488
+ `Timeout: ${config.timeoutSeconds}s`,
489
+ `Approvals: ${config.approvals.mode}`,
490
+ "",
491
+ "Use /config <key> <value> to change",
492
+ "Examples:",
493
+ " /config model gpt-4o",
494
+ " /config thinking high",
495
+ " /config approvals.mode smart",
496
+ ];
497
+ await ctx.reply(lines.filter(Boolean).join("\n"));
498
+ return;
499
+ }
500
+ const spaceIdx = args.indexOf(" ");
501
+ if (spaceIdx === -1) {
502
+ await ctx.reply(`Usage: /config <key> <value>\n\nExample: /config model gpt-4o`);
503
+ return;
504
+ }
505
+ const key = args.slice(0, spaceIdx).trim();
506
+ const value = args.slice(spaceIdx + 1).trim();
507
+ const parts = key.split(".");
508
+ let update;
509
+ if (parts.length === 1) {
510
+ let parsed = value;
511
+ if (value === "true")
512
+ parsed = true;
513
+ else if (value === "false")
514
+ parsed = false;
515
+ else if (/^\d+$/.test(value))
516
+ parsed = parseInt(value, 10);
517
+ update = { [key]: parsed };
518
+ }
519
+ else {
520
+ const config = getConfig();
521
+ const topKey = parts[0];
522
+ const subKey = parts.slice(1).join(".");
523
+ const existing = config[topKey];
524
+ if (typeof existing === "object" && existing !== null) {
525
+ let parsed = value;
526
+ if (value === "true")
527
+ parsed = true;
528
+ else if (value === "false")
529
+ parsed = false;
530
+ else if (/^\d+$/.test(value))
531
+ parsed = parseInt(value, 10);
532
+ update = { [topKey]: { ...existing, [subKey]: parsed } };
533
+ }
534
+ else {
535
+ await ctx.reply(`Unknown config section: ${topKey}`);
536
+ return;
537
+ }
538
+ }
539
+ try {
540
+ saveConfig(update);
541
+ await ctx.reply(`${key} = ${value}`);
542
+ }
543
+ catch (err) {
544
+ await ctx.reply(`Error: ${err instanceof Error ? err.message : String(err)}`);
545
+ }
546
+ });
547
+ b.command("sessions", async (ctx) => {
548
+ const sessions = listSessions();
549
+ if (sessions.length === 0) {
550
+ await ctx.reply("No sessions.");
551
+ return;
552
+ }
553
+ const recent = sessions.slice(0, 15);
554
+ const lines = [`Sessions (${sessions.length} total):\n`];
555
+ for (const s of recent) {
556
+ const age = formatAge(s.createdAt);
557
+ const msgs = loadMessages(s.id).length;
558
+ const label = s.label ? ` (${s.label})` : "";
559
+ lines.push(`${s.id}${label}`);
560
+ lines.push(` ${s.model} · ${msgs} msgs · ${age}`);
561
+ }
562
+ if (sessions.length > 15) {
563
+ lines.push(`\n... and ${sessions.length - 15} more`);
564
+ }
565
+ const kb = new InlineKeyboard()
566
+ .text("Clear > 1 day", "clearsessions:1d")
567
+ .text("Clear > 1 week", "clearsessions:1w")
568
+ .text("Clear > 1 month", "clearsessions:1m");
569
+ await ctx.reply(lines.join("\n"), { reply_markup: kb });
570
+ });
571
+ b.command("status", async (ctx) => {
572
+ const config = getConfig();
573
+ const runningBots = getActiveBotIds();
574
+ const sessions = listSessions();
575
+ const lines = [
576
+ "CamelAGI Status\n",
577
+ `Provider: ${config.provider}`,
578
+ `Model: ${config.model}`,
579
+ `API Key: ${config.apiKey ? "set" : "not set"}`,
580
+ "",
581
+ `Bots: ${runningBots.length} running`,
582
+ ];
583
+ for (const id of runningBots) {
584
+ lines.push(` running: ${id}`);
585
+ }
586
+ const allIds = Object.keys(config.agents);
587
+ const stoppedIds = allIds.filter((id) => !runningBots.includes(id));
588
+ for (const id of stoppedIds) {
589
+ lines.push(` stopped: ${id}`);
590
+ }
591
+ lines.push("");
592
+ lines.push(`Sessions: ${sessions.length}`);
593
+ lines.push(`Approvals: ${config.approvals.mode}`);
594
+ await ctx.reply(lines.join("\n"));
595
+ });
596
+ b.command("restart", async (ctx) => {
597
+ const targetId = (ctx.match ?? "").trim();
598
+ const config = getConfig();
599
+ if (targetId) {
600
+ if (!config.agents[targetId]) {
601
+ await ctx.reply(`Agent "${targetId}" not found.`);
602
+ return;
603
+ }
604
+ const token = config.agents[targetId].telegram?.botToken;
605
+ if (!token) {
606
+ await ctx.reply(`Agent "${targetId}" has no Telegram bot.`);
607
+ return;
608
+ }
609
+ stopBot(targetId);
610
+ try {
611
+ const sysPrompt = getSystemPrompt();
612
+ await startBot(targetId, token, getConfig, () => sysPrompt);
613
+ await ctx.reply(`Restarted ${targetId}`);
614
+ }
615
+ catch (err) {
616
+ await ctx.reply(`Error restarting: ${err instanceof Error ? err.message : String(err)}`);
617
+ }
618
+ return;
619
+ }
620
+ const restarted = [];
621
+ for (const [id, a] of Object.entries(config.agents)) {
622
+ if (a.admin)
623
+ continue;
624
+ if (!a.telegram?.botToken)
625
+ continue;
626
+ stopBot(id);
627
+ try {
628
+ const sysPrompt = getSystemPrompt();
629
+ await startBot(id, a.telegram.botToken, getConfig, () => sysPrompt);
630
+ restarted.push(id);
631
+ }
632
+ catch { /* skip */ }
633
+ }
634
+ await ctx.reply(restarted.length > 0
635
+ ? `Restarted: ${restarted.join(", ")}`
636
+ : "No bots to restart.");
637
+ });
638
+ b.command("pairing", async (ctx) => {
639
+ const requests = listPendingRequests();
640
+ if (requests.length === 0) {
641
+ await ctx.reply("No pending access requests.");
642
+ return;
643
+ }
644
+ for (const r of requests) {
645
+ const userLabel = r.username ? `@${r.username}` : r.firstName ?? String(r.userId);
646
+ const age = formatAge(r.requestedAt);
647
+ const text = `Pending request\n\nUser: ${userLabel} (${r.userId})\nAgent: ${r.agentId}\nCode: ${r.code}\nRequested: ${age}`;
648
+ const kb = new InlineKeyboard()
649
+ .text("Approve", `pairing:approve:${r.code}`)
650
+ .text("Deny", `pairing:deny:${r.code}`);
651
+ await ctx.reply(text, { reply_markup: kb });
652
+ }
653
+ });
654
+ // ─── Voice configuration ─────────────────────────────────────────
655
+ b.command("voice", async (ctx) => {
656
+ const config = getConfig();
657
+ if (config.voice.enabled && config.voice.apiKey) {
658
+ const masked = "***" + config.voice.apiKey.slice(-4);
659
+ const kb = new InlineKeyboard()
660
+ .text("Reconfigure", "voice:reconfigure")
661
+ .text("Reset", "voice:reset");
662
+ await ctx.reply([
663
+ "Voice transcription: enabled\n",
664
+ `Provider: ${config.voice.provider}`,
665
+ `Model: ${config.voice.model ?? "default"}`,
666
+ `API Key: ${masked}`,
667
+ config.voice.language ? `Language: ${config.voice.language}` : null,
668
+ ].filter(Boolean).join("\n"), { reply_markup: kb });
669
+ }
670
+ else {
671
+ await startWizard(ctx.chat.id, createVoiceWizard(getConfig), b);
672
+ }
673
+ });
674
+ b.callbackQuery(/^voice:(reconfigure|reset)$/, async (ctx) => {
675
+ const action = ctx.callbackQuery.data.match(/^voice:(reconfigure|reset)$/)?.[1];
676
+ await ctx.answerCallbackQuery();
677
+ try {
678
+ await ctx.editMessageText(`${ctx.callbackQuery.message?.text ?? ""}\n\n-> ${action}`);
679
+ }
680
+ catch { }
681
+ if (action === "reconfigure") {
682
+ await startWizard(ctx.chat.id, createVoiceWizard(getConfig), b);
683
+ }
684
+ else {
685
+ await startWizard(ctx.chat.id, createVoiceResetWizard(), b);
686
+ }
687
+ });
688
+ // ─── Message handler: wizard intercept ─────────────────────────────
689
+ b.on("message:text", async (ctx) => {
690
+ const chatId = ctx.chat.id;
691
+ const text = ctx.message.text;
692
+ if (hasActiveWizard(chatId)) {
693
+ if (text === "/cancel") {
694
+ cancelWizard(chatId);
695
+ await ctx.reply("Wizard cancelled.");
696
+ return;
697
+ }
698
+ const handled = await advanceWizard(chatId, text, b);
699
+ if (handled)
700
+ return;
701
+ }
702
+ });
703
+ return b;
704
+ }
705
+ //# sourceMappingURL=admin-bot.js.map