groove-dev 0.19.9 → 0.21.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/CHANGELOG.md +33 -0
- package/CLAUDE.md +1 -1
- package/node_modules/@groove-dev/cli/bin/groove.js +1 -1
- package/node_modules/@groove-dev/daemon/package.json +4 -0
- package/node_modules/@groove-dev/daemon/src/api.js +86 -0
- package/node_modules/@groove-dev/daemon/src/gateways/base.js +87 -0
- package/node_modules/@groove-dev/daemon/src/gateways/discord.js +220 -0
- package/node_modules/@groove-dev/daemon/src/gateways/formatter.js +201 -0
- package/node_modules/@groove-dev/daemon/src/gateways/manager.js +695 -0
- package/node_modules/@groove-dev/daemon/src/gateways/slack.js +165 -0
- package/node_modules/@groove-dev/daemon/src/gateways/telegram.js +265 -0
- package/node_modules/@groove-dev/daemon/src/index.js +4 -0
- package/node_modules/@groove-dev/daemon/src/validate.js +55 -0
- package/node_modules/@groove-dev/gui/.groove/audit.log +2 -0
- package/node_modules/@groove-dev/gui/.groove/codebase-index.json +1 -1
- package/node_modules/@groove-dev/gui/.groove/config.json +2 -2
- package/node_modules/@groove-dev/gui/.groove/daemon.pid +1 -1
- package/node_modules/@groove-dev/gui/.groove/timeline.json +3000 -0
- package/node_modules/@groove-dev/gui/AGENTS_REGISTRY.md +1 -1
- package/node_modules/@groove-dev/gui/dist/assets/index-CNqM3_F2.js +552 -0
- package/node_modules/@groove-dev/gui/dist/assets/index-ChDhUvQR.css +1 -0
- package/node_modules/@groove-dev/gui/dist/index.html +2 -2
- package/node_modules/@groove-dev/gui/src/stores/groove.js +7 -0
- package/node_modules/@groove-dev/gui/src/views/settings.jsx +382 -25
- package/package.json +1 -1
- package/packages/cli/bin/groove.js +1 -1
- package/packages/daemon/package.json +4 -0
- package/packages/daemon/src/api.js +86 -0
- package/packages/daemon/src/gateways/base.js +87 -0
- package/packages/daemon/src/gateways/discord.js +220 -0
- package/packages/daemon/src/gateways/formatter.js +201 -0
- package/packages/daemon/src/gateways/manager.js +695 -0
- package/packages/daemon/src/gateways/slack.js +165 -0
- package/packages/daemon/src/gateways/telegram.js +265 -0
- package/packages/daemon/src/index.js +4 -0
- package/packages/daemon/src/validate.js +55 -0
- package/packages/gui/dist/assets/index-CNqM3_F2.js +552 -0
- package/packages/gui/dist/assets/index-ChDhUvQR.css +1 -0
- package/packages/gui/dist/index.html +2 -2
- package/packages/gui/src/stores/groove.js +7 -0
- package/packages/gui/src/views/settings.jsx +382 -25
- package/node_modules/@groove-dev/gui/dist/assets/index-CdbNHOqF.css +0 -1
- package/node_modules/@groove-dev/gui/dist/assets/index-Db0ZssmH.js +0 -537
- package/packages/gui/dist/assets/index-CdbNHOqF.css +0 -1
- package/packages/gui/dist/assets/index-Db0ZssmH.js +0 -537
|
@@ -0,0 +1,165 @@
|
|
|
1
|
+
// GROOVE — Slack Gateway (@slack/bolt Socket Mode)
|
|
2
|
+
// FSL-1.1-Apache-2.0 — see LICENSE
|
|
3
|
+
|
|
4
|
+
import { BaseGateway } from './base.js';
|
|
5
|
+
import { truncate, statusEmoji, formatTokens } from './formatter.js';
|
|
6
|
+
|
|
7
|
+
export class SlackGateway extends BaseGateway {
|
|
8
|
+
static type = 'slack';
|
|
9
|
+
static displayName = 'Slack';
|
|
10
|
+
static description = 'Slack bot for notifications and agent commands';
|
|
11
|
+
static credentialKeys = [
|
|
12
|
+
{ key: 'bot_token', label: 'Bot Token (xoxb-...)', required: true, help: 'Slack App \u2192 OAuth & Permissions' },
|
|
13
|
+
{ key: 'app_token', label: 'App Token (xapp-...)', required: true, help: 'Socket Mode requires an app-level token' },
|
|
14
|
+
];
|
|
15
|
+
|
|
16
|
+
constructor(daemon, config) {
|
|
17
|
+
super(daemon, config);
|
|
18
|
+
this.app = null;
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
async connect() {
|
|
22
|
+
const botToken = this._getCredential('bot_token');
|
|
23
|
+
const appToken = this._getCredential('app_token');
|
|
24
|
+
if (!botToken) throw new Error('Slack bot token not configured');
|
|
25
|
+
if (!appToken) throw new Error('Slack app token not configured (required for Socket Mode)');
|
|
26
|
+
|
|
27
|
+
let App;
|
|
28
|
+
try {
|
|
29
|
+
const bolt = await import('@slack/bolt');
|
|
30
|
+
App = bolt.default?.App || bolt.App;
|
|
31
|
+
} catch {
|
|
32
|
+
throw new Error('Slack gateway requires @slack/bolt. Install with: npm i @slack/bolt');
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
this.app = new App({
|
|
36
|
+
token: botToken,
|
|
37
|
+
appToken,
|
|
38
|
+
socketMode: true,
|
|
39
|
+
});
|
|
40
|
+
|
|
41
|
+
// Handle messages starting with /
|
|
42
|
+
this.app.message(/^\/\w+/, async ({ message, say }) => {
|
|
43
|
+
const userId = message.user;
|
|
44
|
+
const [command, ...args] = message.text.slice(1).split(/\s+/);
|
|
45
|
+
|
|
46
|
+
// Auto-capture channelId
|
|
47
|
+
if (!this.config.chatId) {
|
|
48
|
+
this.config.chatId = message.channel;
|
|
49
|
+
this.daemon.gateways._save(this.config.id);
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
const response = await this.handleCommand(command, args, userId);
|
|
53
|
+
if (response) {
|
|
54
|
+
await say(this._buildReply(response));
|
|
55
|
+
}
|
|
56
|
+
});
|
|
57
|
+
|
|
58
|
+
// Handle approve button action
|
|
59
|
+
this.app.action('groove_approve', async ({ action, ack, respond }) => {
|
|
60
|
+
await ack();
|
|
61
|
+
const approvalId = action.value;
|
|
62
|
+
try {
|
|
63
|
+
this.daemon.supervisor.approve(approvalId);
|
|
64
|
+
await respond({
|
|
65
|
+
replace_original: true,
|
|
66
|
+
text: `\u2705 Approved: ${approvalId}`,
|
|
67
|
+
blocks: [
|
|
68
|
+
{ type: 'section', text: { type: 'mrkdwn', text: `\u2705 *Approved:* \`${approvalId}\`` } },
|
|
69
|
+
],
|
|
70
|
+
});
|
|
71
|
+
} catch (err) {
|
|
72
|
+
await respond({ text: `Error: ${err.message}` });
|
|
73
|
+
}
|
|
74
|
+
});
|
|
75
|
+
|
|
76
|
+
// Handle reject button action
|
|
77
|
+
this.app.action('groove_reject', async ({ action, ack, respond }) => {
|
|
78
|
+
await ack();
|
|
79
|
+
const approvalId = action.value;
|
|
80
|
+
try {
|
|
81
|
+
this.daemon.supervisor.reject(approvalId);
|
|
82
|
+
await respond({
|
|
83
|
+
replace_original: true,
|
|
84
|
+
text: `\u274c Rejected: ${approvalId}`,
|
|
85
|
+
blocks: [
|
|
86
|
+
{ type: 'section', text: { type: 'mrkdwn', text: `\u274c *Rejected:* \`${approvalId}\`` } },
|
|
87
|
+
],
|
|
88
|
+
});
|
|
89
|
+
} catch (err) {
|
|
90
|
+
await respond({ text: `Error: ${err.message}` });
|
|
91
|
+
}
|
|
92
|
+
});
|
|
93
|
+
|
|
94
|
+
await this.app.start();
|
|
95
|
+
|
|
96
|
+
this.connected = true;
|
|
97
|
+
console.log('[Groove:Slack] Connected via Socket Mode');
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
async disconnect() {
|
|
101
|
+
if (this.app) {
|
|
102
|
+
await this.app.stop();
|
|
103
|
+
this.app = null;
|
|
104
|
+
}
|
|
105
|
+
this.connected = false;
|
|
106
|
+
console.log('[Groove:Slack] Disconnected');
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
async send(text, options = {}) {
|
|
110
|
+
if (!this.app || !this.config.chatId) return;
|
|
111
|
+
|
|
112
|
+
const payload = { channel: this.config.chatId };
|
|
113
|
+
|
|
114
|
+
if (options.approvalId) {
|
|
115
|
+
// Block Kit message with approve/reject buttons
|
|
116
|
+
payload.text = text; // Fallback for notifications
|
|
117
|
+
payload.blocks = [
|
|
118
|
+
{ type: 'section', text: { type: 'mrkdwn', text: `\ud83d\udea8 *Approval Required*\n${truncate(text, 2800)}` } },
|
|
119
|
+
{ type: 'divider' },
|
|
120
|
+
{
|
|
121
|
+
type: 'actions',
|
|
122
|
+
elements: [
|
|
123
|
+
{
|
|
124
|
+
type: 'button',
|
|
125
|
+
text: { type: 'plain_text', text: '\u2705 Approve' },
|
|
126
|
+
style: 'primary',
|
|
127
|
+
action_id: 'groove_approve',
|
|
128
|
+
value: options.approvalId,
|
|
129
|
+
},
|
|
130
|
+
{
|
|
131
|
+
type: 'button',
|
|
132
|
+
text: { type: 'plain_text', text: '\u274c Reject' },
|
|
133
|
+
style: 'danger',
|
|
134
|
+
action_id: 'groove_reject',
|
|
135
|
+
value: options.approvalId,
|
|
136
|
+
},
|
|
137
|
+
],
|
|
138
|
+
},
|
|
139
|
+
];
|
|
140
|
+
} else {
|
|
141
|
+
// Standard Block Kit message
|
|
142
|
+
payload.text = truncate(text, 3000); // Fallback
|
|
143
|
+
payload.blocks = [
|
|
144
|
+
{ type: 'section', text: { type: 'mrkdwn', text: truncate(text, 3000) } },
|
|
145
|
+
];
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
await this.app.client.chat.postMessage(payload);
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
/**
|
|
152
|
+
* Build a reply payload from a command response.
|
|
153
|
+
*/
|
|
154
|
+
_buildReply(response) {
|
|
155
|
+
if (!response) return {};
|
|
156
|
+
const text = response.text || '';
|
|
157
|
+
// Wrap in a code block for command output readability
|
|
158
|
+
return {
|
|
159
|
+
text,
|
|
160
|
+
blocks: [
|
|
161
|
+
{ type: 'section', text: { type: 'mrkdwn', text: '```' + truncate(text, 2900) + '```' } },
|
|
162
|
+
],
|
|
163
|
+
};
|
|
164
|
+
}
|
|
165
|
+
}
|
|
@@ -0,0 +1,265 @@
|
|
|
1
|
+
// GROOVE — Telegram Gateway (Zero-dependency Bot API)
|
|
2
|
+
// FSL-1.1-Apache-2.0 — see LICENSE
|
|
3
|
+
|
|
4
|
+
import { BaseGateway } from './base.js';
|
|
5
|
+
|
|
6
|
+
const POLL_TIMEOUT = 30; // seconds — Telegram long-poll
|
|
7
|
+
const BACKOFF_BASE = 3000; // ms
|
|
8
|
+
const BACKOFF_MAX = 30000; // ms
|
|
9
|
+
|
|
10
|
+
export class TelegramGateway extends BaseGateway {
|
|
11
|
+
static type = 'telegram';
|
|
12
|
+
static displayName = 'Telegram';
|
|
13
|
+
static description = 'Telegram bot for notifications and agent commands';
|
|
14
|
+
static credentialKeys = [
|
|
15
|
+
{ key: 'bot_token', label: 'Bot Token', required: true, help: 'Create a bot via @BotFather on Telegram' },
|
|
16
|
+
];
|
|
17
|
+
|
|
18
|
+
constructor(daemon, config) {
|
|
19
|
+
super(daemon, config);
|
|
20
|
+
this.token = null;
|
|
21
|
+
this.botInfo = null;
|
|
22
|
+
this._pollOffset = 0;
|
|
23
|
+
this._polling = false;
|
|
24
|
+
this._abort = null;
|
|
25
|
+
this._backoff = BACKOFF_BASE;
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
async connect() {
|
|
29
|
+
this.token = this._getCredential('bot_token');
|
|
30
|
+
if (!this.token) throw new Error('Telegram bot token not configured');
|
|
31
|
+
|
|
32
|
+
// Validate token with getMe
|
|
33
|
+
this.botInfo = await this._api('getMe');
|
|
34
|
+
this.connected = true;
|
|
35
|
+
this._backoff = BACKOFF_BASE;
|
|
36
|
+
|
|
37
|
+
console.log(`[Groove:Telegram] Connected as @${this.botInfo.username}`);
|
|
38
|
+
|
|
39
|
+
// Start polling loop (non-blocking)
|
|
40
|
+
this._startPolling();
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
async disconnect() {
|
|
44
|
+
this._polling = false;
|
|
45
|
+
if (this._abort) {
|
|
46
|
+
this._abort.abort();
|
|
47
|
+
this._abort = null;
|
|
48
|
+
}
|
|
49
|
+
this.connected = false;
|
|
50
|
+
console.log('[Groove:Telegram] Disconnected');
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
async send(text, options = {}) {
|
|
54
|
+
const chatId = this.config.chatId;
|
|
55
|
+
if (!chatId) return; // No chat configured yet — will auto-capture on first command
|
|
56
|
+
|
|
57
|
+
const params = {
|
|
58
|
+
chat_id: chatId,
|
|
59
|
+
text,
|
|
60
|
+
parse_mode: 'HTML',
|
|
61
|
+
disable_web_page_preview: true,
|
|
62
|
+
};
|
|
63
|
+
|
|
64
|
+
// Add inline keyboard for approval actions
|
|
65
|
+
if (options.approvalId) {
|
|
66
|
+
params.reply_markup = {
|
|
67
|
+
inline_keyboard: [[
|
|
68
|
+
{ text: '\u2705 Approve', callback_data: `approve:${options.approvalId}` },
|
|
69
|
+
{ text: '\u274c Reject', callback_data: `reject:${options.approvalId}` },
|
|
70
|
+
]],
|
|
71
|
+
};
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
await this._api('sendMessage', params);
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
getStatus() {
|
|
78
|
+
return {
|
|
79
|
+
...super.getStatus(),
|
|
80
|
+
botUsername: this.botInfo?.username || null,
|
|
81
|
+
};
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
// -------------------------------------------------------------------
|
|
85
|
+
// Long-Polling Loop
|
|
86
|
+
// -------------------------------------------------------------------
|
|
87
|
+
|
|
88
|
+
async _startPolling() {
|
|
89
|
+
this._polling = true;
|
|
90
|
+
|
|
91
|
+
while (this._polling) {
|
|
92
|
+
try {
|
|
93
|
+
this._abort = new AbortController();
|
|
94
|
+
const updates = await this._api('getUpdates', {
|
|
95
|
+
offset: this._pollOffset,
|
|
96
|
+
timeout: POLL_TIMEOUT,
|
|
97
|
+
allowed_updates: ['message', 'callback_query'],
|
|
98
|
+
}, this._abort.signal);
|
|
99
|
+
|
|
100
|
+
// Reset backoff on success
|
|
101
|
+
this._backoff = BACKOFF_BASE;
|
|
102
|
+
|
|
103
|
+
for (const update of updates) {
|
|
104
|
+
this._pollOffset = update.update_id + 1;
|
|
105
|
+
this._handleUpdate(update).catch((err) => {
|
|
106
|
+
console.log(`[Groove:Telegram] Error handling update: ${err.message}`);
|
|
107
|
+
});
|
|
108
|
+
}
|
|
109
|
+
} catch (err) {
|
|
110
|
+
if (err.name === 'AbortError') break;
|
|
111
|
+
console.log(`[Groove:Telegram] Poll error: ${err.message}`);
|
|
112
|
+
// Exponential backoff
|
|
113
|
+
await new Promise((r) => setTimeout(r, this._backoff));
|
|
114
|
+
this._backoff = Math.min(this._backoff * 1.5, BACKOFF_MAX);
|
|
115
|
+
}
|
|
116
|
+
}
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
// -------------------------------------------------------------------
|
|
120
|
+
// Update Handlers
|
|
121
|
+
// -------------------------------------------------------------------
|
|
122
|
+
|
|
123
|
+
async _handleUpdate(update) {
|
|
124
|
+
if (update.callback_query) {
|
|
125
|
+
return this._handleCallbackQuery(update.callback_query);
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
const msg = update.message;
|
|
129
|
+
if (!msg?.text) return;
|
|
130
|
+
|
|
131
|
+
// Auto-capture chatId from first message
|
|
132
|
+
if (!this.config.chatId) {
|
|
133
|
+
this.config.chatId = String(msg.chat.id);
|
|
134
|
+
// Persist the captured chatId
|
|
135
|
+
this.daemon.gateways._save(this.config.id);
|
|
136
|
+
console.log(`[Groove:Telegram] Auto-captured chatId: ${this.config.chatId}`);
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
// Only process commands (starts with /)
|
|
140
|
+
if (!msg.text.startsWith('/')) return;
|
|
141
|
+
|
|
142
|
+
const userId = String(msg.from.id);
|
|
143
|
+
const text = msg.text.split('@')[0]; // Remove @botname suffix from group commands
|
|
144
|
+
const [command, ...args] = text.slice(1).split(/\s+/);
|
|
145
|
+
|
|
146
|
+
const response = await this.handleCommand(command, args, userId);
|
|
147
|
+
if (response) {
|
|
148
|
+
await this._reply(msg.chat.id, response.text, response.options);
|
|
149
|
+
}
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
async _handleCallbackQuery(query) {
|
|
153
|
+
const userId = String(query.from.id);
|
|
154
|
+
|
|
155
|
+
// Authorization check
|
|
156
|
+
if (!this._isAuthorized(userId)) {
|
|
157
|
+
await this._answerCallback(query.id, 'Unauthorized');
|
|
158
|
+
return;
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
const data = query.data || '';
|
|
162
|
+
const [action, ...rest] = data.split(':');
|
|
163
|
+
const approvalId = rest.join(':');
|
|
164
|
+
|
|
165
|
+
if (!approvalId) {
|
|
166
|
+
await this._answerCallback(query.id, 'Invalid action');
|
|
167
|
+
return;
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
let responseText;
|
|
171
|
+
try {
|
|
172
|
+
if (action === 'approve') {
|
|
173
|
+
this.daemon.supervisor.approve(approvalId);
|
|
174
|
+
responseText = `\u2705 Approved: ${approvalId}`;
|
|
175
|
+
} else if (action === 'reject') {
|
|
176
|
+
this.daemon.supervisor.reject(approvalId);
|
|
177
|
+
responseText = `\u274c Rejected: ${approvalId}`;
|
|
178
|
+
} else {
|
|
179
|
+
await this._answerCallback(query.id, 'Unknown action');
|
|
180
|
+
return;
|
|
181
|
+
}
|
|
182
|
+
} catch (err) {
|
|
183
|
+
await this._answerCallback(query.id, `Error: ${err.message}`);
|
|
184
|
+
return;
|
|
185
|
+
}
|
|
186
|
+
|
|
187
|
+
// Acknowledge the callback
|
|
188
|
+
await this._answerCallback(query.id, responseText);
|
|
189
|
+
|
|
190
|
+
// Update the original message to reflect the action
|
|
191
|
+
if (query.message) {
|
|
192
|
+
try {
|
|
193
|
+
await this._api('editMessageReplyMarkup', {
|
|
194
|
+
chat_id: query.message.chat.id,
|
|
195
|
+
message_id: query.message.message_id,
|
|
196
|
+
reply_markup: { inline_keyboard: [] }, // Remove buttons
|
|
197
|
+
});
|
|
198
|
+
await this._api('editMessageText', {
|
|
199
|
+
chat_id: query.message.chat.id,
|
|
200
|
+
message_id: query.message.message_id,
|
|
201
|
+
text: `${query.message.text}\n\n${responseText}`,
|
|
202
|
+
parse_mode: 'HTML',
|
|
203
|
+
});
|
|
204
|
+
} catch { /* best effort — message may be too old to edit */ }
|
|
205
|
+
}
|
|
206
|
+
}
|
|
207
|
+
|
|
208
|
+
// -------------------------------------------------------------------
|
|
209
|
+
// Telegram Bot API Helpers
|
|
210
|
+
// -------------------------------------------------------------------
|
|
211
|
+
|
|
212
|
+
async _reply(chatId, text, options = {}) {
|
|
213
|
+
const params = {
|
|
214
|
+
chat_id: chatId,
|
|
215
|
+
text,
|
|
216
|
+
parse_mode: 'HTML',
|
|
217
|
+
disable_web_page_preview: true,
|
|
218
|
+
};
|
|
219
|
+
if (options.approvalId) {
|
|
220
|
+
params.reply_markup = {
|
|
221
|
+
inline_keyboard: [[
|
|
222
|
+
{ text: '\u2705 Approve', callback_data: `approve:${options.approvalId}` },
|
|
223
|
+
{ text: '\u274c Reject', callback_data: `reject:${options.approvalId}` },
|
|
224
|
+
]],
|
|
225
|
+
};
|
|
226
|
+
}
|
|
227
|
+
await this._api('sendMessage', params);
|
|
228
|
+
}
|
|
229
|
+
|
|
230
|
+
async _answerCallback(callbackQueryId, text) {
|
|
231
|
+
await this._api('answerCallbackQuery', {
|
|
232
|
+
callback_query_id: callbackQueryId,
|
|
233
|
+
text,
|
|
234
|
+
});
|
|
235
|
+
}
|
|
236
|
+
|
|
237
|
+
/**
|
|
238
|
+
* Call the Telegram Bot API.
|
|
239
|
+
* @param {string} method — API method name
|
|
240
|
+
* @param {object} [body] — request body (JSON)
|
|
241
|
+
* @param {AbortSignal} [signal] — abort signal
|
|
242
|
+
* @returns {any} — result from Telegram API
|
|
243
|
+
*/
|
|
244
|
+
async _api(method, body, signal) {
|
|
245
|
+
const url = `https://api.telegram.org/bot${this.token}/${method}`;
|
|
246
|
+
const options = {
|
|
247
|
+
method: body ? 'POST' : 'GET',
|
|
248
|
+
signal,
|
|
249
|
+
};
|
|
250
|
+
|
|
251
|
+
if (body) {
|
|
252
|
+
options.headers = { 'Content-Type': 'application/json' };
|
|
253
|
+
options.body = JSON.stringify(body);
|
|
254
|
+
}
|
|
255
|
+
|
|
256
|
+
const res = await fetch(url, options);
|
|
257
|
+
const data = await res.json();
|
|
258
|
+
|
|
259
|
+
if (!data.ok) {
|
|
260
|
+
throw new Error(data.description || `Telegram API error: ${method}`);
|
|
261
|
+
}
|
|
262
|
+
|
|
263
|
+
return data.result;
|
|
264
|
+
}
|
|
265
|
+
}
|
|
@@ -33,6 +33,7 @@ import { Scheduler } from './scheduler.js';
|
|
|
33
33
|
import { FileWatcher } from './filewatcher.js';
|
|
34
34
|
import { TimelineTracker } from './timeline.js';
|
|
35
35
|
import { TerminalManager } from './terminal-pty.js';
|
|
36
|
+
import { GatewayManager } from './gateways/manager.js';
|
|
36
37
|
import { isFirstRun, runFirstTimeSetup, loadConfig, saveConfig, printWelcome } from './firstrun.js';
|
|
37
38
|
|
|
38
39
|
const DEFAULT_PORT = 31415;
|
|
@@ -127,6 +128,7 @@ export class Daemon {
|
|
|
127
128
|
this.scheduler = new Scheduler(this);
|
|
128
129
|
this.fileWatcher = new FileWatcher(this);
|
|
129
130
|
this.terminalManager = new TerminalManager(this);
|
|
131
|
+
this.gateways = new GatewayManager(this);
|
|
130
132
|
|
|
131
133
|
// HTTP + WebSocket server
|
|
132
134
|
this.app = express();
|
|
@@ -296,6 +298,7 @@ export class Daemon {
|
|
|
296
298
|
this.rotator.start();
|
|
297
299
|
this.scheduler.start();
|
|
298
300
|
this.timeline.start();
|
|
301
|
+
this.gateways.start();
|
|
299
302
|
this._startGarbageCollector();
|
|
300
303
|
|
|
301
304
|
// Scan codebase for workspace/structure awareness
|
|
@@ -368,6 +371,7 @@ export class Daemon {
|
|
|
368
371
|
this.state.save();
|
|
369
372
|
|
|
370
373
|
// Stop background services
|
|
374
|
+
await this.gateways.stop();
|
|
371
375
|
this.journalist.stop();
|
|
372
376
|
this.rotator.stop();
|
|
373
377
|
this.scheduler.stop();
|
|
@@ -134,6 +134,61 @@ export function sanitizeForFilename(name) {
|
|
|
134
134
|
return sanitized;
|
|
135
135
|
}
|
|
136
136
|
|
|
137
|
+
const VALID_GATEWAY_TYPES = ['telegram', 'discord', 'slack'];
|
|
138
|
+
const VALID_NOTIFICATION_PRESETS = ['critical', 'lifecycle', 'all', 'custom'];
|
|
139
|
+
|
|
140
|
+
export function validateGatewayConfig(config) {
|
|
141
|
+
if (!config || typeof config !== 'object') {
|
|
142
|
+
throw new Error('Invalid gateway config');
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
if (!config.type || !VALID_GATEWAY_TYPES.includes(config.type)) {
|
|
146
|
+
throw new Error(`Invalid gateway type. Must be one of: ${VALID_GATEWAY_TYPES.join(', ')}`);
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
if (config.chatId !== undefined && config.chatId !== null) {
|
|
150
|
+
if (typeof config.chatId !== 'string' || config.chatId.length > 100) {
|
|
151
|
+
throw new Error('Invalid chatId');
|
|
152
|
+
}
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
if (config.allowedUsers !== undefined && config.allowedUsers !== null) {
|
|
156
|
+
if (!Array.isArray(config.allowedUsers)) {
|
|
157
|
+
throw new Error('allowedUsers must be an array');
|
|
158
|
+
}
|
|
159
|
+
if (config.allowedUsers.length > 50) {
|
|
160
|
+
throw new Error('Too many allowed users (max 50)');
|
|
161
|
+
}
|
|
162
|
+
for (const u of config.allowedUsers) {
|
|
163
|
+
if (typeof u !== 'string' || u.length > 100) {
|
|
164
|
+
throw new Error('Invalid user ID in allowedUsers');
|
|
165
|
+
}
|
|
166
|
+
}
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
if (config.notifications !== undefined && config.notifications !== null) {
|
|
170
|
+
if (typeof config.notifications !== 'object') {
|
|
171
|
+
throw new Error('notifications must be an object');
|
|
172
|
+
}
|
|
173
|
+
if (config.notifications.preset && !VALID_NOTIFICATION_PRESETS.includes(config.notifications.preset)) {
|
|
174
|
+
throw new Error(`Invalid notification preset. Must be one of: ${VALID_NOTIFICATION_PRESETS.join(', ')}`);
|
|
175
|
+
}
|
|
176
|
+
}
|
|
177
|
+
|
|
178
|
+
if (config.commandPermission !== undefined && !['full', 'read-only'].includes(config.commandPermission)) {
|
|
179
|
+
throw new Error('Invalid commandPermission. Must be "full" or "read-only"');
|
|
180
|
+
}
|
|
181
|
+
|
|
182
|
+
return {
|
|
183
|
+
type: config.type,
|
|
184
|
+
enabled: config.enabled !== false,
|
|
185
|
+
chatId: config.chatId || null,
|
|
186
|
+
allowedUsers: (config.allowedUsers || []).map(String),
|
|
187
|
+
notifications: config.notifications || { preset: 'critical' },
|
|
188
|
+
commandPermission: config.commandPermission || 'full',
|
|
189
|
+
};
|
|
190
|
+
}
|
|
191
|
+
|
|
137
192
|
export function escapeMd(text) {
|
|
138
193
|
if (!text) return '';
|
|
139
194
|
// Escape markdown special chars that could break table rendering or inject formatting.
|