tg-agent 0.1.0 → 1.0.1
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/dist/auth.js +1 -71
- package/dist/cli.js +1 -1
- package/dist/codexAuth.js +1 -93
- package/dist/config.js +1 -59
- package/dist/customTools.js +9 -386
- package/dist/index.js +14 -954
- package/dist/mcp.js +3 -427
- package/dist/piAgentRunner.js +7 -407
- package/dist/piAiRunner.js +5 -99
- package/dist/proxy.js +1 -19
- package/dist/sessionStore.js +1 -138
- package/dist/types.js +0 -1
- package/dist/utils.js +2 -91
- package/package.json +4 -2
package/dist/index.js
CHANGED
|
@@ -1,954 +1,14 @@
|
|
|
1
|
-
import
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
const
|
|
14
|
-
const
|
|
15
|
-
const withSemaphore = createSemaphore(config.maxConcurrent);
|
|
16
|
-
const activeRuns = new Map();
|
|
17
|
-
function registerActiveRun(userId, run) {
|
|
18
|
-
activeRuns.set(userId, run);
|
|
19
|
-
}
|
|
20
|
-
function clearActiveRun(userId) {
|
|
21
|
-
activeRuns.delete(userId);
|
|
22
|
-
}
|
|
23
|
-
function resolveTelegramParseMode(value) {
|
|
24
|
-
const trimmed = value.trim();
|
|
25
|
-
if (!trimmed) {
|
|
26
|
-
return undefined;
|
|
27
|
-
}
|
|
28
|
-
if (trimmed === "Markdown" || trimmed === "MarkdownV2" || trimmed === "HTML") {
|
|
29
|
-
return trimmed;
|
|
30
|
-
}
|
|
31
|
-
console.warn(`[tg-agent] invalid TELEGRAM_PARSE_MODE=${trimmed}, ignoring`);
|
|
32
|
-
return undefined;
|
|
33
|
-
}
|
|
34
|
-
const MARKDOWN_V2_RESERVED = new Set([
|
|
35
|
-
"_",
|
|
36
|
-
"*",
|
|
37
|
-
"[",
|
|
38
|
-
"]",
|
|
39
|
-
"(",
|
|
40
|
-
")",
|
|
41
|
-
"~",
|
|
42
|
-
"`",
|
|
43
|
-
">",
|
|
44
|
-
"#",
|
|
45
|
-
"+",
|
|
46
|
-
"-",
|
|
47
|
-
"=",
|
|
48
|
-
"|",
|
|
49
|
-
"{",
|
|
50
|
-
"}",
|
|
51
|
-
".",
|
|
52
|
-
"!",
|
|
53
|
-
]);
|
|
54
|
-
function countUnescaped(text, target) {
|
|
55
|
-
let count = 0;
|
|
56
|
-
for (let i = 0; i < text.length; i += 1) {
|
|
57
|
-
const char = text[i];
|
|
58
|
-
if (char === "\\") {
|
|
59
|
-
i += 1;
|
|
60
|
-
continue;
|
|
61
|
-
}
|
|
62
|
-
if (char === target) {
|
|
63
|
-
count += 1;
|
|
64
|
-
}
|
|
65
|
-
}
|
|
66
|
-
return count;
|
|
67
|
-
}
|
|
68
|
-
function isMarkdownSafe(text) {
|
|
69
|
-
const stars = countUnescaped(text, "*");
|
|
70
|
-
if (stars % 2 !== 0)
|
|
71
|
-
return false;
|
|
72
|
-
const underscores = countUnescaped(text, "_");
|
|
73
|
-
if (underscores % 2 !== 0)
|
|
74
|
-
return false;
|
|
75
|
-
const backticks = countUnescaped(text, "`");
|
|
76
|
-
if (backticks % 2 !== 0)
|
|
77
|
-
return false;
|
|
78
|
-
const left = countUnescaped(text, "[");
|
|
79
|
-
const right = countUnescaped(text, "]");
|
|
80
|
-
if (left !== right)
|
|
81
|
-
return false;
|
|
82
|
-
return true;
|
|
83
|
-
}
|
|
84
|
-
function isMarkdownV2Safe(text) {
|
|
85
|
-
for (let i = 0; i < text.length; i += 1) {
|
|
86
|
-
const char = text[i];
|
|
87
|
-
if (char === "\\") {
|
|
88
|
-
i += 1;
|
|
89
|
-
continue;
|
|
90
|
-
}
|
|
91
|
-
if (MARKDOWN_V2_RESERVED.has(char)) {
|
|
92
|
-
return false;
|
|
93
|
-
}
|
|
94
|
-
}
|
|
95
|
-
return true;
|
|
96
|
-
}
|
|
97
|
-
function resolveBestParseModes(text) {
|
|
98
|
-
const configured = resolveTelegramParseMode(config.telegramParseMode);
|
|
99
|
-
if (configured) {
|
|
100
|
-
return [configured];
|
|
101
|
-
}
|
|
102
|
-
if (isMarkdownV2Safe(text)) {
|
|
103
|
-
return ["MarkdownV2", "Markdown"];
|
|
104
|
-
}
|
|
105
|
-
if (isMarkdownSafe(text)) {
|
|
106
|
-
return ["Markdown"];
|
|
107
|
-
}
|
|
108
|
-
return [];
|
|
109
|
-
}
|
|
110
|
-
const AUTH_PROMPT_TIMEOUT_MS = 10 * 60 * 1000;
|
|
111
|
-
const authPromptState = new Map();
|
|
112
|
-
const authInFlight = new Set();
|
|
113
|
-
const PROVIDER_ALIASES = new Map([
|
|
114
|
-
["codex", "openai-codex"],
|
|
115
|
-
["antigravity", "google-antigravity"],
|
|
116
|
-
["gemini", "google-gemini-cli"],
|
|
117
|
-
["gemini-cli", "google-gemini-cli"],
|
|
118
|
-
]);
|
|
119
|
-
const CODEX_MODEL_ALIASES = {
|
|
120
|
-
"codex-mini-latest": "gpt-5.1-codex-mini",
|
|
121
|
-
"codex-max-latest": "gpt-5.1-codex-max",
|
|
122
|
-
"codex-latest": "gpt-5.2-codex",
|
|
123
|
-
};
|
|
124
|
-
function normalizeProviderId(input) {
|
|
125
|
-
const trimmed = input.trim().toLowerCase();
|
|
126
|
-
return PROVIDER_ALIASES.get(trimmed) ?? trimmed;
|
|
127
|
-
}
|
|
128
|
-
function normalizeModelId(provider, modelId) {
|
|
129
|
-
if (provider !== "openai-codex") {
|
|
130
|
-
return modelId;
|
|
131
|
-
}
|
|
132
|
-
return CODEX_MODEL_ALIASES[modelId] ?? modelId;
|
|
133
|
-
}
|
|
134
|
-
async function loadModelRegistry() {
|
|
135
|
-
await ensureDir(config.agentDir);
|
|
136
|
-
const authStorage = discoverAuthStorage(config.agentDir);
|
|
137
|
-
const codex = readCodexOAuth();
|
|
138
|
-
if (codex) {
|
|
139
|
-
authStorage.setRuntimeApiKey("openai-codex", codex.accessToken);
|
|
140
|
-
}
|
|
141
|
-
const modelRegistry = discoverModels(authStorage, config.agentDir);
|
|
142
|
-
return { authStorage, modelRegistry };
|
|
143
|
-
}
|
|
144
|
-
function clearAuthPrompt(userId) {
|
|
145
|
-
const pending = authPromptState.get(userId);
|
|
146
|
-
if (!pending)
|
|
147
|
-
return;
|
|
148
|
-
clearTimeout(pending.timeoutId);
|
|
149
|
-
authPromptState.delete(userId);
|
|
150
|
-
}
|
|
151
|
-
function cancelAuthPrompt(userId, reason) {
|
|
152
|
-
const pending = authPromptState.get(userId);
|
|
153
|
-
if (!pending)
|
|
154
|
-
return;
|
|
155
|
-
clearTimeout(pending.timeoutId);
|
|
156
|
-
authPromptState.delete(userId);
|
|
157
|
-
pending.reject(new Error(reason));
|
|
158
|
-
}
|
|
159
|
-
async function promptForAuthInput(userId, chatId, prompt) {
|
|
160
|
-
if (authPromptState.has(userId)) {
|
|
161
|
-
throw new Error("Login is already awaiting input.");
|
|
162
|
-
}
|
|
163
|
-
const text = prompt.placeholder ? `${prompt.message} (${prompt.placeholder})` : prompt.message;
|
|
164
|
-
await sendLongMessage(chatId, text);
|
|
165
|
-
return await new Promise((resolve, reject) => {
|
|
166
|
-
const timeoutId = setTimeout(() => {
|
|
167
|
-
authPromptState.delete(userId);
|
|
168
|
-
reject(new Error("Login prompt timed out."));
|
|
169
|
-
}, AUTH_PROMPT_TIMEOUT_MS);
|
|
170
|
-
authPromptState.set(userId, { resolve, reject, timeoutId, chatId });
|
|
171
|
-
});
|
|
172
|
-
}
|
|
173
|
-
async function handleStopCommand(chatId, userId) {
|
|
174
|
-
const active = activeRuns.get(userId);
|
|
175
|
-
if (!active) {
|
|
176
|
-
await sendLongMessage(chatId, "No active request to stop.");
|
|
177
|
-
return;
|
|
178
|
-
}
|
|
179
|
-
active.cancelRequested = true;
|
|
180
|
-
active.abortRequested = true;
|
|
181
|
-
if (active.status) {
|
|
182
|
-
active.status.update("Cancelled by user.", true);
|
|
183
|
-
}
|
|
184
|
-
if (active.abort) {
|
|
185
|
-
active.abort();
|
|
186
|
-
}
|
|
187
|
-
if (!active.status || active.chatId !== chatId) {
|
|
188
|
-
await sendLongMessage(chatId, "Stopping current request...");
|
|
189
|
-
}
|
|
190
|
-
}
|
|
191
|
-
function logStartupInfo() {
|
|
192
|
-
const proxyVars = {
|
|
193
|
-
https_proxy: process.env.https_proxy ?? process.env.HTTPS_PROXY ?? "",
|
|
194
|
-
http_proxy: process.env.http_proxy ?? process.env.HTTP_PROXY ?? "",
|
|
195
|
-
all_proxy: process.env.all_proxy ?? process.env.ALL_PROXY ?? "",
|
|
196
|
-
no_proxy: process.env.no_proxy ?? process.env.NO_PROXY ?? "",
|
|
197
|
-
};
|
|
198
|
-
const tlsVars = {
|
|
199
|
-
NODE_EXTRA_CA_CERTS: process.env.NODE_EXTRA_CA_CERTS ?? "",
|
|
200
|
-
NODE_TLS_REJECT_UNAUTHORIZED: process.env.NODE_TLS_REJECT_UNAUTHORIZED ?? "",
|
|
201
|
-
};
|
|
202
|
-
console.log(`[tg-agent] modelProvider=${config.modelProvider} modelRef=${config.modelRef} sessionDir=${config.sessionDir} maxConcurrent=${config.maxConcurrent}`);
|
|
203
|
-
console.log(`[tg-agent] agentDir=${config.agentDir} workspaceDir=${config.workspaceDir}`);
|
|
204
|
-
const mcpServers = loadMcpServersSync(config.agentDir);
|
|
205
|
-
const mcpStatus = mcpServers.length > 0 ? `on (${mcpServers.length})` : "off";
|
|
206
|
-
console.log(`[tg-agent] tools fetchMaxBytes=${config.fetchMaxBytes} fetchTimeoutMs=${config.fetchTimeoutMs} mcp=${mcpStatus}`);
|
|
207
|
-
console.log(`[tg-agent] proxy https=${proxyVars.https_proxy || "(empty)"} http=${proxyVars.http_proxy || "(empty)"} all=${proxyVars.all_proxy || "(empty)"} no=${proxyVars.no_proxy || "(empty)"}`);
|
|
208
|
-
console.log(`[tg-agent] tls extra_ca=${tlsVars.NODE_EXTRA_CA_CERTS || "(empty)"} reject_unauthorized=${tlsVars.NODE_TLS_REJECT_UNAUTHORIZED || "(empty)"}`);
|
|
209
|
-
try {
|
|
210
|
-
const { source } = resolveApiKeyForProvider(config.modelProvider);
|
|
211
|
-
console.log(`[tg-agent] authSource=${source}`);
|
|
212
|
-
}
|
|
213
|
-
catch (error) {
|
|
214
|
-
const message = error instanceof Error ? error.message : String(error);
|
|
215
|
-
console.warn(`[tg-agent] authSource=missing (${message})`);
|
|
216
|
-
}
|
|
217
|
-
const proxyInfo = resolveProxyInfo();
|
|
218
|
-
if (proxyInfo) {
|
|
219
|
-
console.log(`[tg-agent] proxyUrl=${proxyInfo.url} kind=${proxyInfo.kind} source=${proxyInfo.source}`);
|
|
220
|
-
}
|
|
221
|
-
else {
|
|
222
|
-
console.log("[tg-agent] proxyUrl=(none)");
|
|
223
|
-
}
|
|
224
|
-
}
|
|
225
|
-
logStartupInfo();
|
|
226
|
-
void ensureDir(config.agentDir).catch((error) => {
|
|
227
|
-
console.warn(`[tg-agent] ensure agentDir failed: ${error instanceof Error ? error.message : String(error)}`);
|
|
228
|
-
});
|
|
229
|
-
const appliedProxy = applyFetchProxy();
|
|
230
|
-
if (appliedProxy) {
|
|
231
|
-
console.log(`[tg-agent] fetchProxy=${appliedProxy.url} kind=${appliedProxy.kind} source=${appliedProxy.source}`);
|
|
232
|
-
}
|
|
233
|
-
else {
|
|
234
|
-
console.log("[tg-agent] fetchProxy=(none)");
|
|
235
|
-
}
|
|
236
|
-
function parseCommand(text) {
|
|
237
|
-
if (!text.startsWith("/")) {
|
|
238
|
-
return null;
|
|
239
|
-
}
|
|
240
|
-
const trimmed = text.trim();
|
|
241
|
-
const [rawCommand, ...rest] = trimmed.split(" ");
|
|
242
|
-
const command = rawCommand.split("@")[0].slice(1).toLowerCase();
|
|
243
|
-
return { command, args: rest.join(" ").trim() };
|
|
244
|
-
}
|
|
245
|
-
async function ensureActiveSessionForCommand(state, chatId) {
|
|
246
|
-
let session = getActiveSession(state);
|
|
247
|
-
if (!session) {
|
|
248
|
-
session = createSession(state, "");
|
|
249
|
-
await saveUserState(state);
|
|
250
|
-
await sendLongMessage(chatId, `Created session ${session.id}.`);
|
|
251
|
-
}
|
|
252
|
-
return session;
|
|
253
|
-
}
|
|
254
|
-
function formatProviders(modelRegistry, authStorage) {
|
|
255
|
-
const models = modelRegistry.getAll();
|
|
256
|
-
const providerStats = new Map();
|
|
257
|
-
for (const model of models) {
|
|
258
|
-
const entry = providerStats.get(model.provider) ?? { count: 0, auth: authStorage.hasAuth(model.provider) };
|
|
259
|
-
entry.count += 1;
|
|
260
|
-
providerStats.set(model.provider, entry);
|
|
261
|
-
}
|
|
262
|
-
if (providerStats.size === 0) {
|
|
263
|
-
return "No providers found.";
|
|
264
|
-
}
|
|
265
|
-
const lines = Array.from(providerStats.entries())
|
|
266
|
-
.sort((a, b) => a[0].localeCompare(b[0]))
|
|
267
|
-
.map(([provider, info]) => `- ${provider} (models: ${info.count}, auth: ${info.auth ? "ok" : "missing"})`);
|
|
268
|
-
return ["Providers:", ...lines, "", "Use /models <provider> to list models."].join("\n");
|
|
269
|
-
}
|
|
270
|
-
function formatModelsForProvider(modelRegistry, provider, authOk) {
|
|
271
|
-
const models = modelRegistry.getAll().filter((model) => model.provider === provider);
|
|
272
|
-
if (models.length === 0) {
|
|
273
|
-
return `No models found for provider ${provider}.`;
|
|
274
|
-
}
|
|
275
|
-
const lines = models.map((model) => `- ${model.id} | ${model.name}`);
|
|
276
|
-
return [`Models for ${provider} (auth: ${authOk ? "ok" : "missing"}):`, ...lines].join("\n");
|
|
277
|
-
}
|
|
278
|
-
function parseModelSelector(raw) {
|
|
279
|
-
const trimmed = raw.trim();
|
|
280
|
-
if (!trimmed)
|
|
281
|
-
return null;
|
|
282
|
-
if (trimmed.includes("/")) {
|
|
283
|
-
const [provider, modelId] = trimmed.split("/", 2);
|
|
284
|
-
if (!provider || !modelId)
|
|
285
|
-
return null;
|
|
286
|
-
return { provider: normalizeProviderId(provider), modelId: modelId.trim() };
|
|
287
|
-
}
|
|
288
|
-
return { modelId: trimmed };
|
|
289
|
-
}
|
|
290
|
-
const unauthorizedLogWindowMs = 60_000;
|
|
291
|
-
const unauthorizedLogState = new Map();
|
|
292
|
-
function logUnauthorizedUser(userId, chatId) {
|
|
293
|
-
const now = Date.now();
|
|
294
|
-
const lastLoggedAt = unauthorizedLogState.get(userId) ?? 0;
|
|
295
|
-
if (now - lastLoggedAt < unauthorizedLogWindowMs) {
|
|
296
|
-
return;
|
|
297
|
-
}
|
|
298
|
-
unauthorizedLogState.set(userId, now);
|
|
299
|
-
console.warn(`[tg-agent] unauthorized user=${userId} chat=${chatId}`);
|
|
300
|
-
}
|
|
301
|
-
function isUserAllowed(userId) {
|
|
302
|
-
const allowed = config.telegramAllowedUsers;
|
|
303
|
-
if (!allowed || allowed.size === 0) {
|
|
304
|
-
return true;
|
|
305
|
-
}
|
|
306
|
-
return allowed.has(userId);
|
|
307
|
-
}
|
|
308
|
-
function formatSessions(stateSessions) {
|
|
309
|
-
if (stateSessions.length === 0) {
|
|
310
|
-
return "No sessions found.";
|
|
311
|
-
}
|
|
312
|
-
const lines = stateSessions.map((session) => {
|
|
313
|
-
const updated = new Date(session.updatedAt).toISOString();
|
|
314
|
-
return `- ${session.id} | ${session.title} | updated ${updated}`;
|
|
315
|
-
});
|
|
316
|
-
return ["Sessions:", ...lines].join("\n");
|
|
317
|
-
}
|
|
318
|
-
async function sendTelegramMessage(chatId, text, parseMode) {
|
|
319
|
-
if (parseMode) {
|
|
320
|
-
return await bot.sendMessage(chatId, text, { parse_mode: parseMode });
|
|
321
|
-
}
|
|
322
|
-
return await bot.sendMessage(chatId, text);
|
|
323
|
-
}
|
|
324
|
-
async function editTelegramMessage(chatId, messageId, text, parseMode) {
|
|
325
|
-
if (parseMode) {
|
|
326
|
-
await bot.editMessageText(text, { chat_id: chatId, message_id: messageId, parse_mode: parseMode });
|
|
327
|
-
return;
|
|
328
|
-
}
|
|
329
|
-
await bot.editMessageText(text, { chat_id: chatId, message_id: messageId });
|
|
330
|
-
}
|
|
331
|
-
async function sendWithParseModes(attempt, parseModes) {
|
|
332
|
-
let lastError;
|
|
333
|
-
for (const mode of parseModes) {
|
|
334
|
-
try {
|
|
335
|
-
return await attempt(mode);
|
|
336
|
-
}
|
|
337
|
-
catch (error) {
|
|
338
|
-
lastError = error;
|
|
339
|
-
const message = error instanceof Error ? error.message : String(error);
|
|
340
|
-
console.warn(`[tg-agent] parse_mode ${mode} failed, trying fallback: ${message}`);
|
|
341
|
-
}
|
|
342
|
-
}
|
|
343
|
-
try {
|
|
344
|
-
return await attempt(undefined);
|
|
345
|
-
}
|
|
346
|
-
catch (error) {
|
|
347
|
-
if (lastError) {
|
|
348
|
-
const message = error instanceof Error ? error.message : String(error);
|
|
349
|
-
console.warn(`[tg-agent] parse_mode fallback to plain failed: ${message}`);
|
|
350
|
-
}
|
|
351
|
-
throw error;
|
|
352
|
-
}
|
|
353
|
-
}
|
|
354
|
-
async function sendTelegramMessageBest(chatId, text) {
|
|
355
|
-
return await sendWithParseModes((parseMode) => sendTelegramMessage(chatId, text, parseMode), resolveBestParseModes(text));
|
|
356
|
-
}
|
|
357
|
-
async function editTelegramMessageBest(chatId, messageId, text) {
|
|
358
|
-
await sendWithParseModes((parseMode) => editTelegramMessage(chatId, messageId, text, parseMode), resolveBestParseModes(text));
|
|
359
|
-
}
|
|
360
|
-
async function sendLongMessage(chatId, text) {
|
|
361
|
-
const chunks = chunkText(text, 3900);
|
|
362
|
-
for (const chunk of chunks) {
|
|
363
|
-
await sendTelegramMessageBest(chatId, chunk);
|
|
364
|
-
}
|
|
365
|
-
}
|
|
366
|
-
const TELEGRAM_MAX_LEN = 3900;
|
|
367
|
-
const STATUS_UPDATE_MIN_MS = 1200;
|
|
368
|
-
function truncateForTelegram(text, maxLen = TELEGRAM_MAX_LEN) {
|
|
369
|
-
if (text.length <= maxLen) {
|
|
370
|
-
return { text, truncated: false };
|
|
371
|
-
}
|
|
372
|
-
const slice = text.slice(0, Math.max(0, maxLen - 15));
|
|
373
|
-
return { text: `${slice}...\n\n[truncated]`, truncated: true };
|
|
374
|
-
}
|
|
375
|
-
async function createStatusReporter(chatId) {
|
|
376
|
-
const initial = "Working...";
|
|
377
|
-
const sent = await sendTelegramMessage(chatId, initial);
|
|
378
|
-
const messageId = sent.message_id;
|
|
379
|
-
let lastText = initial;
|
|
380
|
-
let lastSentAt = 0;
|
|
381
|
-
let chain = Promise.resolve();
|
|
382
|
-
const enqueueEdit = (text, force = false) => {
|
|
383
|
-
const now = Date.now();
|
|
384
|
-
if (!force && now - lastSentAt < STATUS_UPDATE_MIN_MS)
|
|
385
|
-
return;
|
|
386
|
-
if (text === lastText)
|
|
387
|
-
return;
|
|
388
|
-
lastText = text;
|
|
389
|
-
lastSentAt = now;
|
|
390
|
-
chain = chain
|
|
391
|
-
.then(async () => {
|
|
392
|
-
await editTelegramMessage(chatId, messageId, text);
|
|
393
|
-
})
|
|
394
|
-
.catch((error) => {
|
|
395
|
-
console.warn("[tg-agent] status edit failed", error);
|
|
396
|
-
});
|
|
397
|
-
};
|
|
398
|
-
const enqueueEditBest = (text) => {
|
|
399
|
-
lastText = text;
|
|
400
|
-
lastSentAt = Date.now();
|
|
401
|
-
chain = chain
|
|
402
|
-
.then(async () => {
|
|
403
|
-
await editTelegramMessageBest(chatId, messageId, text);
|
|
404
|
-
})
|
|
405
|
-
.catch((error) => {
|
|
406
|
-
console.warn("[tg-agent] status edit failed", error);
|
|
407
|
-
});
|
|
408
|
-
};
|
|
409
|
-
return {
|
|
410
|
-
update: (text, force = false) => {
|
|
411
|
-
const trimmed = truncateForTelegram(text).text;
|
|
412
|
-
enqueueEdit(trimmed, force);
|
|
413
|
-
},
|
|
414
|
-
finalize: async (text) => {
|
|
415
|
-
const trimmed = truncateForTelegram(text).text;
|
|
416
|
-
enqueueEditBest(trimmed);
|
|
417
|
-
await chain;
|
|
418
|
-
},
|
|
419
|
-
fail: async (text) => {
|
|
420
|
-
const trimmed = truncateForTelegram(text).text;
|
|
421
|
-
enqueueEditBest(trimmed);
|
|
422
|
-
await chain;
|
|
423
|
-
},
|
|
424
|
-
};
|
|
425
|
-
}
|
|
426
|
-
function formatStatusEvent(event) {
|
|
427
|
-
switch (event.type) {
|
|
428
|
-
case "agent_start":
|
|
429
|
-
return "Working...";
|
|
430
|
-
case "tool_start":
|
|
431
|
-
return `Running tool: ${event.name}\nargs: ${summarizeArgs(event.args)}`;
|
|
432
|
-
case "tool_end":
|
|
433
|
-
return event.ok
|
|
434
|
-
? `Tool finished: ${event.name} (${event.durationMs}ms)`
|
|
435
|
-
: `Tool failed: ${event.name} (${event.durationMs}ms)`;
|
|
436
|
-
case "message_start":
|
|
437
|
-
return event.role === "assistant" ? "Generating reply..." : null;
|
|
438
|
-
case "message_end":
|
|
439
|
-
return event.role === "assistant" ? "Finalizing reply..." : null;
|
|
440
|
-
case "heartbeat": {
|
|
441
|
-
const seconds = Math.max(1, Math.round(event.elapsedMs / 1000));
|
|
442
|
-
return `Working... ${seconds}s`;
|
|
443
|
-
}
|
|
444
|
-
default:
|
|
445
|
-
return null;
|
|
446
|
-
}
|
|
447
|
-
}
|
|
448
|
-
function summarizeArgs(args) {
|
|
449
|
-
try {
|
|
450
|
-
const seen = new WeakSet();
|
|
451
|
-
const maxString = 200;
|
|
452
|
-
const maxKeys = 12;
|
|
453
|
-
const maxArray = 12;
|
|
454
|
-
const maxDepth = 3;
|
|
455
|
-
const normalize = (value, depth) => {
|
|
456
|
-
if (value === null || value === undefined)
|
|
457
|
-
return value;
|
|
458
|
-
if (typeof value === "string") {
|
|
459
|
-
return value.length > maxString ? `${value.slice(0, maxString)}...` : value;
|
|
460
|
-
}
|
|
461
|
-
if (typeof value === "number" || typeof value === "boolean")
|
|
462
|
-
return value;
|
|
463
|
-
if (typeof value === "bigint")
|
|
464
|
-
return value.toString();
|
|
465
|
-
if (typeof value === "function")
|
|
466
|
-
return "[function]";
|
|
467
|
-
if (typeof value !== "object")
|
|
468
|
-
return String(value);
|
|
469
|
-
if (seen.has(value))
|
|
470
|
-
return "[circular]";
|
|
471
|
-
if (depth >= maxDepth)
|
|
472
|
-
return "[truncated]";
|
|
473
|
-
seen.add(value);
|
|
474
|
-
if (Array.isArray(value)) {
|
|
475
|
-
const items = value.slice(0, maxArray).map((item) => normalize(item, depth + 1));
|
|
476
|
-
if (value.length > maxArray)
|
|
477
|
-
items.push("[truncated]");
|
|
478
|
-
return items;
|
|
479
|
-
}
|
|
480
|
-
const entries = Object.entries(value);
|
|
481
|
-
const result = {};
|
|
482
|
-
for (const [key, val] of entries.slice(0, maxKeys)) {
|
|
483
|
-
result[key] = normalize(val, depth + 1);
|
|
484
|
-
}
|
|
485
|
-
if (entries.length > maxKeys) {
|
|
486
|
-
result._truncated = true;
|
|
487
|
-
}
|
|
488
|
-
return result;
|
|
489
|
-
};
|
|
490
|
-
const normalized = normalize(args, 0);
|
|
491
|
-
const json = JSON.stringify(normalized);
|
|
492
|
-
if (!json)
|
|
493
|
-
return "null";
|
|
494
|
-
if (json.length > 700) {
|
|
495
|
-
return `${json.slice(0, 700)}...`;
|
|
496
|
-
}
|
|
497
|
-
return json;
|
|
498
|
-
}
|
|
499
|
-
catch (error) {
|
|
500
|
-
return "[unavailable]";
|
|
501
|
-
}
|
|
502
|
-
}
|
|
503
|
-
function formatErrorMessage(error) {
|
|
504
|
-
const base = error instanceof Error ? error.message : String(error);
|
|
505
|
-
const cause = error?.cause;
|
|
506
|
-
if (cause instanceof Error) {
|
|
507
|
-
return `${base} (${cause.message})`;
|
|
508
|
-
}
|
|
509
|
-
if (cause) {
|
|
510
|
-
return `${base} (${String(cause)})`;
|
|
511
|
-
}
|
|
512
|
-
return base;
|
|
513
|
-
}
|
|
514
|
-
function truncateLine(text, maxLen = 140) {
|
|
515
|
-
if (text.length <= maxLen) {
|
|
516
|
-
return text;
|
|
517
|
-
}
|
|
518
|
-
return `${text.slice(0, maxLen - 3)}...`;
|
|
519
|
-
}
|
|
520
|
-
async function formatMcpStatus() {
|
|
521
|
-
const servers = await loadMcpServers(config.agentDir);
|
|
522
|
-
if (servers.length === 0) {
|
|
523
|
-
return "No MCP servers configured. Add [mcp_servers.*] to ~/.tg-agent/config.toml.";
|
|
524
|
-
}
|
|
525
|
-
const probes = await Promise.all(servers.map((server) => probeMcpServer(server)));
|
|
526
|
-
const lines = ["MCP servers:"];
|
|
527
|
-
for (let i = 0; i < servers.length; i += 1) {
|
|
528
|
-
const server = servers[i];
|
|
529
|
-
const probe = probes[i];
|
|
530
|
-
const status = probe.ok ? "ok" : `error: ${truncateLine(probe.error ?? "unknown")}`;
|
|
531
|
-
lines.push(`- ${server.name} (${server.type}) ${formatMcpTarget(server)} status=${status} (${probe.durationMs}ms)`);
|
|
532
|
-
}
|
|
533
|
-
return lines.join("\n");
|
|
534
|
-
}
|
|
535
|
-
async function handleCommand(chatId, userId, command, args) {
|
|
536
|
-
const state = await loadUserState(userId);
|
|
537
|
-
const removed = pruneExpiredSessions(state);
|
|
538
|
-
if (removed.length > 0) {
|
|
539
|
-
await saveUserState(state);
|
|
540
|
-
await Promise.all(removed.map((sessionId) => deleteSessionFile(userId, sessionId).catch((error) => {
|
|
541
|
-
console.warn(`[tg-agent] cleanup session file failed id=${sessionId}`, error);
|
|
542
|
-
})));
|
|
543
|
-
}
|
|
544
|
-
switch (command) {
|
|
545
|
-
case "start":
|
|
546
|
-
case "help": {
|
|
547
|
-
const helpText = [
|
|
548
|
-
"Commands:",
|
|
549
|
-
"/new [title] - create a new session",
|
|
550
|
-
"/list - list sessions",
|
|
551
|
-
"/use <id> - switch active session",
|
|
552
|
-
"/close [id] - close a session (default: active)",
|
|
553
|
-
"/reset - clear active session history",
|
|
554
|
-
"/providers - list available providers",
|
|
555
|
-
"/models [provider] - list models for provider",
|
|
556
|
-
"/provider <name> - set provider for current session",
|
|
557
|
-
"/model <provider>/<model> - set model for current session",
|
|
558
|
-
"/mcp - list configured MCP servers",
|
|
559
|
-
"/status - show session model settings",
|
|
560
|
-
"/login <provider> - login to OAuth provider",
|
|
561
|
-
"/logout <provider> - logout from provider",
|
|
562
|
-
"/stop - stop the current running request",
|
|
563
|
-
].join("\n");
|
|
564
|
-
await sendLongMessage(chatId, helpText);
|
|
565
|
-
return;
|
|
566
|
-
}
|
|
567
|
-
case "new": {
|
|
568
|
-
try {
|
|
569
|
-
const title = args || "";
|
|
570
|
-
const session = createSession(state, title);
|
|
571
|
-
await saveUserState(state);
|
|
572
|
-
await sendLongMessage(chatId, `Created session ${session.id} (${session.title}).`);
|
|
573
|
-
}
|
|
574
|
-
catch (error) {
|
|
575
|
-
await sendLongMessage(chatId, error.message);
|
|
576
|
-
}
|
|
577
|
-
return;
|
|
578
|
-
}
|
|
579
|
-
case "list": {
|
|
580
|
-
await saveUserState(state);
|
|
581
|
-
await sendLongMessage(chatId, formatSessions(listSessions(state)));
|
|
582
|
-
return;
|
|
583
|
-
}
|
|
584
|
-
case "use": {
|
|
585
|
-
if (!args) {
|
|
586
|
-
await sendLongMessage(chatId, "Usage: /use <id>");
|
|
587
|
-
return;
|
|
588
|
-
}
|
|
589
|
-
const ok = setActiveSession(state, args);
|
|
590
|
-
if (!ok) {
|
|
591
|
-
await sendLongMessage(chatId, `Session not found: ${args}`);
|
|
592
|
-
return;
|
|
593
|
-
}
|
|
594
|
-
await saveUserState(state);
|
|
595
|
-
await sendLongMessage(chatId, `Active session set to ${args}.`);
|
|
596
|
-
return;
|
|
597
|
-
}
|
|
598
|
-
case "close": {
|
|
599
|
-
const target = args || state.activeSessionId;
|
|
600
|
-
if (!target) {
|
|
601
|
-
await sendLongMessage(chatId, "No active session to close.");
|
|
602
|
-
return;
|
|
603
|
-
}
|
|
604
|
-
const ok = closeSession(state, target);
|
|
605
|
-
if (!ok) {
|
|
606
|
-
await sendLongMessage(chatId, `Session not found: ${target}`);
|
|
607
|
-
return;
|
|
608
|
-
}
|
|
609
|
-
await saveUserState(state);
|
|
610
|
-
await deleteSessionFile(userId, target).catch((error) => {
|
|
611
|
-
console.warn(`[tg-agent] delete session file failed id=${target}`, error);
|
|
612
|
-
});
|
|
613
|
-
await sendLongMessage(chatId, `Closed session ${target}.`);
|
|
614
|
-
return;
|
|
615
|
-
}
|
|
616
|
-
case "reset": {
|
|
617
|
-
const active = getActiveSession(state);
|
|
618
|
-
if (!active) {
|
|
619
|
-
await sendLongMessage(chatId, "No active session.");
|
|
620
|
-
return;
|
|
621
|
-
}
|
|
622
|
-
resetSession(active);
|
|
623
|
-
await saveUserState(state);
|
|
624
|
-
await deleteSessionFile(userId, active.id).catch((error) => {
|
|
625
|
-
console.warn(`[tg-agent] reset session file failed id=${active.id}`, error);
|
|
626
|
-
});
|
|
627
|
-
await sendLongMessage(chatId, `Reset session ${active.id}.`);
|
|
628
|
-
return;
|
|
629
|
-
}
|
|
630
|
-
case "providers": {
|
|
631
|
-
const { authStorage, modelRegistry } = await loadModelRegistry();
|
|
632
|
-
const error = modelRegistry.getError();
|
|
633
|
-
const text = formatProviders(modelRegistry, authStorage);
|
|
634
|
-
const message = error ? `Warning: ${error}\n\n${text}` : text;
|
|
635
|
-
await sendLongMessage(chatId, message);
|
|
636
|
-
return;
|
|
637
|
-
}
|
|
638
|
-
case "models": {
|
|
639
|
-
const { authStorage, modelRegistry } = await loadModelRegistry();
|
|
640
|
-
let provider = args ? normalizeProviderId(args) : "";
|
|
641
|
-
if (!provider) {
|
|
642
|
-
const active = getActiveSession(state);
|
|
643
|
-
if (!active?.modelProvider) {
|
|
644
|
-
await sendLongMessage(chatId, "Usage: /models <provider>");
|
|
645
|
-
return;
|
|
646
|
-
}
|
|
647
|
-
provider = active.modelProvider;
|
|
648
|
-
}
|
|
649
|
-
const authOk = authStorage.hasAuth(provider);
|
|
650
|
-
await sendLongMessage(chatId, formatModelsForProvider(modelRegistry, provider, authOk));
|
|
651
|
-
return;
|
|
652
|
-
}
|
|
653
|
-
case "provider": {
|
|
654
|
-
if (!args) {
|
|
655
|
-
await sendLongMessage(chatId, "Usage: /provider <name>");
|
|
656
|
-
return;
|
|
657
|
-
}
|
|
658
|
-
const provider = normalizeProviderId(args);
|
|
659
|
-
const { modelRegistry } = await loadModelRegistry();
|
|
660
|
-
const exists = modelRegistry.getAll().some((model) => model.provider === provider);
|
|
661
|
-
if (!exists) {
|
|
662
|
-
await sendLongMessage(chatId, `Unknown provider: ${provider}`);
|
|
663
|
-
return;
|
|
664
|
-
}
|
|
665
|
-
const session = await ensureActiveSessionForCommand(state, chatId);
|
|
666
|
-
session.modelProvider = provider;
|
|
667
|
-
session.modelId = undefined;
|
|
668
|
-
session.updatedAt = nowMs();
|
|
669
|
-
await saveUserState(state);
|
|
670
|
-
await sendLongMessage(chatId, `Session ${session.id} provider set to ${provider}.`);
|
|
671
|
-
return;
|
|
672
|
-
}
|
|
673
|
-
case "model": {
|
|
674
|
-
const selector = parseModelSelector(args);
|
|
675
|
-
if (!selector) {
|
|
676
|
-
await sendLongMessage(chatId, "Usage: /model <provider>/<model>");
|
|
677
|
-
return;
|
|
678
|
-
}
|
|
679
|
-
const session = await ensureActiveSessionForCommand(state, chatId);
|
|
680
|
-
const provider = normalizeProviderId(selector.provider ?? session.modelProvider ?? "");
|
|
681
|
-
if (!provider) {
|
|
682
|
-
await sendLongMessage(chatId, "Usage: /model <provider>/<model>");
|
|
683
|
-
return;
|
|
684
|
-
}
|
|
685
|
-
const { modelRegistry } = await loadModelRegistry();
|
|
686
|
-
const normalizedModelId = normalizeModelId(provider, selector.modelId);
|
|
687
|
-
const model = modelRegistry.find(provider, normalizedModelId);
|
|
688
|
-
if (!model) {
|
|
689
|
-
await sendLongMessage(chatId, `Model not found: ${provider}/${normalizedModelId}`);
|
|
690
|
-
return;
|
|
691
|
-
}
|
|
692
|
-
session.modelProvider = provider;
|
|
693
|
-
session.modelId = normalizedModelId;
|
|
694
|
-
session.updatedAt = nowMs();
|
|
695
|
-
await saveUserState(state);
|
|
696
|
-
await sendLongMessage(chatId, `Session ${session.id} model set to ${provider}/${normalizedModelId}.`);
|
|
697
|
-
return;
|
|
698
|
-
}
|
|
699
|
-
case "status": {
|
|
700
|
-
const active = getActiveSession(state);
|
|
701
|
-
if (!active) {
|
|
702
|
-
await sendLongMessage(chatId, "No active session.");
|
|
703
|
-
return;
|
|
704
|
-
}
|
|
705
|
-
const defaultModel = config.modelRef.includes("/")
|
|
706
|
-
? config.modelRef
|
|
707
|
-
: `${config.modelProvider}/${config.modelRef}`;
|
|
708
|
-
const overrides = active.modelProvider || active.modelId
|
|
709
|
-
? `Session model: ${active.modelProvider ?? "?"}/${active.modelId ?? "?"}`
|
|
710
|
-
: `Default model: ${defaultModel}`;
|
|
711
|
-
const message = [
|
|
712
|
-
`Session: ${active.id} (${active.title})`,
|
|
713
|
-
overrides,
|
|
714
|
-
].join("\n");
|
|
715
|
-
await sendLongMessage(chatId, message);
|
|
716
|
-
return;
|
|
717
|
-
}
|
|
718
|
-
case "mcp": {
|
|
719
|
-
const message = await formatMcpStatus();
|
|
720
|
-
await sendLongMessage(chatId, message);
|
|
721
|
-
return;
|
|
722
|
-
}
|
|
723
|
-
case "login": {
|
|
724
|
-
const providers = getOAuthProviders();
|
|
725
|
-
if (!args) {
|
|
726
|
-
const lines = providers.map((p) => `- ${p.id} (${p.name})`);
|
|
727
|
-
await sendLongMessage(chatId, ["OAuth providers:", ...lines].join("\n"));
|
|
728
|
-
return;
|
|
729
|
-
}
|
|
730
|
-
const provider = normalizeProviderId(args);
|
|
731
|
-
if (authInFlight.has(userId)) {
|
|
732
|
-
await sendLongMessage(chatId, "Login already in progress.");
|
|
733
|
-
return;
|
|
734
|
-
}
|
|
735
|
-
const providerInfo = providers.find((p) => p.id === provider);
|
|
736
|
-
if (!providerInfo) {
|
|
737
|
-
await sendLongMessage(chatId, `Unknown OAuth provider: ${provider}`);
|
|
738
|
-
return;
|
|
739
|
-
}
|
|
740
|
-
authInFlight.add(userId);
|
|
741
|
-
try {
|
|
742
|
-
const { authStorage } = await loadModelRegistry();
|
|
743
|
-
await sendLongMessage(chatId, `Starting login for ${providerInfo.id}...`);
|
|
744
|
-
await authStorage.login(providerInfo.id, {
|
|
745
|
-
onAuth: ({ url, instructions }) => {
|
|
746
|
-
const lines = [
|
|
747
|
-
"Open this URL in your browser:",
|
|
748
|
-
url,
|
|
749
|
-
];
|
|
750
|
-
if (instructions) {
|
|
751
|
-
lines.push("", instructions);
|
|
752
|
-
}
|
|
753
|
-
void sendLongMessage(chatId, lines.join("\n"));
|
|
754
|
-
},
|
|
755
|
-
onPrompt: (prompt) => promptForAuthInput(userId, chatId, prompt),
|
|
756
|
-
onProgress: (message) => {
|
|
757
|
-
void sendLongMessage(chatId, message);
|
|
758
|
-
},
|
|
759
|
-
onManualCodeInput: () => promptForAuthInput(userId, chatId, { message: "Paste the authorization code:" }),
|
|
760
|
-
});
|
|
761
|
-
await sendLongMessage(chatId, `Login completed for ${providerInfo.id}.`);
|
|
762
|
-
}
|
|
763
|
-
catch (error) {
|
|
764
|
-
const message = formatErrorMessage(error);
|
|
765
|
-
await sendLongMessage(chatId, `Login failed: ${message}`);
|
|
766
|
-
}
|
|
767
|
-
finally {
|
|
768
|
-
authInFlight.delete(userId);
|
|
769
|
-
clearAuthPrompt(userId);
|
|
770
|
-
}
|
|
771
|
-
return;
|
|
772
|
-
}
|
|
773
|
-
case "logout": {
|
|
774
|
-
if (!args) {
|
|
775
|
-
await sendLongMessage(chatId, "Usage: /logout <provider>");
|
|
776
|
-
return;
|
|
777
|
-
}
|
|
778
|
-
const provider = normalizeProviderId(args);
|
|
779
|
-
const { authStorage } = await loadModelRegistry();
|
|
780
|
-
authStorage.logout(provider);
|
|
781
|
-
await sendLongMessage(chatId, `Logged out from ${provider}.`);
|
|
782
|
-
return;
|
|
783
|
-
}
|
|
784
|
-
default: {
|
|
785
|
-
await sendLongMessage(chatId, `Unknown command: ${command}`);
|
|
786
|
-
}
|
|
787
|
-
}
|
|
788
|
-
}
|
|
789
|
-
async function handleMessage(chatId, userId, text) {
|
|
790
|
-
const command = parseCommand(text);
|
|
791
|
-
if (command) {
|
|
792
|
-
await handleCommand(chatId, userId, command.command, command.args);
|
|
793
|
-
return;
|
|
794
|
-
}
|
|
795
|
-
const state = await loadUserState(userId);
|
|
796
|
-
const expired = pruneExpiredSessions(state);
|
|
797
|
-
if (expired.length > 0) {
|
|
798
|
-
await saveUserState(state);
|
|
799
|
-
await Promise.all(expired.map((sessionId) => deleteSessionFile(userId, sessionId).catch((error) => {
|
|
800
|
-
console.warn(`[tg-agent] cleanup session file failed id=${sessionId}`, error);
|
|
801
|
-
})));
|
|
802
|
-
}
|
|
803
|
-
let session = getActiveSession(state);
|
|
804
|
-
if (!session) {
|
|
805
|
-
try {
|
|
806
|
-
session = createSession(state, "");
|
|
807
|
-
await saveUserState(state);
|
|
808
|
-
await sendLongMessage(chatId, `Created session ${session.id}.`);
|
|
809
|
-
}
|
|
810
|
-
catch (error) {
|
|
811
|
-
await sendLongMessage(chatId, error.message);
|
|
812
|
-
return;
|
|
813
|
-
}
|
|
814
|
-
}
|
|
815
|
-
const userMessage = {
|
|
816
|
-
role: "user",
|
|
817
|
-
content: text,
|
|
818
|
-
ts: nowMs(),
|
|
819
|
-
};
|
|
820
|
-
appendMessage(session, userMessage, config.maxHistoryMessages);
|
|
821
|
-
await saveUserState(state);
|
|
822
|
-
console.log(`[tg-agent] request user=${userId} session=${session.id} messages=${session.messages.length} textLen=${text.length} provider=${session.modelProvider ?? "-"} model=${session.modelId ?? "-"}`);
|
|
823
|
-
let statusReporter = null;
|
|
824
|
-
try {
|
|
825
|
-
statusReporter = await createStatusReporter(chatId);
|
|
826
|
-
}
|
|
827
|
-
catch (error) {
|
|
828
|
-
console.warn("[tg-agent] status message failed", error);
|
|
829
|
-
}
|
|
830
|
-
const activeRun = {
|
|
831
|
-
sessionId: session.id,
|
|
832
|
-
chatId,
|
|
833
|
-
status: statusReporter,
|
|
834
|
-
abortRequested: false,
|
|
835
|
-
cancelRequested: false,
|
|
836
|
-
};
|
|
837
|
-
registerActiveRun(userId, activeRun);
|
|
838
|
-
try {
|
|
839
|
-
const assistantText = await withSemaphore(async () => {
|
|
840
|
-
const result = await runPiAgentPrompt({
|
|
841
|
-
userId,
|
|
842
|
-
sessionId: session.id,
|
|
843
|
-
prompt: text,
|
|
844
|
-
systemPrompt: config.systemPrompt,
|
|
845
|
-
modelProvider: session.modelProvider,
|
|
846
|
-
modelId: session.modelId,
|
|
847
|
-
onAbortReady: (abort) => {
|
|
848
|
-
activeRun.abort = abort;
|
|
849
|
-
if (activeRun.abortRequested) {
|
|
850
|
-
abort();
|
|
851
|
-
}
|
|
852
|
-
},
|
|
853
|
-
onStatus: statusReporter
|
|
854
|
-
? (event) => {
|
|
855
|
-
const statusText = formatStatusEvent(event);
|
|
856
|
-
if (statusText) {
|
|
857
|
-
statusReporter?.update(statusText);
|
|
858
|
-
}
|
|
859
|
-
}
|
|
860
|
-
: undefined,
|
|
861
|
-
});
|
|
862
|
-
console.log(`[tg-agent] response session=${session.id} model=${result.modelProvider}/${result.modelId} file=${result.sessionFile}`);
|
|
863
|
-
return result.text;
|
|
864
|
-
});
|
|
865
|
-
if (activeRun.cancelRequested) {
|
|
866
|
-
if (statusReporter) {
|
|
867
|
-
await statusReporter.finalize("Cancelled by user.");
|
|
868
|
-
}
|
|
869
|
-
else {
|
|
870
|
-
await sendLongMessage(chatId, "Cancelled by user.");
|
|
871
|
-
}
|
|
872
|
-
return;
|
|
873
|
-
}
|
|
874
|
-
const assistantMessage = {
|
|
875
|
-
role: "assistant",
|
|
876
|
-
content: assistantText,
|
|
877
|
-
ts: nowMs(),
|
|
878
|
-
};
|
|
879
|
-
appendMessage(session, assistantMessage, config.maxHistoryMessages);
|
|
880
|
-
await saveUserState(state);
|
|
881
|
-
if (statusReporter) {
|
|
882
|
-
await statusReporter.finalize(assistantText);
|
|
883
|
-
}
|
|
884
|
-
else {
|
|
885
|
-
await sendLongMessage(chatId, assistantText);
|
|
886
|
-
}
|
|
887
|
-
}
|
|
888
|
-
catch (error) {
|
|
889
|
-
console.error("[tg-agent] runModel error", error);
|
|
890
|
-
if (activeRun.cancelRequested) {
|
|
891
|
-
if (statusReporter) {
|
|
892
|
-
await statusReporter.fail("Cancelled by user.");
|
|
893
|
-
}
|
|
894
|
-
else {
|
|
895
|
-
await sendLongMessage(chatId, "Cancelled by user.");
|
|
896
|
-
}
|
|
897
|
-
return;
|
|
898
|
-
}
|
|
899
|
-
const message = formatErrorMessage(error);
|
|
900
|
-
if (statusReporter) {
|
|
901
|
-
await statusReporter.fail(`Error: ${message}`);
|
|
902
|
-
}
|
|
903
|
-
else {
|
|
904
|
-
await sendLongMessage(chatId, `Error: ${message}`);
|
|
905
|
-
}
|
|
906
|
-
}
|
|
907
|
-
finally {
|
|
908
|
-
clearActiveRun(userId);
|
|
909
|
-
}
|
|
910
|
-
}
|
|
911
|
-
bot.on("message", (msg) => {
|
|
912
|
-
if (!msg.text) {
|
|
913
|
-
return;
|
|
914
|
-
}
|
|
915
|
-
if (msg.chat.type !== "private") {
|
|
916
|
-
void bot.sendMessage(msg.chat.id, "This bot only supports direct messages.");
|
|
917
|
-
return;
|
|
918
|
-
}
|
|
919
|
-
if (!msg.from?.id) {
|
|
920
|
-
return;
|
|
921
|
-
}
|
|
922
|
-
const userId = String(msg.from.id);
|
|
923
|
-
const chatId = msg.chat.id;
|
|
924
|
-
if (!isUserAllowed(userId)) {
|
|
925
|
-
logUnauthorizedUser(userId, chatId);
|
|
926
|
-
return;
|
|
927
|
-
}
|
|
928
|
-
const text = msg.text.trim();
|
|
929
|
-
const pendingAuth = authPromptState.get(userId);
|
|
930
|
-
if (pendingAuth) {
|
|
931
|
-
if (text === "/stop" || text === "/cancel") {
|
|
932
|
-
cancelAuthPrompt(userId, "Login cancelled by user.");
|
|
933
|
-
void sendLongMessage(chatId, "Login cancelled.");
|
|
934
|
-
return;
|
|
935
|
-
}
|
|
936
|
-
clearAuthPrompt(userId);
|
|
937
|
-
pendingAuth.resolve(text);
|
|
938
|
-
return;
|
|
939
|
-
}
|
|
940
|
-
const command = parseCommand(text);
|
|
941
|
-
if (command?.command === "stop") {
|
|
942
|
-
void handleStopCommand(chatId, userId);
|
|
943
|
-
return;
|
|
944
|
-
}
|
|
945
|
-
enqueueUser(userId, () => handleMessage(chatId, userId, text)).catch(async (error) => {
|
|
946
|
-
console.error("[tg-agent] runModel error", error);
|
|
947
|
-
const message = formatErrorMessage(error);
|
|
948
|
-
await sendLongMessage(chatId, `Error: ${message}`);
|
|
949
|
-
});
|
|
950
|
-
});
|
|
951
|
-
bot.on("polling_error", (error) => {
|
|
952
|
-
console.error("Polling error", error);
|
|
953
|
-
});
|
|
954
|
-
console.log("tg-agent started");
|
|
1
|
+
import V from"node-telegram-bot-api";import{discoverAuthStorage as B,discoverModels as J}from"@mariozechner/pi-coding-agent";import{getOAuthProviders as K}from"@mariozechner/pi-ai";import{config as f,assertConfig as Y}from"./config.js";import{resolveApiKeyForProvider as G,resolveProxyInfo as Z}from"./auth.js";import{applyFetchProxy as Q}from"./proxy.js";import{runPiAgentPrompt as I}from"./piAgentRunner.js";import{readCodexOAuth as ee}from"./codexAuth.js";import{formatMcpTarget as te,loadMcpServers as oe,loadMcpServersSync as ne,probeMcpServer as re}from"./mcp.js";import{appendMessage as D,closeSession as se,createSession as R,deleteSessionFile as A,getActiveSession as M,listSessions as ie,loadUserState as b,pruneExpiredSessions as O,resetSession as ae,saveUserState as p,setActiveSession as ce}from"./sessionStore.js";import{chunkText as le,createQueueMap as ue,createSemaphore as de,ensureDir as U,nowMs as P}from"./utils.js";Y();const y=new V(f.telegramToken,{polling:!0}),fe=ue(),me=de(f.maxConcurrent),T=new Map;function ge(e,t){T.set(e,t)}function pe(e){T.delete(e)}function we(e){const t=e.trim();if(t){if(t==="Markdown"||t==="MarkdownV2"||t==="HTML")return t;console.warn(`[tg-agent] invalid TELEGRAM_PARSE_MODE=${t}, ignoring`)}}const $e=new Set(["_","*","[","]","(",")","~","`",">","#","+","-","=","|","{","}",".","!"]);function _(e,t){let o=0;for(let r=0;r<e.length;r+=1){const s=e[r];if(s==="\\"){r+=1;continue}s===t&&(o+=1)}return o}function ye(e){if(_(e,"*")%2!==0||_(e,"_")%2!==0||_(e,"`")%2!==0)return!1;const s=_(e,"["),u=_(e,"]");return s===u}function ve(e){for(let t=0;t<e.length;t+=1){const o=e[t];if(o==="\\"){t+=1;continue}if($e.has(o))return!1}return!0}function L(e){const t=we(f.telegramParseMode);return t?[t]:ve(e)?["MarkdownV2","Markdown"]:ye(e)?["Markdown"]:[]}const he=600*1e3,$=new Map,x=new Set,Se=new Map([["codex","openai-codex"],["antigravity","google-antigravity"],["gemini","google-gemini-cli"],["gemini-cli","google-gemini-cli"]]),Me={"codex-mini-latest":"gpt-5.1-codex-mini","codex-max-latest":"gpt-5.1-codex-max","codex-latest":"gpt-5.2-codex"};function h(e){const t=e.trim().toLowerCase();return Se.get(t)??t}function _e(e,t){return e!=="openai-codex"?t:Me[t]??t}async function S(){await U(f.agentDir);const e=B(f.agentDir),t=ee();t&&e.setRuntimeApiKey("openai-codex",t.accessToken);const o=J(e,f.agentDir);return{authStorage:e,modelRegistry:o}}function N(e){const t=$.get(e);t&&(clearTimeout(t.timeoutId),$.delete(e))}function Ae(e,t){const o=$.get(e);o&&(clearTimeout(o.timeoutId),$.delete(e),o.reject(new Error(t)))}async function z(e,t,o){if($.has(e))throw new Error("Login is already awaiting input.");const r=o.placeholder?`${o.message} (${o.placeholder})`:o.message;return await c(t,r),await new Promise((s,u)=>{const n=setTimeout(()=>{$.delete(e),u(new Error("Login prompt timed out."))},he);$.set(e,{resolve:s,reject:u,timeoutId:n,chatId:t})})}async function Pe(e,t){const o=T.get(t);if(!o){await c(e,"No active request to stop.");return}o.cancelRequested=!0,o.abortRequested=!0,o.status&&o.status.update("Cancelled by user.",!0),o.abort&&o.abort(),(!o.status||o.chatId!==e)&&await c(e,"Stopping current request...")}function Ee(){const e={https_proxy:process.env.https_proxy??process.env.HTTPS_PROXY??"",http_proxy:process.env.http_proxy??process.env.HTTP_PROXY??"",all_proxy:process.env.all_proxy??process.env.ALL_PROXY??"",no_proxy:process.env.no_proxy??process.env.NO_PROXY??""},t={NODE_EXTRA_CA_CERTS:process.env.NODE_EXTRA_CA_CERTS??"",NODE_TLS_REJECT_UNAUTHORIZED:process.env.NODE_TLS_REJECT_UNAUTHORIZED??""};console.log(`[tg-agent] modelProvider=${f.modelProvider} modelRef=${f.modelRef} sessionDir=${f.sessionDir} maxConcurrent=${f.maxConcurrent}`),console.log(`[tg-agent] agentDir=${f.agentDir} workspaceDir=${f.workspaceDir}`);const o=ne(f.agentDir),r=o.length>0?`on (${o.length})`:"off";console.log(`[tg-agent] tools fetchMaxBytes=${f.fetchMaxBytes} fetchTimeoutMs=${f.fetchTimeoutMs} mcp=${r}`),console.log(`[tg-agent] proxy https=${e.https_proxy||"(empty)"} http=${e.http_proxy||"(empty)"} all=${e.all_proxy||"(empty)"} no=${e.no_proxy||"(empty)"}`),console.log(`[tg-agent] tls extra_ca=${t.NODE_EXTRA_CA_CERTS||"(empty)"} reject_unauthorized=${t.NODE_TLS_REJECT_UNAUTHORIZED||"(empty)"}`);try{const{source:u}=G(f.modelProvider);console.log(`[tg-agent] authSource=${u}`)}catch(u){const n=u instanceof Error?u.message:String(u);console.warn(`[tg-agent] authSource=missing (${n})`)}const s=Z();console.log(s?`[tg-agent] proxyUrl=${s.url} kind=${s.kind} source=${s.source}`:"[tg-agent] proxyUrl=(none)")}Ee(),U(f.agentDir).catch(e=>{console.warn(`[tg-agent] ensure agentDir failed: ${e instanceof Error?e.message:String(e)}`)});const E=Q();console.log(E?`[tg-agent] fetchProxy=${E.url} kind=${E.kind} source=${E.source}`:"[tg-agent] fetchProxy=(none)");function q(e){if(!e.startsWith("/"))return null;const t=e.trim(),[o,...r]=t.split(" ");return{command:o.split("@")[0].slice(1).toLowerCase(),args:r.join(" ").trim()}}async function j(e,t){let o=M(e);return o||(o=R(e,""),await p(e),await c(t,`Created session ${o.id}.`)),o}function Re(e,t){const o=e.getAll(),r=new Map;for(const u of o){const n=r.get(u.provider)??{count:0,auth:t.hasAuth(u.provider)};n.count+=1,r.set(u.provider,n)}return r.size===0?"No providers found.":["Providers:",...Array.from(r.entries()).sort((u,n)=>u[0].localeCompare(n[0])).map(([u,n])=>`- ${u} (models: ${n.count}, auth: ${n.auth?"ok":"missing"})`),"","Use /models <provider> to list models."].join(`
|
|
2
|
+
`)}function Te(e,t,o){const r=e.getAll().filter(u=>u.provider===t);if(r.length===0)return`No models found for provider ${t}.`;const s=r.map(u=>`- ${u.id} | ${u.name}`);return[`Models for ${t} (auth: ${o?"ok":"missing"}):`,...s].join(`
|
|
3
|
+
`)}function xe(e){const t=e.trim();if(!t)return null;if(t.includes("/")){const[o,r]=t.split("/",2);return!o||!r?null:{provider:h(o),modelId:r.trim()}}return{modelId:t}}const ke=6e4,F=new Map;function Ce(e,t){const o=Date.now(),r=F.get(e)??0;o-r<ke||(F.set(e,o),console.warn(`[tg-agent] unauthorized user=${e} chat=${t}`))}function De(e){const t=f.telegramAllowedUsers;return!t||t.size===0?!0:t.has(e)}function be(e){return e.length===0?"No sessions found.":["Sessions:",...e.map(o=>{const r=new Date(o.updatedAt).toISOString();return`- ${o.id} | ${o.title} | updated ${r}`})].join(`
|
|
4
|
+
`)}async function H(e,t,o){return o?await y.sendMessage(e,t,{parse_mode:o}):await y.sendMessage(e,t)}async function W(e,t,o,r){if(r){await y.editMessageText(o,{chat_id:e,message_id:t,parse_mode:r});return}await y.editMessageText(o,{chat_id:e,message_id:t})}async function X(e,t){let o;for(const r of t)try{return await e(r)}catch(s){o=s;const u=s instanceof Error?s.message:String(s);console.warn(`[tg-agent] parse_mode ${r} failed, trying fallback: ${u}`)}try{return await e(void 0)}catch(r){if(o){const s=r instanceof Error?r.message:String(r);console.warn(`[tg-agent] parse_mode fallback to plain failed: ${s}`)}throw r}}async function Oe(e,t){return await X(o=>H(e,t,o),L(t))}async function Ue(e,t,o){await X(r=>W(e,t,o,r),L(o))}async function c(e,t){const o=le(t,3900);for(const r of o)await Oe(e,r)}const Le=3900,Ne=1200;function k(e,t=Le){return e.length<=t?{text:e,truncated:!1}:{text:`${e.slice(0,Math.max(0,t-15))}...
|
|
5
|
+
|
|
6
|
+
[truncated]`,truncated:!0}}async function ze(e){const t="Working...",r=(await H(e,t)).message_id;let s=t,u=0,n=Promise.resolve();const d=(i,a=!1)=>{const m=Date.now();!a&&m-u<Ne||i!==s&&(s=i,u=m,n=n.then(async()=>{await W(e,r,i)}).catch(g=>{console.warn("[tg-agent] status edit failed",g)}))},l=i=>{s=i,u=Date.now(),n=n.then(async()=>{await Ue(e,r,i)}).catch(a=>{console.warn("[tg-agent] status edit failed",a)})};return{update:(i,a=!1)=>{const m=k(i).text;d(m,a)},finalize:async i=>{const a=k(i).text;l(a),await n},fail:async i=>{const a=k(i).text;l(a),await n}}}function qe(e){switch(e.type){case"agent_start":return"Working...";case"tool_start":return`Running tool: ${e.name}
|
|
7
|
+
args: ${je(e.args)}`;case"tool_end":return e.ok?`Tool finished: ${e.name} (${e.durationMs}ms)`:`Tool failed: ${e.name} (${e.durationMs}ms)`;case"message_start":return e.role==="assistant"?"Generating reply...":null;case"message_end":return e.role==="assistant"?"Finalizing reply...":null;case"heartbeat":return`Working... ${Math.max(1,Math.round(e.elapsedMs/1e3))}s`;default:return null}}function je(e){try{const t=new WeakSet,o=200,r=12,s=12,u=3,n=(i,a)=>{if(i==null)return i;if(typeof i=="string")return i.length>o?`${i.slice(0,o)}...`:i;if(typeof i=="number"||typeof i=="boolean")return i;if(typeof i=="bigint")return i.toString();if(typeof i=="function")return"[function]";if(typeof i!="object")return String(i);if(t.has(i))return"[circular]";if(a>=u)return"[truncated]";if(t.add(i),Array.isArray(i)){const w=i.slice(0,s).map(v=>n(v,a+1));return i.length>s&&w.push("[truncated]"),w}const m=Object.entries(i),g={};for(const[w,v]of m.slice(0,r))g[w]=n(v,a+1);return m.length>r&&(g._truncated=!0),g},d=n(e,0),l=JSON.stringify(d);return l?l.length>700?`${l.slice(0,700)}...`:l:"null"}catch{return"[unavailable]"}}function C(e){const t=e instanceof Error?e.message:String(e),o=e?.cause;return o instanceof Error?`${t} (${o.message})`:o?`${t} (${String(o)})`:t}function Fe(e,t=140){return e.length<=t?e:`${e.slice(0,t-3)}...`}async function He(){const e=await oe(f.agentDir);if(e.length===0)return"No MCP servers configured. Add [mcp_servers.*] to ~/.tg-agent/config.toml.";const t=await Promise.all(e.map(r=>re(r))),o=["MCP servers:"];for(let r=0;r<e.length;r+=1){const s=e[r],u=t[r],n=u.ok?"ok":`error: ${Fe(u.error??"unknown")}`;o.push(`- ${s.name} (${s.type}) ${te(s)} status=${n} (${u.durationMs}ms)`)}return o.join(`
|
|
8
|
+
`)}async function We(e,t,o,r){const s=await b(t),u=O(s);switch(u.length>0&&(await p(s),await Promise.all(u.map(n=>A(t,n).catch(d=>{console.warn(`[tg-agent] cleanup session file failed id=${n}`,d)})))),o){case"start":case"help":{const n=["Commands:","/new [title] - create a new session","/list - list sessions","/use <id> - switch active session","/close [id] - close a session (default: active)","/reset - clear active session history","/providers - list available providers","/models [provider] - list models for provider","/provider <name> - set provider for current session","/model <provider>/<model> - set model for current session","/mcp - list configured MCP servers","/status - show session model settings","/login <provider> - login to OAuth provider","/logout <provider> - logout from provider","/stop - stop the current running request"].join(`
|
|
9
|
+
`);await c(e,n);return}case"new":{try{const d=R(s,r||"");await p(s),await c(e,`Created session ${d.id} (${d.title}).`)}catch(n){await c(e,n.message)}return}case"list":{await p(s),await c(e,be(ie(s)));return}case"use":{if(!r){await c(e,"Usage: /use <id>");return}if(!ce(s,r)){await c(e,`Session not found: ${r}`);return}await p(s),await c(e,`Active session set to ${r}.`);return}case"close":{const n=r||s.activeSessionId;if(!n){await c(e,"No active session to close.");return}if(!se(s,n)){await c(e,`Session not found: ${n}`);return}await p(s),await A(t,n).catch(l=>{console.warn(`[tg-agent] delete session file failed id=${n}`,l)}),await c(e,`Closed session ${n}.`);return}case"reset":{const n=M(s);if(!n){await c(e,"No active session.");return}ae(n),await p(s),await A(t,n.id).catch(d=>{console.warn(`[tg-agent] reset session file failed id=${n.id}`,d)}),await c(e,`Reset session ${n.id}.`);return}case"providers":{const{authStorage:n,modelRegistry:d}=await S(),l=d.getError(),i=Re(d,n),a=l?`Warning: ${l}
|
|
10
|
+
|
|
11
|
+
${i}`:i;await c(e,a);return}case"models":{const{authStorage:n,modelRegistry:d}=await S();let l=r?h(r):"";if(!l){const a=M(s);if(!a?.modelProvider){await c(e,"Usage: /models <provider>");return}l=a.modelProvider}const i=n.hasAuth(l);await c(e,Te(d,l,i));return}case"provider":{if(!r){await c(e,"Usage: /provider <name>");return}const n=h(r),{modelRegistry:d}=await S();if(!d.getAll().some(a=>a.provider===n)){await c(e,`Unknown provider: ${n}`);return}const i=await j(s,e);i.modelProvider=n,i.modelId=void 0,i.updatedAt=P(),await p(s),await c(e,`Session ${i.id} provider set to ${n}.`);return}case"model":{const n=xe(r);if(!n){await c(e,"Usage: /model <provider>/<model>");return}const d=await j(s,e),l=h(n.provider??d.modelProvider??"");if(!l){await c(e,"Usage: /model <provider>/<model>");return}const{modelRegistry:i}=await S(),a=_e(l,n.modelId);if(!i.find(l,a)){await c(e,`Model not found: ${l}/${a}`);return}d.modelProvider=l,d.modelId=a,d.updatedAt=P(),await p(s),await c(e,`Session ${d.id} model set to ${l}/${a}.`);return}case"status":{const n=M(s);if(!n){await c(e,"No active session.");return}const d=f.modelRef.includes("/")?f.modelRef:`${f.modelProvider}/${f.modelRef}`,l=n.modelProvider||n.modelId?`Session model: ${n.modelProvider??"?"}/${n.modelId??"?"}`:`Default model: ${d}`,i=[`Session: ${n.id} (${n.title})`,l].join(`
|
|
12
|
+
`);await c(e,i);return}case"mcp":{const n=await He();await c(e,n);return}case"login":{const n=K();if(!r){const i=n.map(a=>`- ${a.id} (${a.name})`);await c(e,["OAuth providers:",...i].join(`
|
|
13
|
+
`));return}const d=h(r);if(x.has(t)){await c(e,"Login already in progress.");return}const l=n.find(i=>i.id===d);if(!l){await c(e,`Unknown OAuth provider: ${d}`);return}x.add(t);try{const{authStorage:i}=await S();await c(e,`Starting login for ${l.id}...`),await i.login(l.id,{onAuth:({url:a,instructions:m})=>{const g=["Open this URL in your browser:",a];m&&g.push("",m),c(e,g.join(`
|
|
14
|
+
`))},onPrompt:a=>z(t,e,a),onProgress:a=>{c(e,a)},onManualCodeInput:()=>z(t,e,{message:"Paste the authorization code:"})}),await c(e,`Login completed for ${l.id}.`)}catch(i){const a=C(i);await c(e,`Login failed: ${a}`)}finally{x.delete(t),N(t)}return}case"logout":{if(!r){await c(e,"Usage: /logout <provider>");return}const n=h(r),{authStorage:d}=await S();d.logout(n),await c(e,`Logged out from ${n}.`);return}default:await c(e,`Unknown command: ${o}`)}}async function Xe(e,t,o){const r=q(o);if(r){await We(e,t,r.command,r.args);return}const s=await b(t),u=O(s);u.length>0&&(await p(s),await Promise.all(u.map(a=>A(t,a).catch(m=>{console.warn(`[tg-agent] cleanup session file failed id=${a}`,m)}))));let n=M(s);if(!n)try{n=R(s,""),await p(s),await c(e,`Created session ${n.id}.`)}catch(a){await c(e,a.message);return}const d={role:"user",content:o,ts:P()};D(n,d,f.maxHistoryMessages),await p(s),console.log(`[tg-agent] request user=${t} session=${n.id} messages=${n.messages.length} textLen=${o.length} provider=${n.modelProvider??"-"} model=${n.modelId??"-"}`);let l=null;try{l=await ze(e)}catch(a){console.warn("[tg-agent] status message failed",a)}const i={sessionId:n.id,chatId:e,status:l,abortRequested:!1,cancelRequested:!1};ge(t,i);try{const a=await me(async()=>{const g=await I({userId:t,sessionId:n.id,prompt:o,systemPrompt:f.systemPrompt,modelProvider:n.modelProvider,modelId:n.modelId,onAbortReady:w=>{i.abort=w,i.abortRequested&&w()},onStatus:l?w=>{const v=qe(w);v&&l?.update(v)}:void 0});return console.log(`[tg-agent] response session=${n.id} model=${g.modelProvider}/${g.modelId} file=${g.sessionFile}`),g.text});if(i.cancelRequested){l?await l.finalize("Cancelled by user."):await c(e,"Cancelled by user.");return}const m={role:"assistant",content:a,ts:P()};D(n,m,f.maxHistoryMessages),await p(s),l?await l.finalize(a):await c(e,a)}catch(a){if(console.error("[tg-agent] runModel error",a),i.cancelRequested){l?await l.fail("Cancelled by user."):await c(e,"Cancelled by user.");return}const m=C(a);l?await l.fail(`Error: ${m}`):await c(e,`Error: ${m}`)}finally{pe(t)}}y.on("message",e=>{if(!e.text)return;if(e.chat.type!=="private"){y.sendMessage(e.chat.id,"This bot only supports direct messages.");return}if(!e.from?.id)return;const t=String(e.from.id),o=e.chat.id;if(!De(t)){Ce(t,o);return}const r=e.text.trim(),s=$.get(t);if(s){if(r==="/stop"||r==="/cancel"){Ae(t,"Login cancelled by user."),c(o,"Login cancelled.");return}N(t),s.resolve(r);return}if(q(r)?.command==="stop"){Pe(o,t);return}fe(t,()=>Xe(o,t,r)).catch(async n=>{console.error("[tg-agent] runModel error",n);const d=C(n);await c(o,`Error: ${d}`)})}),y.on("polling_error",e=>{console.error("Polling error",e)}),console.log("tg-agent started");
|