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.
- package/LICENSE +21 -0
- package/README.md +224 -0
- package/camelagi.mjs +2 -0
- package/config.example.yaml +107 -0
- package/dist/agent/agent-openai.js +206 -0
- package/dist/agent/agent-openai.js.map +1 -0
- package/dist/agent/agent-sdk.js +209 -0
- package/dist/agent/agent-sdk.js.map +1 -0
- package/dist/agent/tool-adapter.js +31 -0
- package/dist/agent/tool-adapter.js.map +1 -0
- package/dist/agent/types.js +3 -0
- package/dist/agent/types.js.map +1 -0
- package/dist/agent.js +17 -0
- package/dist/agent.js.map +1 -0
- package/dist/approval-forward.js +42 -0
- package/dist/approval-forward.js.map +1 -0
- package/dist/approvals.js +151 -0
- package/dist/approvals.js.map +1 -0
- package/dist/boot.js +34 -0
- package/dist/boot.js.map +1 -0
- package/dist/bootstrap.js +451 -0
- package/dist/bootstrap.js.map +1 -0
- package/dist/camelagi-gateway.mjs +93611 -0
- package/dist/camelagi-gateway.mjs.map +7 -0
- package/dist/channels/adapter.js +10 -0
- package/dist/channels/adapter.js.map +1 -0
- package/dist/channels/discord.js +232 -0
- package/dist/channels/discord.js.map +1 -0
- package/dist/channels/handler.js +349 -0
- package/dist/channels/handler.js.map +1 -0
- package/dist/channels/index.js +19 -0
- package/dist/channels/index.js.map +1 -0
- package/dist/channels/registry.js +71 -0
- package/dist/channels/registry.js.map +1 -0
- package/dist/channels/telegram.js +83 -0
- package/dist/channels/telegram.js.map +1 -0
- package/dist/channels/types.js +3 -0
- package/dist/channels/types.js.map +1 -0
- package/dist/chunker.js +102 -0
- package/dist/chunker.js.map +1 -0
- package/dist/cli/cmd-agents.js +65 -0
- package/dist/cli/cmd-agents.js.map +1 -0
- package/dist/cli/cmd-bootstrap.js +10 -0
- package/dist/cli/cmd-bootstrap.js.map +1 -0
- package/dist/cli/cmd-chat.js +32 -0
- package/dist/cli/cmd-chat.js.map +1 -0
- package/dist/cli/cmd-config.js +88 -0
- package/dist/cli/cmd-config.js.map +1 -0
- package/dist/cli/cmd-cron.js +120 -0
- package/dist/cli/cmd-cron.js.map +1 -0
- package/dist/cli/cmd-daemon.js +37 -0
- package/dist/cli/cmd-daemon.js.map +1 -0
- package/dist/cli/cmd-doctor.js +18 -0
- package/dist/cli/cmd-doctor.js.map +1 -0
- package/dist/cli/cmd-logs.js +30 -0
- package/dist/cli/cmd-logs.js.map +1 -0
- package/dist/cli/cmd-pairing.js +41 -0
- package/dist/cli/cmd-pairing.js.map +1 -0
- package/dist/cli/cmd-reset.js +39 -0
- package/dist/cli/cmd-reset.js.map +1 -0
- package/dist/cli/cmd-serve.js +30 -0
- package/dist/cli/cmd-serve.js.map +1 -0
- package/dist/cli/cmd-sessions.js +56 -0
- package/dist/cli/cmd-sessions.js.map +1 -0
- package/dist/cli/cmd-setup.js +11 -0
- package/dist/cli/cmd-setup.js.map +1 -0
- package/dist/cli/cmd-soul.js +43 -0
- package/dist/cli/cmd-soul.js.map +1 -0
- package/dist/cli/parse.js +50 -0
- package/dist/cli/parse.js.map +1 -0
- package/dist/cli/registry.js +15 -0
- package/dist/cli/registry.js.map +1 -0
- package/dist/cli.js +103 -0
- package/dist/cli.js.map +1 -0
- package/dist/compact.js +92 -0
- package/dist/compact.js.map +1 -0
- package/dist/config.js +153 -0
- package/dist/config.js.map +1 -0
- package/dist/constants.js +21 -0
- package/dist/constants.js.map +1 -0
- package/dist/core/config.js +212 -0
- package/dist/core/config.js.map +1 -0
- package/dist/core/constants.js +21 -0
- package/dist/core/constants.js.map +1 -0
- package/dist/core/errors.js +5 -0
- package/dist/core/errors.js.map +1 -0
- package/dist/core/log.js +41 -0
- package/dist/core/log.js.map +1 -0
- package/dist/core/models.js +123 -0
- package/dist/core/models.js.map +1 -0
- package/dist/core/types.js +3 -0
- package/dist/core/types.js.map +1 -0
- package/dist/core/update-check.js +51 -0
- package/dist/core/update-check.js.map +1 -0
- package/dist/cron.js +81 -0
- package/dist/cron.js.map +1 -0
- package/dist/daemon.js +109 -0
- package/dist/daemon.js.map +1 -0
- package/dist/doctor.js +194 -0
- package/dist/doctor.js.map +1 -0
- package/dist/errors.js +5 -0
- package/dist/errors.js.map +1 -0
- package/dist/extensions/approval-forward.js +42 -0
- package/dist/extensions/approval-forward.js.map +1 -0
- package/dist/extensions/approvals.js +144 -0
- package/dist/extensions/approvals.js.map +1 -0
- package/dist/extensions/cron.js +306 -0
- package/dist/extensions/cron.js.map +1 -0
- package/dist/extensions/hooks.js +72 -0
- package/dist/extensions/hooks.js.map +1 -0
- package/dist/extensions/skills.js +97 -0
- package/dist/extensions/skills.js.map +1 -0
- package/dist/gateway/csrf.js +44 -0
- package/dist/gateway/csrf.js.map +1 -0
- package/dist/gateway/logger.js +81 -0
- package/dist/gateway/logger.js.map +1 -0
- package/dist/gateway/rate-limit.js +33 -0
- package/dist/gateway/rate-limit.js.map +1 -0
- package/dist/gateway/routes.js +315 -0
- package/dist/gateway/routes.js.map +1 -0
- package/dist/gateway/state.js +54 -0
- package/dist/gateway/state.js.map +1 -0
- package/dist/gateway/ws-handler.js +200 -0
- package/dist/gateway/ws-handler.js.map +1 -0
- package/dist/gateway-entry.js +16 -0
- package/dist/gateway-entry.js.map +1 -0
- package/dist/hooks.js +72 -0
- package/dist/hooks.js.map +1 -0
- package/dist/lanes.js +62 -0
- package/dist/lanes.js.map +1 -0
- package/dist/model.js +30 -0
- package/dist/model.js.map +1 -0
- package/dist/policy.js +22 -0
- package/dist/policy.js.map +1 -0
- package/dist/queue.js +45 -0
- package/dist/queue.js.map +1 -0
- package/dist/retry.js +96 -0
- package/dist/retry.js.map +1 -0
- package/dist/runs.js +83 -0
- package/dist/runs.js.map +1 -0
- package/dist/runtime/compact.js +99 -0
- package/dist/runtime/compact.js.map +1 -0
- package/dist/runtime/lanes.js +66 -0
- package/dist/runtime/lanes.js.map +1 -0
- package/dist/runtime/orchestrate.js +121 -0
- package/dist/runtime/orchestrate.js.map +1 -0
- package/dist/runtime/queue.js +50 -0
- package/dist/runtime/queue.js.map +1 -0
- package/dist/runtime/retry.js +127 -0
- package/dist/runtime/retry.js.map +1 -0
- package/dist/runtime/runs.js +105 -0
- package/dist/runtime/runs.js.map +1 -0
- package/dist/serve.js +209 -0
- package/dist/serve.js.map +1 -0
- package/dist/session.js +75 -0
- package/dist/session.js.map +1 -0
- package/dist/setup.js +254 -0
- package/dist/setup.js.map +1 -0
- package/dist/skills.js +89 -0
- package/dist/skills.js.map +1 -0
- package/dist/subagent.js +71 -0
- package/dist/subagent.js.map +1 -0
- package/dist/system-prompt.js +157 -0
- package/dist/system-prompt.js.map +1 -0
- package/dist/telegram/admin-bot.js +705 -0
- package/dist/telegram/admin-bot.js.map +1 -0
- package/dist/telegram/agent-bot.js +551 -0
- package/dist/telegram/agent-bot.js.map +1 -0
- package/dist/telegram/bot-approval.js +63 -0
- package/dist/telegram/bot-approval.js.map +1 -0
- package/dist/telegram/draft-stream.js +86 -0
- package/dist/telegram/draft-stream.js.map +1 -0
- package/dist/telegram/format.js +106 -0
- package/dist/telegram/format.js.map +1 -0
- package/dist/telegram/helpers.js +87 -0
- package/dist/telegram/helpers.js.map +1 -0
- package/dist/telegram/pairing-notify.js +52 -0
- package/dist/telegram/pairing-notify.js.map +1 -0
- package/dist/telegram/pairing.js +138 -0
- package/dist/telegram/pairing.js.map +1 -0
- package/dist/telegram/resolve.js +33 -0
- package/dist/telegram/resolve.js.map +1 -0
- package/dist/telegram/transcribe.js +77 -0
- package/dist/telegram/transcribe.js.map +1 -0
- package/dist/telegram/types.js +3 -0
- package/dist/telegram/types.js.map +1 -0
- package/dist/telegram/voice-wizard.js +84 -0
- package/dist/telegram/voice-wizard.js.map +1 -0
- package/dist/telegram/wizard.js +89 -0
- package/dist/telegram/wizard.js.map +1 -0
- package/dist/telegram/wizards.js +297 -0
- package/dist/telegram/wizards.js.map +1 -0
- package/dist/telegram-admin.js +800 -0
- package/dist/telegram-admin.js.map +1 -0
- package/dist/telegram.js +118 -0
- package/dist/telegram.js.map +1 -0
- package/dist/tools/cron.js +94 -0
- package/dist/tools/cron.js.map +1 -0
- package/dist/tools/edit.js +29 -0
- package/dist/tools/edit.js.map +1 -0
- package/dist/tools/exec.js +38 -0
- package/dist/tools/exec.js.map +1 -0
- package/dist/tools/fetch.js +28 -0
- package/dist/tools/fetch.js.map +1 -0
- package/dist/tools/index.js +16 -0
- package/dist/tools/index.js.map +1 -0
- package/dist/tools/memory.js +164 -0
- package/dist/tools/memory.js.map +1 -0
- package/dist/tools/patch.js +284 -0
- package/dist/tools/patch.js.map +1 -0
- package/dist/tools/read.js +26 -0
- package/dist/tools/read.js.map +1 -0
- package/dist/tools/search.js +62 -0
- package/dist/tools/search.js.map +1 -0
- package/dist/tools/subagent.js +48 -0
- package/dist/tools/subagent.js.map +1 -0
- package/dist/tools/write.js +22 -0
- package/dist/tools/write.js.map +1 -0
- package/dist/tui/commands.js +450 -0
- package/dist/tui/commands.js.map +1 -0
- package/dist/tui/components/assistant-message.js +26 -0
- package/dist/tui/components/assistant-message.js.map +1 -0
- package/dist/tui/components/chat-log.js +94 -0
- package/dist/tui/components/chat-log.js.map +1 -0
- package/dist/tui/components/custom-editor.js +40 -0
- package/dist/tui/components/custom-editor.js.map +1 -0
- package/dist/tui/components/hint-bar.js +13 -0
- package/dist/tui/components/hint-bar.js.map +1 -0
- package/dist/tui/components/tool-execution.js +73 -0
- package/dist/tui/components/tool-execution.js.map +1 -0
- package/dist/tui/components/user-message.js +19 -0
- package/dist/tui/components/user-message.js.map +1 -0
- package/dist/tui/components/welcome.js +147 -0
- package/dist/tui/components/welcome.js.map +1 -0
- package/dist/tui/context.js +3 -0
- package/dist/tui/context.js.map +1 -0
- package/dist/tui/theme.js +91 -0
- package/dist/tui/theme.js.map +1 -0
- package/dist/tui/tui.js +389 -0
- package/dist/tui/tui.js.map +1 -0
- package/dist/tui/ws-handler.js +154 -0
- package/dist/tui/ws-handler.js.map +1 -0
- package/dist/types.js +3 -0
- package/dist/types.js.map +1 -0
- package/dist/usage.js +88 -0
- package/dist/usage.js.map +1 -0
- package/dist/workspace.js +245 -0
- package/dist/workspace.js.map +1 -0
- 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
|