@ebowwa/channel-telegram 1.9.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (88) hide show
  1. package/README.md +58 -0
  2. package/dist/api/fetch-retry.d.ts +5 -0
  3. package/dist/api/fetch-retry.d.ts.map +1 -0
  4. package/dist/api/fetch-retry.js +35 -0
  5. package/dist/api/fetch-retry.js.map +1 -0
  6. package/dist/api/keys.d.ts +9 -0
  7. package/dist/api/keys.d.ts.map +1 -0
  8. package/dist/api/keys.js +23 -0
  9. package/dist/api/keys.js.map +1 -0
  10. package/dist/commands/clear.d.ts +6 -0
  11. package/dist/commands/clear.d.ts.map +1 -0
  12. package/dist/commands/clear.js +11 -0
  13. package/dist/commands/clear.js.map +1 -0
  14. package/dist/commands/doppler.d.ts +8 -0
  15. package/dist/commands/doppler.d.ts.map +1 -0
  16. package/dist/commands/doppler.js +64 -0
  17. package/dist/commands/doppler.js.map +1 -0
  18. package/dist/commands/git.d.ts +8 -0
  19. package/dist/commands/git.d.ts.map +1 -0
  20. package/dist/commands/git.js +70 -0
  21. package/dist/commands/git.js.map +1 -0
  22. package/dist/commands/help.d.ts +8 -0
  23. package/dist/commands/help.d.ts.map +1 -0
  24. package/dist/commands/help.js +26 -0
  25. package/dist/commands/help.js.map +1 -0
  26. package/dist/commands/index.d.ts +19 -0
  27. package/dist/commands/index.d.ts.map +1 -0
  28. package/dist/commands/index.js +36 -0
  29. package/dist/commands/index.js.map +1 -0
  30. package/dist/commands/logs.d.ts +8 -0
  31. package/dist/commands/logs.d.ts.map +1 -0
  32. package/dist/commands/logs.js +28 -0
  33. package/dist/commands/logs.js.map +1 -0
  34. package/dist/commands/resources.d.ts +6 -0
  35. package/dist/commands/resources.d.ts.map +1 -0
  36. package/dist/commands/resources.js +34 -0
  37. package/dist/commands/resources.js.map +1 -0
  38. package/dist/commands/start.d.ts +8 -0
  39. package/dist/commands/start.d.ts.map +1 -0
  40. package/dist/commands/start.js +20 -0
  41. package/dist/commands/start.js.map +1 -0
  42. package/dist/commands/status.d.ts +8 -0
  43. package/dist/commands/status.d.ts.map +1 -0
  44. package/dist/commands/status.js +14 -0
  45. package/dist/commands/status.js.map +1 -0
  46. package/dist/commands/tools.d.ts +6 -0
  47. package/dist/commands/tools.d.ts.map +1 -0
  48. package/dist/commands/tools.js +18 -0
  49. package/dist/commands/tools.js.map +1 -0
  50. package/dist/commands/toolsoutput.d.ts +11 -0
  51. package/dist/commands/toolsoutput.d.ts.map +1 -0
  52. package/dist/commands/toolsoutput.js +35 -0
  53. package/dist/commands/toolsoutput.js.map +1 -0
  54. package/dist/commands/types.d.ts +18 -0
  55. package/dist/commands/types.d.ts.map +1 -0
  56. package/dist/commands/types.js +5 -0
  57. package/dist/commands/types.js.map +1 -0
  58. package/dist/conversation-memory.d.ts +21 -0
  59. package/dist/conversation-memory.d.ts.map +1 -0
  60. package/dist/conversation-memory.js +51 -0
  61. package/dist/conversation-memory.js.map +1 -0
  62. package/dist/index.d.ts +37 -0
  63. package/dist/index.d.ts.map +1 -0
  64. package/dist/index.js +530 -0
  65. package/dist/index.js.map +1 -0
  66. package/dist/types.d.ts +20 -0
  67. package/dist/types.d.ts.map +1 -0
  68. package/dist/types.js +5 -0
  69. package/dist/types.js.map +1 -0
  70. package/package.json +55 -0
  71. package/src/api/fetch-retry.ts +46 -0
  72. package/src/api/keys.ts +21 -0
  73. package/src/commands/clear.ts +13 -0
  74. package/src/commands/doppler.ts +74 -0
  75. package/src/commands/git.ts +77 -0
  76. package/src/commands/help.ts +38 -0
  77. package/src/commands/index.ts +46 -0
  78. package/src/commands/logs.ts +38 -0
  79. package/src/commands/resources.ts +44 -0
  80. package/src/commands/start.ts +31 -0
  81. package/src/commands/status.ts +26 -0
  82. package/src/commands/tools.ts +23 -0
  83. package/src/commands/toolsoutput.ts +50 -0
  84. package/src/commands/types.ts +24 -0
  85. package/src/conversation-memory.ts +57 -0
  86. package/src/index.ts +614 -0
  87. package/src/todo +11 -0
  88. package/src/types.ts +21 -0
@@ -0,0 +1,46 @@
1
+ /**
2
+ * Fetch with retry and exponential backoff for 429 handling
3
+ */
4
+
5
+ const sleep = (ms: number) => new Promise(resolve => setTimeout(resolve, ms));
6
+
7
+ export async function fetchWithRetry(
8
+ url: string,
9
+ options: RequestInit,
10
+ maxRetries = 3,
11
+ baseDelay = 1000
12
+ ): Promise<Response> {
13
+ let lastError: Error | null = null;
14
+
15
+ for (let attempt = 0; attempt < maxRetries; attempt++) {
16
+ try {
17
+ const response = await fetch(url, options);
18
+
19
+ if (response.status === 429) {
20
+ const retryAfter = response.headers.get('Retry-After');
21
+ const delay = retryAfter
22
+ ? parseInt(retryAfter) * 1000
23
+ : baseDelay * Math.pow(2, attempt);
24
+
25
+ console.log(`[429] Rate limited, retrying in ${delay}ms (attempt ${attempt + 1}/${maxRetries})`);
26
+ await sleep(delay);
27
+ continue;
28
+ }
29
+
30
+ if (!response.ok) {
31
+ throw new Error(`HTTP ${response.status}: ${response.statusText}`);
32
+ }
33
+
34
+ return response;
35
+ } catch (error) {
36
+ lastError = error as Error;
37
+ if (attempt < maxRetries - 1) {
38
+ const delay = baseDelay * Math.pow(2, attempt);
39
+ console.log(`[Retry] Attempt ${attempt + 1} failed: ${lastError.message}, retrying in ${delay}ms`);
40
+ await sleep(delay);
41
+ }
42
+ }
43
+ }
44
+
45
+ throw lastError;
46
+ }
@@ -0,0 +1,21 @@
1
+ /**
2
+ * API Key Resolution - supports rolling keys
3
+ */
4
+
5
+ /**
6
+ * Get Z.AI API key from environment
7
+ * Supports single key or JSON array of keys for rolling key rotation
8
+ */
9
+ export function getZAIKey(): string | null {
10
+ const keyValue = process.env.Z_AI_API_KEY || process.env.ANTHROPIC_API_KEYS;
11
+ if (!keyValue) return null;
12
+ try {
13
+ const keysArray = JSON.parse(keyValue);
14
+ if (Array.isArray(keysArray) && keysArray.length > 0) {
15
+ return keysArray[0];
16
+ }
17
+ } catch {
18
+ return keyValue;
19
+ }
20
+ return keyValue;
21
+ }
@@ -0,0 +1,13 @@
1
+ /**
2
+ * /clear command - Reset conversation memory
3
+ */
4
+
5
+ import type { CommandRegistrar } from './types';
6
+
7
+ export const registerClearCommand: CommandRegistrar = (bot, memory) => {
8
+ bot.onText(/\/clear/, async (msg) => {
9
+ const chatId = msg.chat.id;
10
+ memory.clear(chatId);
11
+ await bot.sendMessage(chatId, '🧹 Conversation memory cleared. Starting fresh!');
12
+ });
13
+ };
@@ -0,0 +1,74 @@
1
+ /**
2
+ * /doppler command - Check Doppler config
3
+ */
4
+
5
+ import TelegramBot from 'node-telegram-bot-api';
6
+ import { execSync } from 'child_process';
7
+ import { ConversationMemory } from '../conversation-memory';
8
+ import type { Tool } from '../types';
9
+
10
+ export function registerDopplerCommand(
11
+ bot: TelegramBot,
12
+ _memory: ConversationMemory,
13
+ _tools: Tool[]
14
+ ): void {
15
+ bot.onText(/\/doppler(?:\s+(.+))?/, async (msg: TelegramBot.Message, match: RegExpExecArray | null) => {
16
+ const chatId = msg.chat.id;
17
+ const subCommand = match?.[1]?.trim();
18
+
19
+ try {
20
+ if (subCommand === 'secrets') {
21
+ let statusMsg = 'Doppler Secrets\n\n';
22
+ try {
23
+ const secrets = execSync('doppler secrets --plain 2>&1 | cut -d\'\\t\' -f1', {
24
+ encoding: 'utf-8',
25
+ cwd: '/root'
26
+ });
27
+ const secretList = secrets.trim().split('\n').filter(s => s.length > 0);
28
+ statusMsg += `Found ${secretList.length} secrets:\n\n`;
29
+ statusMsg += secretList.slice(0, 20).join('\n');
30
+ if (secretList.length > 20) {
31
+ statusMsg += `\n... and ${secretList.length - 20} more`;
32
+ }
33
+ } catch (e) {
34
+ statusMsg += `Error: ${(e as Error).message}`;
35
+ }
36
+ await bot.sendMessage(chatId, statusMsg);
37
+ } else {
38
+ let statusMsg = '*Doppler Status*\n\n';
39
+
40
+ try {
41
+ // Get project - extract value from table output
42
+ const projectRaw = execSync('doppler configure get project 2>/dev/null', { encoding: 'utf-8', cwd: '/root' });
43
+ const projectMatch = projectRaw.match(/│\s*project\s*│\s*(\S+)\s*│/);
44
+ const project = projectMatch ? projectMatch[1] : 'Not set';
45
+
46
+ // Get config - extract value from table output
47
+ const configRaw = execSync('doppler configure get config 2>/dev/null', { encoding: 'utf-8', cwd: '/root' });
48
+ const configMatch = configRaw.match(/│\s*config\s*│\s*(\S+)\s*│/);
49
+ const config = configMatch ? configMatch[1] : 'Not set';
50
+
51
+ statusMsg += `Project: \`${project}\`\n`;
52
+ statusMsg += `Config: \`${config}\`\n`;
53
+
54
+ const tokenCheck = execSync('doppler me 2>&1 | head -5', { encoding: 'utf-8', cwd: '/root' });
55
+ if (tokenCheck.includes('WORKPLACE') || tokenCheck.includes('TOKEN PREVIEW')) {
56
+ statusMsg += `Token: Valid`;
57
+ } else {
58
+ statusMsg += `Token: Not configured`;
59
+ }
60
+
61
+ statusMsg += `\n\n_Commands:_\n`;
62
+ statusMsg += `/doppler secrets - List secret names`;
63
+ } catch {
64
+ statusMsg += `Not configured\n\n`;
65
+ statusMsg += `Run: \`doppler setup\``;
66
+ }
67
+
68
+ await bot.sendMessage(chatId, statusMsg, { parse_mode: 'Markdown' });
69
+ }
70
+ } catch (error) {
71
+ await bot.sendMessage(chatId, `Error: ${(error as Error).message}`);
72
+ }
73
+ });
74
+ }
@@ -0,0 +1,77 @@
1
+ /**
2
+ * /git command - Check git status & GitHub auth
3
+ */
4
+
5
+ import TelegramBot from 'node-telegram-bot-api';
6
+ import { execSync } from 'child_process';
7
+ import { ConversationMemory } from '../conversation-memory';
8
+ import type { Tool } from '../types';
9
+
10
+ export function registerGitCommand(
11
+ bot: TelegramBot,
12
+ _memory: ConversationMemory,
13
+ _tools: Tool[]
14
+ ): void {
15
+ bot.onText(/\/git(?:\s+(.+))?/, async (msg: TelegramBot.Message, match: RegExpExecArray | null) => {
16
+ const chatId = msg.chat.id;
17
+ const subCommand = match?.[1]?.trim();
18
+
19
+ try {
20
+ if (subCommand === 'auth') {
21
+ let statusMsg = 'GitHub Auth\n\n';
22
+ try {
23
+ const deviceResp = execSync('gh auth login --web --git-protocol https 2>&1 | head -20', {
24
+ encoding: 'utf-8',
25
+ timeout: 8000,
26
+ cwd: '/root'
27
+ });
28
+ const codeMatch = deviceResp.match(/verification code:\s*([A-Z0-9-]+)/i);
29
+ const urlMatch = deviceResp.match(/https:\/\/github\.com\/login\/device[^\s]*/);
30
+
31
+ if (codeMatch && urlMatch) {
32
+ statusMsg += `Code: ${codeMatch[1]}\n`;
33
+ statusMsg += `URL: ${urlMatch[0]}\n\n`;
34
+ statusMsg += `Open URL and enter code to authenticate.`;
35
+ } else {
36
+ statusMsg += deviceResp.slice(0, 500);
37
+ }
38
+ } catch (e) {
39
+ statusMsg += `Error: ${(e as Error).message}`;
40
+ }
41
+ await bot.sendMessage(chatId, statusMsg);
42
+ } else {
43
+ let statusMsg = 'Git Status\n\n';
44
+
45
+ try {
46
+ const authStatus = execSync('gh auth status 2>&1', { encoding: 'utf-8', cwd: '/root' });
47
+ const loggedIn = authStatus.includes('Logged in');
48
+ const accountMatch = authStatus.match(/account\s+(\w+)/i);
49
+
50
+ statusMsg += `GitHub: ${loggedIn ? 'Connected' : 'Not logged in'}\n`;
51
+ if (accountMatch) statusMsg += `Account: ${accountMatch[1]}\n`;
52
+ if (!loggedIn) statusMsg += `\nUse /git auth to authenticate`;
53
+ } catch {
54
+ statusMsg += `GitHub: Not authenticated\n`;
55
+ statusMsg += `Use /git auth to login\n`;
56
+ }
57
+
58
+ try {
59
+ const branch = execSync('git rev-parse --abbrev-ref HEAD 2>/dev/null', { encoding: 'utf-8', cwd: '/root' }).trim();
60
+ const ahead = execSync('git rev-list --count @{upstream}..HEAD 2>/dev/null || echo 0', { encoding: 'utf-8', cwd: '/root' }).trim();
61
+ const dirty = execSync('git status --porcelain 2>/dev/null | wc -l', { encoding: 'utf-8', cwd: '/root' }).trim();
62
+
63
+ statusMsg += `\nRepo:\n`;
64
+ statusMsg += `Branch: ${branch}\n`;
65
+ statusMsg += `Ahead: ${ahead} commits\n`;
66
+ statusMsg += `Modified: ${dirty} files`;
67
+ } catch {
68
+ statusMsg += `\nRepo: Not a git repo or error`;
69
+ }
70
+
71
+ await bot.sendMessage(chatId, statusMsg);
72
+ }
73
+ } catch (error) {
74
+ await bot.sendMessage(chatId, `Error: ${(error as Error).message}`);
75
+ }
76
+ });
77
+ }
@@ -0,0 +1,38 @@
1
+ /**
2
+ * /help command - Show help message
3
+ */
4
+
5
+ import TelegramBot from 'node-telegram-bot-api';
6
+ import { ConversationMemory } from '../conversation-memory';
7
+ import type { Tool } from '../types';
8
+
9
+ export function registerHelpCommand(
10
+ bot: TelegramBot,
11
+ _memory: ConversationMemory,
12
+ _tools: Tool[]
13
+ ): void {
14
+ bot.onText(/\/help/, async (msg: TelegramBot.Message) => {
15
+ const chatId = msg.chat.id;
16
+ await bot.sendMessage(
17
+ chatId,
18
+ '📚 *GLM Daemon Bot Help*\n\n' +
19
+ '🧠 Powered by GLM-4.7 via Z.AI API\n\n' +
20
+ '*Commands:*\n' +
21
+ '/start - Show welcome message\n' +
22
+ '/status - Check API status\n' +
23
+ '/git - Check git status & GitHub auth\n' +
24
+ '/git auth - Start GitHub auth flow\n' +
25
+ '/doppler - Check Doppler config\n' +
26
+ '/doppler secrets - List secret names\n' +
27
+ '/tools - Show available AI tools\n' +
28
+ '/clear - Clear conversation memory\n' +
29
+ '/resources - Show system resources\n' +
30
+ '/logs [n] - Show last n log lines\n' +
31
+ '/toolsoutput [on|off] - Toggle tool output messages\n' +
32
+ '/help - Show this help\n\n' +
33
+ '*Usage:*\n' +
34
+ 'Just send any message and I\\\'ll respond with AI assistance!',
35
+ { parse_mode: 'Markdown' }
36
+ );
37
+ });
38
+ }
@@ -0,0 +1,46 @@
1
+ /**
2
+ * Command index - Exports all command registrars
3
+ */
4
+
5
+ export { registerStartCommand } from './start';
6
+ export { registerStatusCommand } from './status';
7
+ export { registerHelpCommand } from './help';
8
+ export { registerGitCommand } from './git';
9
+ export { registerDopplerCommand } from './doppler';
10
+ export { registerLogsCommand } from './logs';
11
+ export { registerClearCommand } from './clear';
12
+ export { registerToolsCommand } from './tools';
13
+ export { registerResourcesCommand } from './resources';
14
+
15
+ export type { CommandRegistrar, CommandContext } from './types';
16
+
17
+ import type TelegramBot from 'node-telegram-bot-api';
18
+ import type { CommandRegistrar } from './types';
19
+ import { registerStartCommand } from './start';
20
+ import { registerStatusCommand } from './status';
21
+ import { registerHelpCommand } from './help';
22
+ import { registerGitCommand } from './git';
23
+ import { registerDopplerCommand } from './doppler';
24
+ import { registerLogsCommand } from './logs';
25
+ import { registerClearCommand } from './clear';
26
+ import { registerToolsCommand } from './tools';
27
+ import { registerResourcesCommand } from './resources';
28
+
29
+ /**
30
+ * Register all commands with the bot
31
+ */
32
+ export function registerAllCommands(
33
+ bot: TelegramBot,
34
+ memory: import('../conversation-memory').ConversationMemory,
35
+ tools: import('../index').Tool[]
36
+ ): void {
37
+ registerStartCommand(bot, memory, tools);
38
+ registerStatusCommand(bot, memory, tools);
39
+ registerHelpCommand(bot, memory, tools);
40
+ registerGitCommand(bot, memory, tools);
41
+ registerDopplerCommand(bot, memory, tools);
42
+ registerLogsCommand(bot, memory, tools);
43
+ registerClearCommand(bot, memory, tools);
44
+ registerToolsCommand(bot, memory, tools);
45
+ registerResourcesCommand(bot, memory, tools);
46
+ }
@@ -0,0 +1,38 @@
1
+ /**
2
+ * /logs command - Show systemd journal logs
3
+ */
4
+
5
+ import TelegramBot from 'node-telegram-bot-api';
6
+ import { execSync } from 'child_process';
7
+ import { ConversationMemory } from '../conversation-memory';
8
+ import type { Tool } from '../types';
9
+
10
+ export function registerLogsCommand(
11
+ bot: TelegramBot,
12
+ _memory: ConversationMemory,
13
+ _tools: Tool[]
14
+ ): void {
15
+ bot.onText(/\/logs(?:\s+(\d+))?/, async (msg: TelegramBot.Message, match: RegExpExecArray | null) => {
16
+ const chatId = msg.chat.id;
17
+ const lines = match?.[1] ? parseInt(match[1]) : 20;
18
+
19
+ try {
20
+ const logs = execSync(`journalctl -u telegram-bot -n ${Math.min(lines, 100)} --no-pager 2>&1`, {
21
+ encoding: 'utf-8',
22
+ cwd: '/root'
23
+ });
24
+
25
+ // Truncate if too long for Telegram
26
+ const maxLen = 4000;
27
+ let output = logs.trim();
28
+ if (output.length > maxLen) {
29
+ output = '...\n' + output.slice(-maxLen);
30
+ }
31
+
32
+ // Send without Markdown to avoid parsing errors with log content
33
+ await bot.sendMessage(chatId, `System Logs (last ${lines} lines)\n\n${output}`);
34
+ } catch (error) {
35
+ await bot.sendMessage(chatId, `Error reading logs: ${(error as Error).message}`);
36
+ }
37
+ });
38
+ }
@@ -0,0 +1,44 @@
1
+ /**
2
+ * /resources command - Show system resources
3
+ */
4
+
5
+ import { execSync } from 'child_process';
6
+ import type { CommandRegistrar } from './types';
7
+
8
+ export const registerResourcesCommand: CommandRegistrar = (bot) => {
9
+ bot.onText(/\/resources/, async (msg) => {
10
+ const chatId = msg.chat.id;
11
+
12
+ try {
13
+ const loadAvg = execSync('cat /proc/loadavg', { encoding: 'utf-8' }).trim().split(' ').slice(0, 3).join(', ');
14
+ const cpuCount = execSync('nproc', { encoding: 'utf-8' }).trim();
15
+
16
+ const memInfo = execSync('free -m | grep Mem', { encoding: 'utf-8' });
17
+ const memMatch = memInfo.match(/Mem:\s+(\d+)\s+(\d+)\s+(\d+)/);
18
+ const memTotal = memMatch ? Math.round(parseInt(memMatch[1]) / 1024) : 0;
19
+ const memUsed = memMatch ? Math.round(parseInt(memMatch[2]) / 1024) : 0;
20
+ const memPercent = memMatch ? Math.round((parseInt(memMatch[2]) / parseInt(memMatch[1])) * 100) : 0;
21
+
22
+ const diskInfo = execSync('df -h / | tail -1', { encoding: 'utf-8' });
23
+ const diskMatch = diskInfo.match(/\S+\s+(\S+)\s+(\S+)\s+(\S+)\s+(\d+%)/);
24
+ const diskUsed = diskMatch ? diskMatch[2] : '?';
25
+ const diskTotal = diskMatch ? diskMatch[1] : '?';
26
+ const diskPercent = diskMatch ? diskMatch[4] : '?';
27
+
28
+ const uptime = execSync('uptime -p 2>/dev/null || uptime', { encoding: 'utf-8' }).trim().replace('up ', '');
29
+
30
+ await bot.sendMessage(
31
+ chatId,
32
+ `*System Resources*\n\n` +
33
+ `*CPU:* ${cpuCount} cores\n` +
34
+ `*Load:* ${loadAvg}\n\n` +
35
+ `*Memory:* ${memUsed}/${memTotal}GB (${memPercent}%)\n` +
36
+ `*Disk:* ${diskUsed}/${diskTotal} (${diskPercent})\n\n` +
37
+ `*Uptime:* ${uptime}`,
38
+ { parse_mode: 'Markdown' }
39
+ );
40
+ } catch (error) {
41
+ await bot.sendMessage(chatId, `Error reading resources: ${(error as Error).message}`);
42
+ }
43
+ });
44
+ };
@@ -0,0 +1,31 @@
1
+ /**
2
+ * /start command - Show welcome message
3
+ */
4
+
5
+ import TelegramBot from 'node-telegram-bot-api';
6
+ import { ConversationMemory } from '../conversation-memory';
7
+ import type { Tool } from '../types';
8
+
9
+ export function registerStartCommand(
10
+ bot: TelegramBot,
11
+ _memory: ConversationMemory,
12
+ _tools: Tool[]
13
+ ): void {
14
+ bot.onText(/\/start/, async (msg: TelegramBot.Message) => {
15
+ const chatId = msg.chat.id;
16
+ await bot.sendMessage(
17
+ chatId,
18
+ '👋 Hello! I\'m GLM Daemon Telegram Bot.\n\n' +
19
+ '🧠 Powered by GLM-4.7 via Z.AI API\n\n' +
20
+ 'Commands:\n' +
21
+ '/start - Show this message\n' +
22
+ '/status - Check API status\n' +
23
+ '/git - Check git status & GitHub auth\n' +
24
+ '/doppler - Check Doppler config\n' +
25
+ '/tools - Show available AI tools\n' +
26
+ '/clear - Clear conversation memory\n' +
27
+ '/help - Show help\n\n' +
28
+ 'Or just send any message for AI assistance!'
29
+ );
30
+ });
31
+ }
@@ -0,0 +1,26 @@
1
+ /**
2
+ * /status command - Check API status
3
+ */
4
+
5
+ import TelegramBot from 'node-telegram-bot-api';
6
+ import { ConversationMemory } from '../conversation-memory';
7
+ import type { Tool } from '../types';
8
+ import { getZAIKey } from '../api/keys';
9
+
10
+ export function registerStatusCommand(
11
+ bot: TelegramBot,
12
+ _memory: ConversationMemory,
13
+ _tools: Tool[]
14
+ ): void {
15
+ bot.onText(/\/status/, async (msg: TelegramBot.Message) => {
16
+ const chatId = msg.chat.id;
17
+ const apiKey = getZAIKey();
18
+ await bot.sendMessage(
19
+ chatId,
20
+ `*Bot Status*\n\n` +
21
+ `*AI:* GLM-4.7 via Z.AI\n` +
22
+ `*API:* ${apiKey ? 'Connected' : 'Disconnected'}`,
23
+ { parse_mode: 'Markdown' }
24
+ );
25
+ });
26
+ }
@@ -0,0 +1,23 @@
1
+ /**
2
+ * /tools command - Show available LLM tools
3
+ */
4
+
5
+ import type { CommandRegistrar } from './types';
6
+
7
+ export const registerToolsCommand: CommandRegistrar = (bot, _memory, tools) => {
8
+ bot.onText(/\/tools/, async (msg) => {
9
+ const chatId = msg.chat.id;
10
+ let toolsMsg = '*🛠️ Available Tools*\n\n';
11
+ toolsMsg += 'The AI can use these tools:\n\n';
12
+
13
+ for (const tool of tools) {
14
+ toolsMsg += `*${tool.name}*\n`;
15
+ toolsMsg += `${tool.description}\n\n`;
16
+ }
17
+
18
+ toolsMsg += '_Just ask the AI to use them!_';
19
+ toolsMsg += '\nExample: "Show me system info" or "List files in /root"';
20
+
21
+ await bot.sendMessage(chatId, toolsMsg, { parse_mode: 'Markdown' });
22
+ });
23
+ };
@@ -0,0 +1,50 @@
1
+ /**
2
+ * /toolsoutput command - Toggle tool output messages
3
+ *
4
+ * When disabled, tools still execute but their output isn't sent to Telegram
5
+ */
6
+
7
+ import TelegramBot from 'node-telegram-bot-api';
8
+ import { ConversationMemory } from '../conversation-memory';
9
+ import type { Tool } from '../types';
10
+
11
+ // Global state for tool output visibility
12
+ let toolOutputEnabled = true;
13
+
14
+ export function isToolOutputEnabled(): boolean {
15
+ return toolOutputEnabled;
16
+ }
17
+
18
+ export function registerToolsOutputCommand(
19
+ bot: TelegramBot,
20
+ _memory: ConversationMemory,
21
+ _tools: Tool[]
22
+ ): void {
23
+ bot.onText(/\/toolsoutput(?:\s+(on|off|toggle))?/, async (msg: TelegramBot.Message, match: RegExpExecArray | null) => {
24
+ const chatId = msg.chat.id;
25
+ const subCommand = match?.[1]?.toLowerCase();
26
+
27
+ if (subCommand === 'on') {
28
+ toolOutputEnabled = true;
29
+ } else if (subCommand === 'off') {
30
+ toolOutputEnabled = false;
31
+ } else {
32
+ // toggle or no arg = toggle
33
+ toolOutputEnabled = !toolOutputEnabled;
34
+ }
35
+
36
+ const status = toolOutputEnabled ? 'ON' : 'OFF';
37
+ const emoji = toolOutputEnabled ? '🔊' : '🔇';
38
+
39
+ await bot.sendMessage(
40
+ chatId,
41
+ `${emoji} *Tool Output: ${status}*\n\n` +
42
+ `Tool execution messages are now ${toolOutputEnabled ? 'visible' : 'hidden'}.\n\n` +
43
+ `Commands:\n` +
44
+ `/toolsoutput on - Enable tool messages\n` +
45
+ `/toolsoutput off - Disable tool messages\n` +
46
+ `/toolsoutput toggle - Toggle`,
47
+ { parse_mode: 'Markdown' }
48
+ );
49
+ });
50
+ }
@@ -0,0 +1,24 @@
1
+ /**
2
+ * Types for Telegram command handlers
3
+ */
4
+
5
+ import TelegramBot from 'node-telegram-bot-api';
6
+ import { ConversationMemory } from '../conversation-memory';
7
+
8
+ /**
9
+ * Command registration function signature
10
+ */
11
+ export type CommandRegistrar = (
12
+ bot: TelegramBot,
13
+ memory: ConversationMemory,
14
+ tools: import('../index').Tool[]
15
+ ) => void;
16
+
17
+ /**
18
+ * Context passed to command handlers
19
+ */
20
+ export interface CommandContext {
21
+ bot: TelegramBot;
22
+ memory: ConversationMemory;
23
+ tools: import('../index').Tool[];
24
+ }
@@ -0,0 +1,57 @@
1
+ /**
2
+ * Conversation Memory - tracks chat history per user
3
+ */
4
+
5
+ import { readFileSync, writeFileSync, existsSync } from 'fs';
6
+
7
+ interface Message { role: 'user' | 'assistant' | 'system' | 'tool'; content: string; name?: string }
8
+
9
+ export class ConversationMemory {
10
+ private conversations = new Map<number, Message[]>();
11
+ private maxMessages = 10; // Keep last N messages
12
+ private file: string;
13
+
14
+ constructor(file?: string) {
15
+ this.file = file || '/root/conversations.json';
16
+ this.load();
17
+ }
18
+
19
+ private load() {
20
+ if (!existsSync(this.file)) return;
21
+ try {
22
+ const data = JSON.parse(readFileSync(this.file, 'utf-8'));
23
+ for (const [chatId, messages] of Object.entries(data)) {
24
+ this.conversations.set(Number(chatId), messages as Message[]);
25
+ }
26
+ } catch {}
27
+ }
28
+
29
+ private save() {
30
+ const data = Object.fromEntries(this.conversations);
31
+ writeFileSync(this.file, JSON.stringify(data, null, 2));
32
+ }
33
+
34
+ add(chatId: number, role: 'user' | 'assistant' | 'system' | 'tool', content: string, name?: string) {
35
+ if (!this.conversations.has(chatId)) {
36
+ this.conversations.set(chatId, []);
37
+ }
38
+ const history = this.conversations.get(chatId)!;
39
+ const msg: Message = { role, content };
40
+ if (name) msg.name = name;
41
+ history.push(msg);
42
+ // Keep only last N messages
43
+ if (history.length > this.maxMessages) {
44
+ history.splice(0, history.length - this.maxMessages);
45
+ }
46
+ this.save();
47
+ }
48
+
49
+ get(chatId: number): Message[] {
50
+ return this.conversations.get(chatId) || [];
51
+ }
52
+
53
+ clear(chatId: number) {
54
+ this.conversations.delete(chatId);
55
+ this.save();
56
+ }
57
+ }