multis 0.1.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.
@@ -0,0 +1,95 @@
1
+ const { Telegraf } = require('telegraf');
2
+ const { Platform } = require('./base');
3
+ const { Message } = require('./message');
4
+ const { logAudit } = require('../governance/audit');
5
+ const { DocumentIndexer } = require('../indexer/index');
6
+
7
+ /**
8
+ * Telegram platform adapter.
9
+ * Wraps Telegraf bot, converts ctx to normalized Message objects.
10
+ */
11
+ class TelegramPlatform extends Platform {
12
+ constructor(config) {
13
+ super('telegram', config);
14
+ const token = config.platforms?.telegram?.bot_token || config.telegram_bot_token;
15
+ if (!token) {
16
+ throw new Error('Telegram bot token is required');
17
+ }
18
+ this.bot = new Telegraf(token);
19
+ this.indexer = new DocumentIndexer();
20
+ }
21
+
22
+ async start() {
23
+ // Wire up raw message handler that converts to Message objects
24
+ this.bot.on('message', (ctx) => {
25
+ if (!this._messageCallback) return;
26
+
27
+ // Handle document uploads separately
28
+ if (ctx.message.document) {
29
+ this._handleDocument(ctx);
30
+ return;
31
+ }
32
+
33
+ const text = ctx.message.text;
34
+ if (!text) return;
35
+
36
+ const msg = new Message({
37
+ id: ctx.message.message_id,
38
+ platform: 'telegram',
39
+ chatId: ctx.chat.id,
40
+ chatName: ctx.chat.title || ctx.chat.first_name || '',
41
+ senderId: ctx.from.id,
42
+ senderName: ctx.from.username || ctx.from.first_name || '',
43
+ isSelf: false,
44
+ text,
45
+ raw: ctx,
46
+ });
47
+
48
+ this._messageCallback(msg, this);
49
+ });
50
+
51
+ this.bot.catch((err, ctx) => {
52
+ console.error('Telegram error:', err.message);
53
+ logAudit({ action: 'error', platform: 'telegram', error: err.message });
54
+ });
55
+
56
+ this.bot.launch().catch(err => {
57
+ console.error('Telegram: launch error:', err.message);
58
+ });
59
+ console.log('Telegram: bot started');
60
+ }
61
+
62
+ async stop() {
63
+ this.bot.stop('shutdown');
64
+ }
65
+
66
+ async send(chatId, text) {
67
+ await this.bot.telegram.sendMessage(chatId, text);
68
+ }
69
+
70
+ /**
71
+ * Handle document uploads - Telegram-specific (downloads file, indexes).
72
+ * Calls the message callback with a special document Message.
73
+ */
74
+ async _handleDocument(ctx) {
75
+ // Create message for auth check, then handle doc inline
76
+ const msg = new Message({
77
+ id: ctx.message.message_id,
78
+ platform: 'telegram',
79
+ chatId: ctx.chat.id,
80
+ chatName: ctx.chat.title || '',
81
+ senderId: ctx.from.id,
82
+ senderName: ctx.from.username || ctx.from.first_name || '',
83
+ isSelf: false,
84
+ text: ctx.message.caption || '',
85
+ raw: ctx,
86
+ });
87
+ msg._document = ctx.message.document;
88
+ msg._indexer = this.indexer;
89
+ msg._telegram = ctx.telegram;
90
+
91
+ this._messageCallback(msg, this);
92
+ }
93
+ }
94
+
95
+ module.exports = { TelegramPlatform };
@@ -0,0 +1,125 @@
1
+ const { execSync } = require('child_process');
2
+ const fs = require('fs');
3
+ const path = require('path');
4
+ const { isCommandAllowed, isPathAllowed } = require('../governance/validate');
5
+ const { logAudit } = require('../governance/audit');
6
+
7
+ const MAX_OUTPUT = 4000; // Telegram message limit ~4096 chars
8
+
9
+ /**
10
+ * Execute a shell command after governance validation
11
+ * @param {string} command - Full command string
12
+ * @param {number} userId - Telegram user ID for audit
13
+ * @returns {Object} - { success, output, denied, reason }
14
+ */
15
+ function execCommand(command, userId) {
16
+ const check = isCommandAllowed(command);
17
+
18
+ if (!check.allowed) {
19
+ logAudit({ action: 'exec', user_id: userId, command, allowed: false, reason: check.reason });
20
+ return { success: false, denied: true, reason: check.reason };
21
+ }
22
+
23
+ if (check.requiresConfirmation) {
24
+ logAudit({ action: 'exec', user_id: userId, command, allowed: true, requires_confirmation: true });
25
+ return { success: false, denied: false, needsConfirmation: true, command };
26
+ }
27
+
28
+ try {
29
+ const output = execSync(command, {
30
+ encoding: 'utf8',
31
+ timeout: 10000,
32
+ maxBuffer: 1024 * 1024,
33
+ shell: '/bin/bash'
34
+ });
35
+
36
+ const trimmed = output.length > MAX_OUTPUT
37
+ ? output.slice(0, MAX_OUTPUT) + '\n... (truncated)'
38
+ : output;
39
+
40
+ logAudit({ action: 'exec', user_id: userId, command, allowed: true, status: 'success' });
41
+ return { success: true, output: trimmed || '(no output)' };
42
+ } catch (err) {
43
+ const stderr = err.stderr || err.message;
44
+ logAudit({ action: 'exec', user_id: userId, command, allowed: true, status: 'error', error: stderr });
45
+ return { success: false, output: `Error: ${stderr}` };
46
+ }
47
+ }
48
+
49
+ /**
50
+ * Read a file after governance path validation
51
+ * @param {string} filePath - Path to read
52
+ * @param {number} userId - Telegram user ID for audit
53
+ * @returns {Object} - { success, output, denied, reason }
54
+ */
55
+ function readFile(filePath, userId) {
56
+ const expanded = filePath.replace(/^~/, process.env.HOME || process.env.USERPROFILE);
57
+ const resolved = path.resolve(expanded);
58
+
59
+ const check = isPathAllowed(resolved);
60
+
61
+ if (!check.allowed) {
62
+ logAudit({ action: 'read', user_id: userId, path: filePath, allowed: false, reason: check.reason });
63
+ return { success: false, denied: true, reason: check.reason };
64
+ }
65
+
66
+ try {
67
+ if (!fs.existsSync(resolved)) {
68
+ return { success: false, output: `File not found: ${filePath}` };
69
+ }
70
+
71
+ const stat = fs.statSync(resolved);
72
+ if (stat.isDirectory()) {
73
+ // List directory contents instead
74
+ const entries = fs.readdirSync(resolved);
75
+ const output = entries.join('\n') || '(empty directory)';
76
+ const trimmed = output.length > MAX_OUTPUT
77
+ ? output.slice(0, MAX_OUTPUT) + '\n... (truncated)'
78
+ : output;
79
+ logAudit({ action: 'read', user_id: userId, path: filePath, allowed: true, type: 'directory' });
80
+ return { success: true, output: trimmed };
81
+ }
82
+
83
+ if (stat.size > 512 * 1024) {
84
+ return { success: false, output: `File too large: ${(stat.size / 1024).toFixed(0)}KB (max 512KB)` };
85
+ }
86
+
87
+ const content = fs.readFileSync(resolved, 'utf8');
88
+ const trimmed = content.length > MAX_OUTPUT
89
+ ? content.slice(0, MAX_OUTPUT) + '\n... (truncated)'
90
+ : content;
91
+
92
+ logAudit({ action: 'read', user_id: userId, path: filePath, allowed: true, type: 'file' });
93
+ return { success: true, output: trimmed || '(empty file)' };
94
+ } catch (err) {
95
+ logAudit({ action: 'read', user_id: userId, path: filePath, allowed: true, status: 'error', error: err.message });
96
+ return { success: false, output: `Error: ${err.message}` };
97
+ }
98
+ }
99
+
100
+ /**
101
+ * List available skills from skills/ directory
102
+ * @returns {string} - Formatted skill list
103
+ */
104
+ function listSkills() {
105
+ const skillsDir = path.join(__dirname, '..', '..', 'skills');
106
+
107
+ if (!fs.existsSync(skillsDir)) {
108
+ return 'No skills directory found.';
109
+ }
110
+
111
+ const files = fs.readdirSync(skillsDir).filter(f => f.endsWith('.md'));
112
+ const skills = files.map(f => {
113
+ const content = fs.readFileSync(path.join(skillsDir, f), 'utf8');
114
+ // Parse frontmatter
115
+ const match = content.match(/^---\n([\s\S]*?)\n---/);
116
+ if (!match) return `- ${f}`;
117
+ const name = (match[1].match(/name:\s*(.+)/) || [])[1] || f;
118
+ const desc = (match[1].match(/description:\s*(.+)/) || [])[1] || '';
119
+ return `- ${name}: ${desc}`;
120
+ });
121
+
122
+ return skills.join('\n') || 'No skills found.';
123
+ }
124
+
125
+ module.exports = { execCommand, readFile, listSkills };