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.
- package/.env.example +19 -0
- package/CLAUDE.md +66 -0
- package/README.md +98 -0
- package/package.json +32 -0
- package/skills/capture.md +60 -0
- package/skills/files.md +38 -0
- package/skills/shell.md +53 -0
- package/skills/weather.md +32 -0
- package/src/bot/handlers.js +712 -0
- package/src/bot/telegram.js +51 -0
- package/src/cli/setup-beeper.js +239 -0
- package/src/config.js +157 -0
- package/src/governance/audit.js +95 -0
- package/src/governance/validate.js +99 -0
- package/src/index.js +71 -0
- package/src/indexer/chunk.js +68 -0
- package/src/indexer/chunker.js +87 -0
- package/src/indexer/index.js +150 -0
- package/src/indexer/parsers.js +299 -0
- package/src/indexer/store.js +256 -0
- package/src/llm/anthropic.js +106 -0
- package/src/llm/base.js +38 -0
- package/src/llm/client.js +34 -0
- package/src/llm/ollama.js +148 -0
- package/src/llm/openai.js +107 -0
- package/src/llm/prompts.js +71 -0
- package/src/memory/capture.js +85 -0
- package/src/memory/manager.js +123 -0
- package/src/platforms/base.js +38 -0
- package/src/platforms/beeper.js +238 -0
- package/src/platforms/message.js +61 -0
- package/src/platforms/telegram.js +95 -0
- package/src/skills/executor.js +125 -0
|
@@ -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 };
|