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,800 @@
1
+ // Admin bot: BotFather-style Telegram control plane for OpenCamel
2
+ // Conversational wizards + admin commands โ€” all managed from Telegram.
3
+ import { Bot, InlineKeyboard } from "grammy";
4
+ import fs from "node:fs";
5
+ import path from "node:path";
6
+ import { loadConfig, saveConfig } from "./core/config.js";
7
+ import { seedAgentWorkspace, agentMemoryDir } from "./workspace.js";
8
+ import { listSessions, deleteSession, loadMessages } from "./session.js";
9
+ import { getActiveBotIds, startBot, stopBot } from "./telegram.js";
10
+ // โ”€โ”€โ”€ Presets (shared with setup.ts) โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€
11
+ const PRESETS = {
12
+ anthropic: {
13
+ provider: "anthropic",
14
+ models: ["claude-sonnet-4-20250514", "claude-opus-4-20250514", "claude-haiku-4-20250506"],
15
+ },
16
+ openai: {
17
+ provider: "openai",
18
+ models: ["gpt-4o", "gpt-4o-mini", "o3", "o4-mini"],
19
+ },
20
+ openrouter: {
21
+ provider: "openai",
22
+ baseUrl: "https://openrouter.ai/api/v1",
23
+ models: [
24
+ "anthropic/claude-sonnet-4-20250514",
25
+ "anthropic/claude-opus-4-20250514",
26
+ "openai/gpt-4o",
27
+ "google/gemini-2.5-pro",
28
+ "deepseek/deepseek-r1",
29
+ "deepseek/deepseek-chat",
30
+ ],
31
+ },
32
+ ollama: {
33
+ provider: "openai",
34
+ baseUrl: "http://localhost:11434/v1",
35
+ models: ["llama3.3", "qwen3", "deepseek-r1", "gemma3"],
36
+ },
37
+ };
38
+ const WIZARD_TIMEOUT_MS = 10 * 60 * 1000; // 10 minutes
39
+ const activeWizards = new Map();
40
+ function cancelWizard(chatId) {
41
+ const w = activeWizards.get(chatId);
42
+ if (!w)
43
+ return false;
44
+ clearTimeout(w.timer);
45
+ activeWizards.delete(chatId);
46
+ return true;
47
+ }
48
+ async function startWizard(chatId, def, bot) {
49
+ cancelWizard(chatId); // cancel any existing
50
+ const timer = setTimeout(() => {
51
+ activeWizards.delete(chatId);
52
+ bot.api.sendMessage(chatId, "Wizard timed out. Run the command again.").catch(() => { });
53
+ }, WIZARD_TIMEOUT_MS);
54
+ const wizard = { def, stepIndex: 0, data: {}, chatId, timer };
55
+ activeWizards.set(chatId, wizard);
56
+ await sendCurrentStep(wizard, bot);
57
+ }
58
+ async function sendCurrentStep(wizard, bot) {
59
+ // Skip steps that have a skip condition
60
+ while (wizard.stepIndex < wizard.def.steps.length) {
61
+ const step = wizard.def.steps[wizard.stepIndex];
62
+ if (step.skip?.(wizard.data)) {
63
+ wizard.stepIndex++;
64
+ continue;
65
+ }
66
+ break;
67
+ }
68
+ if (wizard.stepIndex >= wizard.def.steps.length) {
69
+ // All steps done
70
+ await completeWizard(wizard, bot);
71
+ return;
72
+ }
73
+ const step = wizard.def.steps[wizard.stepIndex];
74
+ if (step.options) {
75
+ const kb = new InlineKeyboard();
76
+ for (const opt of step.options) {
77
+ kb.text(opt.label, `wizard:${step.id}:${opt.value}`);
78
+ }
79
+ // Split into rows of 3
80
+ await bot.api.sendMessage(wizard.chatId, step.prompt, { reply_markup: kb });
81
+ }
82
+ else {
83
+ await bot.api.sendMessage(wizard.chatId, step.prompt);
84
+ }
85
+ }
86
+ async function advanceWizard(chatId, input, bot) {
87
+ const wizard = activeWizards.get(chatId);
88
+ if (!wizard)
89
+ return false;
90
+ const step = wizard.def.steps[wizard.stepIndex];
91
+ const value = step.transform ? step.transform(input) : input.trim();
92
+ if (step.validate) {
93
+ const error = step.validate(value, wizard.data);
94
+ if (error) {
95
+ await bot.api.sendMessage(chatId, `${error}\n\nTry again or /cancel:`);
96
+ return true;
97
+ }
98
+ }
99
+ wizard.data[step.id] = value;
100
+ wizard.stepIndex++;
101
+ // Reset timeout
102
+ clearTimeout(wizard.timer);
103
+ wizard.timer = setTimeout(() => {
104
+ activeWizards.delete(chatId);
105
+ bot.api.sendMessage(chatId, "Wizard timed out. Run the command again.").catch(() => { });
106
+ }, WIZARD_TIMEOUT_MS);
107
+ await sendCurrentStep(wizard, bot);
108
+ return true;
109
+ }
110
+ async function completeWizard(wizard, bot) {
111
+ cancelWizard(wizard.chatId);
112
+ try {
113
+ const message = await wizard.def.onComplete(wizard.data, wizard.chatId, bot);
114
+ await bot.api.sendMessage(wizard.chatId, message);
115
+ }
116
+ catch (err) {
117
+ await bot.api.sendMessage(wizard.chatId, `Error: ${err instanceof Error ? err.message : String(err)}`);
118
+ }
119
+ }
120
+ function hasActiveWizard(chatId) {
121
+ return activeWizards.has(chatId);
122
+ }
123
+ // โ”€โ”€โ”€ Telegram Token Validation โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€
124
+ async function validateBotToken(token) {
125
+ try {
126
+ const resp = await fetch(`https://api.telegram.org/bot${token}/getMe`);
127
+ const data = await resp.json();
128
+ if (data.ok && data.result) {
129
+ return { ok: true, username: data.result.username, name: data.result.first_name };
130
+ }
131
+ return { ok: false, error: data.description ?? "Invalid token" };
132
+ }
133
+ catch (err) {
134
+ return { ok: false, error: err instanceof Error ? err.message : String(err) };
135
+ }
136
+ }
137
+ // โ”€โ”€โ”€ Wizard Definitions โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€
138
+ function createSetupWizard(getConfig) {
139
+ return {
140
+ id: "setup",
141
+ steps: [
142
+ {
143
+ id: "service",
144
+ prompt: "Which AI provider?",
145
+ options: [
146
+ { label: "Anthropic", value: "anthropic" },
147
+ { label: "OpenAI", value: "openai" },
148
+ { label: "OpenRouter", value: "openrouter" },
149
+ { label: "Ollama", value: "ollama" },
150
+ { label: "Custom", value: "custom" },
151
+ ],
152
+ },
153
+ {
154
+ id: "apiKey",
155
+ prompt: "Enter your API key:",
156
+ skip: (data) => data.service === "ollama",
157
+ },
158
+ {
159
+ id: "baseUrl",
160
+ prompt: "Enter the base URL (e.g. http://localhost:8080/v1):",
161
+ skip: (data) => data.service !== "custom",
162
+ },
163
+ {
164
+ id: "model",
165
+ prompt: "Which model? (type a name or pick below)",
166
+ // Options are set dynamically via a workaround โ€” we pre-populate common choices
167
+ // The user can also type a custom model name
168
+ },
169
+ ],
170
+ onComplete: async (data) => {
171
+ const preset = PRESETS[data.service];
172
+ const values = {
173
+ provider: preset?.provider ?? "openai",
174
+ model: data.model,
175
+ };
176
+ if (data.apiKey)
177
+ values.apiKey = data.apiKey;
178
+ if (data.service === "custom" && data.baseUrl) {
179
+ values.baseUrl = data.baseUrl;
180
+ }
181
+ else if (preset?.baseUrl) {
182
+ values.baseUrl = preset.baseUrl;
183
+ }
184
+ saveConfig(values);
185
+ const maskedKey = data.apiKey ? `***${data.apiKey.slice(-4)}` : "not set";
186
+ return [
187
+ "โœ… Setup complete!\n",
188
+ `Provider: ${values.provider}`,
189
+ `Model: ${data.model}`,
190
+ values.baseUrl ? `Base URL: ${values.baseUrl}` : null,
191
+ `API Key: ${maskedKey}`,
192
+ "",
193
+ "Create your first agent with /newagent",
194
+ ].filter(Boolean).join("\n");
195
+ },
196
+ };
197
+ }
198
+ /** Generate a unique slug ID from a display name */
199
+ function nameToId(name, existingIds) {
200
+ // "Personal Finance" โ†’ "personalfinance"
201
+ let base = name.replace(/[^a-z0-9]/gi, "").toLowerCase();
202
+ if (!base)
203
+ base = "agent";
204
+ let id = base;
205
+ let counter = 1;
206
+ while (existingIds.includes(id)) {
207
+ counter++;
208
+ id = `${base}${counter}`;
209
+ }
210
+ return id;
211
+ }
212
+ function createNewAgentWizard(getConfig, getSystemPrompt) {
213
+ return {
214
+ id: "newagent",
215
+ steps: [
216
+ {
217
+ id: "name",
218
+ prompt: "Agent name (e.g. \"Personal Finance\", \"Coder\", \"Journal\"):",
219
+ validate: (value) => value ? null : "Name cannot be empty.",
220
+ },
221
+ {
222
+ id: "description",
223
+ prompt: "What does this agent do? (one line, goes into SOUL.md):",
224
+ },
225
+ {
226
+ id: "model",
227
+ prompt: "Model:",
228
+ options: [
229
+ { label: "Use default", value: "__default__" },
230
+ { label: "Change", value: "__custom__" },
231
+ ],
232
+ },
233
+ {
234
+ id: "modelCustom",
235
+ prompt: "Enter model name:",
236
+ skip: (data) => data.model !== "__custom__",
237
+ },
238
+ {
239
+ id: "token",
240
+ prompt: "Telegram bot token (create one in @BotFather):",
241
+ options: [
242
+ { label: "Skip Telegram", value: "__skip__" },
243
+ ],
244
+ },
245
+ ],
246
+ onComplete: async (data, _chatId, _bot) => {
247
+ const config = getConfig();
248
+ // Auto-generate unique ID from name
249
+ const existingIds = Object.keys(config.agents);
250
+ const id = nameToId(data.name, existingIds);
251
+ // Resolve model
252
+ const model = data.model === "__custom__" ? data.modelCustom : undefined;
253
+ // Validate telegram token if provided
254
+ let tokenInfo = "";
255
+ if (data.token && data.token !== "__skip__") {
256
+ const result = await validateBotToken(data.token);
257
+ if (!result.ok) {
258
+ return `Token validation failed: ${result.error}\n\nRun /newagent again.`;
259
+ }
260
+ tokenInfo = `\nTelegram: @${result.username}`;
261
+ }
262
+ // Seed workspace
263
+ seedAgentWorkspace(id, data.name, data.description);
264
+ // Save config
265
+ const agentConfig = { name: data.name };
266
+ if (model)
267
+ agentConfig.model = model;
268
+ if (data.token && data.token !== "__skip__") {
269
+ agentConfig.telegram = {
270
+ botToken: data.token,
271
+ allowedUsers: config.agents.admin?.telegram?.allowedUsers ?? [],
272
+ };
273
+ }
274
+ const agents = { ...config.agents, [id]: agentConfig };
275
+ saveConfig({ agents });
276
+ // Hot-start the bot if it has a telegram token
277
+ if (data.token && data.token !== "__skip__") {
278
+ try {
279
+ const freshConfig = loadConfig();
280
+ const sysPrompt = getSystemPrompt();
281
+ await startBot(id, data.token, () => freshConfig, () => sysPrompt);
282
+ }
283
+ catch (err) {
284
+ tokenInfo += `\nโš ๏ธ Bot start failed: ${err instanceof Error ? err.message : String(err)}`;
285
+ }
286
+ }
287
+ const dir = agentMemoryDir(id);
288
+ return [
289
+ `โœ… Agent created!\n`,
290
+ `Name: ${data.name}`,
291
+ `ID: ${id}`,
292
+ `Model: ${model ?? config.model} (default)`,
293
+ tokenInfo,
294
+ `\n๐Ÿ“ ${dir}`,
295
+ ].filter(Boolean).join("\n");
296
+ },
297
+ };
298
+ }
299
+ // โ”€โ”€โ”€ Setup Admin Bot โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€
300
+ export async function setupAdminBot(agentId, botToken, getConfig, getSystemPrompt) {
301
+ const b = new Bot(botToken);
302
+ // Register admin commands with Telegram menu
303
+ await b.api.setMyCommands([
304
+ { command: "help", description: "List all commands" },
305
+ { command: "setup", description: "Configure API provider, key, model" },
306
+ { command: "newagent", description: "Create a new agent" },
307
+ { command: "agents", description: "List all agents" },
308
+ { command: "deleteagent", description: "Delete an agent" },
309
+ { command: "soul", description: "View/edit agent SOUL.md" },
310
+ { command: "config", description: "View/update configuration" },
311
+ { command: "sessions", description: "Manage sessions" },
312
+ { command: "status", description: "System health and stats" },
313
+ { command: "restart", description: "Restart agent bots" },
314
+ { command: "cancel", description: "Cancel active wizard" },
315
+ ]).catch(() => { });
316
+ // Access control
317
+ b.use(async (ctx, next) => {
318
+ const config = getConfig();
319
+ const agent = config.agents[agentId];
320
+ const allowedUsers = agent?.telegram?.allowedUsers ?? [];
321
+ if (allowedUsers.length === 0) {
322
+ await next();
323
+ return;
324
+ }
325
+ const userId = ctx.from?.id;
326
+ if (!userId || !allowedUsers.includes(userId)) {
327
+ if (ctx.chat?.type === "group" || ctx.chat?.type === "supergroup")
328
+ return;
329
+ await ctx.reply("Access denied.");
330
+ return;
331
+ }
332
+ await next();
333
+ });
334
+ // โ”€โ”€โ”€ Wizard callback queries โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€
335
+ b.callbackQuery(/^wizard:(.+):(.+)$/, async (ctx) => {
336
+ const match = ctx.callbackQuery.data.match(/^wizard:(.+):(.+)$/);
337
+ if (!match)
338
+ return;
339
+ const [, stepId, value] = match;
340
+ const chatId = ctx.chat?.id;
341
+ if (!chatId)
342
+ return;
343
+ // Edit the message to show the selection
344
+ try {
345
+ await ctx.editMessageText(`${ctx.callbackQuery.message?.text ?? ""}\n\nโ†’ ${value}`);
346
+ }
347
+ catch { /* ignore */ }
348
+ await ctx.answerCallbackQuery();
349
+ await advanceWizard(chatId, value, b);
350
+ });
351
+ // Pick agent for SOUL.md view/edit
352
+ b.callbackQuery(/^picksoul:(.+):(.+)$/, async (ctx) => {
353
+ const match = ctx.callbackQuery.data.match(/^picksoul:(.+):(.+)$/);
354
+ if (!match)
355
+ return;
356
+ const [, action, id] = match;
357
+ await ctx.answerCallbackQuery();
358
+ try {
359
+ await ctx.editMessageText(`โ†’ ${id}`);
360
+ }
361
+ catch { }
362
+ await showSoul(ctx.chat.id, id, action === "edit", b);
363
+ });
364
+ // Pick agent to delete โ†’ show confirmation
365
+ b.callbackQuery(/^pickdelete:(.+)$/, async (ctx) => {
366
+ const match = ctx.callbackQuery.data.match(/^pickdelete:(.+)$/);
367
+ if (!match)
368
+ return;
369
+ const id = match[1];
370
+ await ctx.answerCallbackQuery();
371
+ const config = getConfig();
372
+ if (!config.agents[id]) {
373
+ try {
374
+ await ctx.editMessageText("Agent not found.");
375
+ }
376
+ catch { }
377
+ return;
378
+ }
379
+ const kb = new InlineKeyboard()
380
+ .text("๐Ÿ—‘ Yes, delete", `confirm:delete:${id}`)
381
+ .text("Cancel", `confirm:cancel:${id}`);
382
+ try {
383
+ await ctx.editMessageText(`Delete "${config.agents[id].name}" (${id})?\n\nBot will be stopped. Workspace files are preserved.`, { reply_markup: kb });
384
+ }
385
+ catch { }
386
+ });
387
+ // Confirmation callback queries (deleteagent, sessions clear)
388
+ b.callbackQuery(/^confirm:(.+):(.+)$/, async (ctx) => {
389
+ const match = ctx.callbackQuery.data.match(/^confirm:(.+):(.+)$/);
390
+ if (!match)
391
+ return;
392
+ const [, action, param] = match;
393
+ try {
394
+ await ctx.editMessageText(`${ctx.callbackQuery.message?.text ?? ""}\n\nโ†’ ${action === "delete" ? "Deleting..." : "Cancelled"}`);
395
+ }
396
+ catch { /* ignore */ }
397
+ await ctx.answerCallbackQuery();
398
+ if (action === "delete") {
399
+ const config = getConfig();
400
+ if (config.agents[param]) {
401
+ // Stop the bot if running
402
+ stopBot(param);
403
+ // Remove from config
404
+ const agents = { ...config.agents };
405
+ delete agents[param];
406
+ saveConfig({ agents });
407
+ await ctx.reply(`โœ… Agent "${param}" deleted.\nWorkspace files preserved at ${agentMemoryDir(param)}`);
408
+ }
409
+ }
410
+ });
411
+ b.callbackQuery(/^clearsessions:(.+)$/, async (ctx) => {
412
+ const match = ctx.callbackQuery.data.match(/^clearsessions:(.+)$/);
413
+ if (!match)
414
+ return;
415
+ const [, period] = match;
416
+ const cutoff = { "1d": 86400000, "1w": 604800000, "1m": 2592000000 }[period] ?? 604800000;
417
+ const now = Date.now();
418
+ const sessions = listSessions();
419
+ let deleted = 0;
420
+ for (const s of sessions) {
421
+ if (now - s.createdAt > cutoff) {
422
+ deleteSession(s.id);
423
+ deleted++;
424
+ }
425
+ }
426
+ try {
427
+ await ctx.editMessageText(`${ctx.callbackQuery.message?.text ?? ""}\n\nโ†’ Deleted ${deleted} sessions`);
428
+ }
429
+ catch { /* ignore */ }
430
+ await ctx.answerCallbackQuery();
431
+ });
432
+ // โ”€โ”€โ”€ Commands โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€
433
+ b.command("start", async (ctx) => {
434
+ const config = getConfig();
435
+ const agent = config.agents[agentId];
436
+ const allowedUsers = agent?.telegram?.allowedUsers ?? [];
437
+ // Auto-lock: if no allowed users yet, lock to the first person who sends /start
438
+ if (allowedUsers.length === 0 && ctx.from?.id) {
439
+ const agents = { ...config.agents };
440
+ const adminAgent = { ...agents[agentId] };
441
+ const tg = { ...adminAgent.telegram, allowedUsers: [ctx.from.id] };
442
+ adminAgent.telegram = tg;
443
+ agents[agentId] = adminAgent;
444
+ saveConfig({ agents });
445
+ await ctx.reply(`๐Ÿ”’ Locked to your account (${ctx.from.id}).`);
446
+ }
447
+ const hasApiKey = !!config.apiKey;
448
+ const agentCount = Object.keys(config.agents).length;
449
+ const lines = [
450
+ "๐Ÿช OpenCamel Admin\n",
451
+ "I manage your AI agents from here.",
452
+ "",
453
+ hasApiKey ? `โœ… API configured (${config.provider}, ${config.model})` : "โš ๏ธ No API key โ€” run /setup first",
454
+ `${agentCount} agent(s) configured`,
455
+ "",
456
+ "Commands:",
457
+ "/setup โ€” configure API provider & key",
458
+ "/newagent โ€” create an agent",
459
+ "/agents โ€” list agents",
460
+ "/help โ€” all commands",
461
+ ];
462
+ await ctx.reply(lines.join("\n"));
463
+ });
464
+ b.command("help", async (ctx) => {
465
+ await ctx.reply([
466
+ "๐Ÿช OpenCamel Admin Commands\n",
467
+ "Setup & Config:",
468
+ " /setup โ€” configure API provider, key, model",
469
+ " /config โ€” view current config",
470
+ " /config <key> <value> โ€” update config",
471
+ "",
472
+ "Agents:",
473
+ " /newagent โ€” create a new agent",
474
+ " /agents โ€” list all agents",
475
+ " /deleteagent โ€” pick & delete an agent",
476
+ " /soul โ€” view/edit agent personality",
477
+ "",
478
+ "Sessions & Status:",
479
+ " /sessions โ€” list sessions",
480
+ " /status โ€” system health",
481
+ " /restart โ€” restart all bots",
482
+ "",
483
+ " /cancel โ€” cancel active wizard",
484
+ ].join("\n"));
485
+ });
486
+ b.command("cancel", async (ctx) => {
487
+ if (cancelWizard(ctx.chat.id)) {
488
+ await ctx.reply("Wizard cancelled.");
489
+ }
490
+ else {
491
+ await ctx.reply("No active wizard.");
492
+ }
493
+ });
494
+ b.command("setup", async (ctx) => {
495
+ await startWizard(ctx.chat.id, createSetupWizard(getConfig), b);
496
+ });
497
+ b.command("newagent", async (ctx) => {
498
+ const config = getConfig();
499
+ if (!config.apiKey) {
500
+ await ctx.reply("โš ๏ธ No API key configured. Run /setup first.");
501
+ return;
502
+ }
503
+ await startWizard(ctx.chat.id, createNewAgentWizard(getConfig, getSystemPrompt), b);
504
+ });
505
+ b.command("agents", async (ctx) => {
506
+ const config = getConfig();
507
+ const entries = Object.entries(config.agents);
508
+ if (entries.length === 0) {
509
+ await ctx.reply("No agents configured.\n\nUse /newagent to create one.");
510
+ return;
511
+ }
512
+ const runningBots = getActiveBotIds();
513
+ const lines = ["Your agents:\n"];
514
+ for (const [id, a] of entries) {
515
+ const running = runningBots.includes(id);
516
+ const status = running ? "๐ŸŸข running" : "โšซ stopped";
517
+ lines.push(`${a.admin ? "๐Ÿ‘‘" : "๐Ÿค–"} ${a.name} (${id}) โ€” ${status}`);
518
+ lines.push(` Model: ${a.model ?? config.model}`);
519
+ if (a.telegram)
520
+ lines.push(` Telegram: configured`);
521
+ lines.push("");
522
+ }
523
+ lines.push("/soul <id> โ€” edit personality");
524
+ lines.push("/deleteagent <id> โ€” delete");
525
+ await ctx.reply(lines.join("\n"));
526
+ });
527
+ b.command("deleteagent", async (ctx) => {
528
+ const config = getConfig();
529
+ // Get non-admin agents
530
+ const deletable = Object.entries(config.agents).filter(([, a]) => !a.admin);
531
+ if (deletable.length === 0) {
532
+ await ctx.reply("No agents to delete.");
533
+ return;
534
+ }
535
+ const kb = new InlineKeyboard();
536
+ for (const [id, a] of deletable) {
537
+ kb.text(`๐Ÿ—‘ ${a.name}`, `pickdelete:${id}`).row();
538
+ }
539
+ await ctx.reply("Which agent do you want to delete?", { reply_markup: kb });
540
+ });
541
+ b.command("soul", async (ctx) => {
542
+ const args = (ctx.match ?? "").trim().split(/\s+/);
543
+ const targetId = args[0];
544
+ const action = args[1];
545
+ if (!targetId) {
546
+ // Show inline buttons to pick an agent
547
+ const config = getConfig();
548
+ const entries = Object.entries(config.agents);
549
+ if (entries.length === 0) {
550
+ await ctx.reply("No agents. Use /newagent first.");
551
+ return;
552
+ }
553
+ const kb = new InlineKeyboard();
554
+ for (const [id, a] of entries) {
555
+ kb.text(`๐Ÿ“ ${a.name}`, `picksoul:view:${id}`).row();
556
+ }
557
+ await ctx.reply("Which agent's SOUL.md?", { reply_markup: kb });
558
+ return;
559
+ }
560
+ const config = getConfig();
561
+ if (!config.agents[targetId]) {
562
+ await ctx.reply(`Agent "${targetId}" not found.`);
563
+ return;
564
+ }
565
+ await showSoul(ctx.chat.id, targetId, action === "edit", b);
566
+ });
567
+ b.command("config", async (ctx) => {
568
+ const args = (ctx.match ?? "").trim();
569
+ if (!args) {
570
+ // View all
571
+ const config = getConfig();
572
+ const lines = [
573
+ "Current configuration:\n",
574
+ `Provider: ${config.provider}`,
575
+ `Model: ${config.model}`,
576
+ config.baseUrl ? `Base URL: ${config.baseUrl}` : null,
577
+ `API Key: ${config.apiKey ? "***" + config.apiKey.slice(-4) : "not set"}`,
578
+ `Thinking: ${config.thinking}`,
579
+ `Effort: ${config.effort}`,
580
+ `Max Turns: ${config.maxTurns}`,
581
+ `Timeout: ${config.timeoutSeconds}s`,
582
+ `Approvals: ${config.approvals.mode}`,
583
+ "",
584
+ "Use /config <key> <value> to change",
585
+ "Examples:",
586
+ " /config model gpt-4o",
587
+ " /config thinking high",
588
+ " /config approvals.mode smart",
589
+ ];
590
+ await ctx.reply(lines.filter(Boolean).join("\n"));
591
+ return;
592
+ }
593
+ // Set mode: /config key value
594
+ const spaceIdx = args.indexOf(" ");
595
+ if (spaceIdx === -1) {
596
+ await ctx.reply(`Usage: /config <key> <value>\n\nExample: /config model gpt-4o`);
597
+ return;
598
+ }
599
+ const key = args.slice(0, spaceIdx).trim();
600
+ const value = args.slice(spaceIdx + 1).trim();
601
+ // Handle dot-notation for nested keys
602
+ const parts = key.split(".");
603
+ let update;
604
+ if (parts.length === 1) {
605
+ // Top-level key
606
+ let parsed = value;
607
+ if (value === "true")
608
+ parsed = true;
609
+ else if (value === "false")
610
+ parsed = false;
611
+ else if (/^\d+$/.test(value))
612
+ parsed = parseInt(value, 10);
613
+ update = { [key]: parsed };
614
+ }
615
+ else {
616
+ // Nested: e.g. approvals.mode โ†’ { approvals: { ...existing, mode: value } }
617
+ const config = getConfig();
618
+ const topKey = parts[0];
619
+ const subKey = parts.slice(1).join(".");
620
+ const existing = config[topKey];
621
+ if (typeof existing === "object" && existing !== null) {
622
+ let parsed = value;
623
+ if (value === "true")
624
+ parsed = true;
625
+ else if (value === "false")
626
+ parsed = false;
627
+ else if (/^\d+$/.test(value))
628
+ parsed = parseInt(value, 10);
629
+ update = { [topKey]: { ...existing, [subKey]: parsed } };
630
+ }
631
+ else {
632
+ await ctx.reply(`Unknown config section: ${topKey}`);
633
+ return;
634
+ }
635
+ }
636
+ try {
637
+ saveConfig(update);
638
+ await ctx.reply(`โœ… ${key} = ${value}`);
639
+ }
640
+ catch (err) {
641
+ await ctx.reply(`Error: ${err instanceof Error ? err.message : String(err)}`);
642
+ }
643
+ });
644
+ b.command("sessions", async (ctx) => {
645
+ const sessions = listSessions();
646
+ if (sessions.length === 0) {
647
+ await ctx.reply("No sessions.");
648
+ return;
649
+ }
650
+ const recent = sessions.slice(0, 15);
651
+ const lines = [`Sessions (${sessions.length} total):\n`];
652
+ for (const s of recent) {
653
+ const age = formatAge(s.createdAt);
654
+ const msgs = loadMessages(s.id).length;
655
+ const label = s.label ? ` (${s.label})` : "";
656
+ lines.push(`${s.id}${label}`);
657
+ lines.push(` ${s.model} ยท ${msgs} msgs ยท ${age}`);
658
+ }
659
+ if (sessions.length > 15) {
660
+ lines.push(`\n... and ${sessions.length - 15} more`);
661
+ }
662
+ const kb = new InlineKeyboard()
663
+ .text("Clear > 1 day", "clearsessions:1d")
664
+ .text("Clear > 1 week", "clearsessions:1w")
665
+ .text("Clear > 1 month", "clearsessions:1m");
666
+ await ctx.reply(lines.join("\n"), { reply_markup: kb });
667
+ });
668
+ b.command("status", async (ctx) => {
669
+ const config = getConfig();
670
+ const runningBots = getActiveBotIds();
671
+ const sessions = listSessions();
672
+ const lines = [
673
+ "๐Ÿช OpenCamel Status\n",
674
+ `Provider: ${config.provider}`,
675
+ `Model: ${config.model}`,
676
+ `API Key: ${config.apiKey ? "โœ…" : "โŒ not set"}`,
677
+ "",
678
+ `Bots: ${runningBots.length} running`,
679
+ ];
680
+ for (const id of runningBots) {
681
+ lines.push(` ๐ŸŸข ${id}`);
682
+ }
683
+ // Show stopped bots
684
+ const allIds = Object.keys(config.agents);
685
+ const stoppedIds = allIds.filter((id) => !runningBots.includes(id));
686
+ for (const id of stoppedIds) {
687
+ lines.push(` โšซ ${id}`);
688
+ }
689
+ lines.push("");
690
+ lines.push(`Sessions: ${sessions.length}`);
691
+ lines.push(`Approvals: ${config.approvals.mode}`);
692
+ await ctx.reply(lines.join("\n"));
693
+ });
694
+ b.command("restart", async (ctx) => {
695
+ const targetId = (ctx.match ?? "").trim();
696
+ const config = getConfig();
697
+ if (targetId) {
698
+ // Restart specific agent
699
+ if (!config.agents[targetId]) {
700
+ await ctx.reply(`Agent "${targetId}" not found.`);
701
+ return;
702
+ }
703
+ const token = config.agents[targetId].telegram?.botToken;
704
+ if (!token) {
705
+ await ctx.reply(`Agent "${targetId}" has no Telegram bot.`);
706
+ return;
707
+ }
708
+ stopBot(targetId);
709
+ try {
710
+ const sysPrompt = getSystemPrompt();
711
+ await startBot(targetId, token, getConfig, () => sysPrompt);
712
+ await ctx.reply(`โœ… Restarted ${targetId}`);
713
+ }
714
+ catch (err) {
715
+ await ctx.reply(`Error restarting: ${err instanceof Error ? err.message : String(err)}`);
716
+ }
717
+ return;
718
+ }
719
+ // Restart all non-admin bots
720
+ const restarted = [];
721
+ for (const [id, a] of Object.entries(config.agents)) {
722
+ if (a.admin)
723
+ continue; // Don't restart ourselves
724
+ if (!a.telegram?.botToken)
725
+ continue;
726
+ stopBot(id);
727
+ try {
728
+ const sysPrompt = getSystemPrompt();
729
+ await startBot(id, a.telegram.botToken, getConfig, () => sysPrompt);
730
+ restarted.push(id);
731
+ }
732
+ catch { /* skip */ }
733
+ }
734
+ await ctx.reply(restarted.length > 0
735
+ ? `โœ… Restarted: ${restarted.join(", ")}`
736
+ : "No bots to restart.");
737
+ });
738
+ // โ”€โ”€โ”€ Message handler: wizard intercept, then echo โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€
739
+ b.on("message:text", async (ctx) => {
740
+ const chatId = ctx.chat.id;
741
+ const text = ctx.message.text;
742
+ // If wizard is active, route to it
743
+ if (hasActiveWizard(chatId)) {
744
+ if (text === "/cancel") {
745
+ cancelWizard(chatId);
746
+ await ctx.reply("Wizard cancelled.");
747
+ return;
748
+ }
749
+ const handled = await advanceWizard(chatId, text, b);
750
+ if (handled)
751
+ return;
752
+ }
753
+ // Not a wizard message โ€” for now, echo or ignore
754
+ // In the future, this falls through to the AI agent
755
+ });
756
+ return b;
757
+ }
758
+ // โ”€โ”€โ”€ Helpers โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€
759
+ async function showSoul(chatId, targetId, edit, bot) {
760
+ const soulPath = path.join(agentMemoryDir(targetId), "SOUL.md");
761
+ if (edit) {
762
+ const editWizard = {
763
+ id: "soul-edit",
764
+ steps: [{
765
+ id: "content",
766
+ prompt: `Send the new SOUL.md content for "${targetId}".\nCurrent content will be replaced. /cancel to abort.`,
767
+ }],
768
+ onComplete: async (data) => {
769
+ const dir = agentMemoryDir(targetId);
770
+ fs.mkdirSync(dir, { recursive: true });
771
+ fs.writeFileSync(soulPath, data.content);
772
+ return `โœ… SOUL.md updated for "${targetId}".\n\n${data.content.slice(0, 200)}${data.content.length > 200 ? "..." : ""}`;
773
+ },
774
+ };
775
+ await startWizard(chatId, editWizard, bot);
776
+ return;
777
+ }
778
+ // View mode
779
+ if (!fs.existsSync(soulPath)) {
780
+ await bot.api.sendMessage(chatId, `No SOUL.md for "${targetId}" yet.`);
781
+ return;
782
+ }
783
+ const content = fs.readFileSync(soulPath, "utf-8").trim();
784
+ const preview = content.length > 3800 ? content.slice(0, 3800) + "\n\n[truncated]" : content;
785
+ const kb = new InlineKeyboard()
786
+ .text("โœ๏ธ Edit", `picksoul:edit:${targetId}`);
787
+ await bot.api.sendMessage(chatId, `๐Ÿ“ SOUL.md (${targetId}):\n\n${preview}`, { reply_markup: kb });
788
+ }
789
+ function formatAge(timestamp) {
790
+ const diff = Date.now() - timestamp;
791
+ const minutes = Math.floor(diff / 60000);
792
+ if (minutes < 60)
793
+ return `${minutes}m ago`;
794
+ const hours = Math.floor(minutes / 60);
795
+ if (hours < 24)
796
+ return `${hours}h ago`;
797
+ const days = Math.floor(hours / 24);
798
+ return `${days}d ago`;
799
+ }
800
+ //# sourceMappingURL=telegram-admin.js.map