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