aiden-runtime 4.0.2 → 4.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/README.md +11 -7
- package/config/hardware.json +2 -2
- package/dist/api/server.js +50 -52
- package/dist/cli/v4/aidenCLI.js +421 -5
- package/dist/cli/v4/aidenPrompt.js +317 -0
- package/dist/cli/v4/box.js +105 -39
- package/dist/cli/v4/callbacks.js +39 -6
- package/dist/cli/v4/chatSession.js +256 -55
- package/dist/cli/v4/citationFooter.js +97 -0
- package/dist/cli/v4/commands/channel.js +656 -0
- package/dist/cli/v4/commands/clear.js +1 -1
- package/dist/cli/v4/commands/compress.js +1 -1
- package/dist/cli/v4/commands/cron.js +44 -16
- package/dist/cli/v4/commands/fanout.js +236 -0
- package/dist/cli/v4/commands/help.js +15 -4
- package/dist/cli/v4/commands/history.js +84 -0
- package/dist/cli/v4/commands/index.js +16 -1
- package/dist/cli/v4/commands/mcp.js +358 -0
- package/dist/cli/v4/commands/show.js +43 -0
- package/dist/cli/v4/commands/skills.js +169 -4
- package/dist/cli/v4/commands/status.js +84 -0
- package/dist/cli/v4/commands/subagent.js +78 -0
- package/dist/cli/v4/commands/verbose.js +1 -1
- package/dist/cli/v4/commands/voice.js +218 -0
- package/dist/cli/v4/cronCli.js +103 -0
- package/dist/cli/v4/display.js +297 -13
- package/dist/cli/v4/doctor.js +41 -0
- package/dist/cli/v4/envSources.js +105 -0
- package/dist/cli/v4/ghostMatch.js +74 -0
- package/dist/cli/v4/historyStore.js +163 -0
- package/dist/cli/v4/pasteCompression.js +124 -0
- package/dist/cli/v4/pasteIntercept.js +203 -0
- package/dist/cli/v4/replyRenderer.js +209 -0
- package/dist/cli/v4/resizeGuard.js +92 -0
- package/dist/cli/v4/shellInterpolation.js +139 -0
- package/dist/cli/v4/skinEngine.js +21 -1
- package/dist/cli/v4/streamingPrefix.js +121 -0
- package/dist/cli/v4/syntaxHighlight.js +345 -0
- package/dist/cli/v4/table.js +216 -0
- package/dist/cli/v4/themeDetect.js +81 -0
- package/dist/cli/v4/uiBuild.js +74 -0
- package/dist/cli/v4/voiceCli.js +113 -0
- package/dist/cli/v4/voicePromptApi.js +196 -0
- package/dist/core/channels/discord.js +16 -10
- package/dist/core/channels/email.js +13 -9
- package/dist/core/channels/imessage.js +13 -9
- package/dist/core/channels/manager.js +25 -7
- package/dist/core/channels/pdf-extract.js +180 -0
- package/dist/core/channels/photo-vision.js +157 -0
- package/dist/core/channels/signal.js +11 -7
- package/dist/core/channels/slack.js +13 -10
- package/dist/core/channels/telegram-commands.js +154 -0
- package/dist/core/channels/telegram-groups.js +198 -0
- package/dist/core/channels/telegram-rate-limit.js +124 -0
- package/dist/core/channels/telegram.js +1980 -0
- package/dist/core/channels/twilio.js +11 -7
- package/dist/core/channels/webhook.js +9 -5
- package/dist/core/channels/whatsapp.js +15 -11
- package/dist/core/channels/whisper-transcribe.js +163 -0
- package/dist/core/cronManager.js +33 -294
- package/dist/core/gateway.js +29 -8
- package/dist/core/playwrightBridge.js +90 -0
- package/dist/core/v4/aidenAgent.js +35 -0
- package/dist/core/v4/auxiliaryClient.js +2 -2
- package/dist/core/v4/cron/atomicWrite.js +18 -4
- package/dist/core/v4/cron/cronExecute.js +300 -0
- package/dist/core/v4/cron/cronManager.js +502 -0
- package/dist/core/v4/cron/cronState.js +314 -0
- package/dist/core/v4/cron/cronTick.js +90 -0
- package/dist/core/v4/cron/diagnostics.js +104 -0
- package/dist/core/v4/cron/graceWindow.js +79 -0
- package/dist/core/v4/logger/factory.js +110 -0
- package/dist/core/v4/logger/index.js +22 -0
- package/dist/core/v4/logger/logger.js +101 -0
- package/dist/core/v4/logger/sinks/fileSink.js +110 -0
- package/dist/core/v4/logger/sinks/multiSink.js +43 -0
- package/dist/core/v4/logger/sinks/nullSink.js +53 -0
- package/dist/core/v4/logger/sinks/stdSink.js +81 -0
- package/dist/core/v4/mcp/server/diagnostics.js +40 -0
- package/dist/core/v4/mcp/server/skillBridge.js +94 -0
- package/dist/core/v4/mcp/server/stdioServer.js +119 -0
- package/dist/core/v4/mcp/server/toolBridge.js +168 -0
- package/dist/core/v4/platformPaths.js +105 -0
- package/dist/core/v4/providerFallback.js +25 -0
- package/dist/core/v4/skillLoader.js +21 -5
- package/dist/core/v4/skillMining/candidateStore.js +164 -0
- package/dist/core/v4/skillMining/extractorPrompt.js +111 -0
- package/dist/core/v4/skillMining/proposalBuilder.js +139 -0
- package/dist/core/v4/skillMining/skillMiner.js +191 -0
- package/dist/core/v4/skillMining/traceFingerprint.js +51 -0
- package/dist/core/v4/subagent/budget.js +76 -0
- package/dist/core/v4/subagent/diagnostics.js +22 -0
- package/dist/core/v4/subagent/fanout.js +216 -0
- package/dist/core/v4/subagent/merger.js +148 -0
- package/dist/core/v4/subagent/providerRotation.js +54 -0
- package/dist/core/v4/voice/audioStream.js +373 -0
- package/dist/core/v4/voice/cliVoice.js +393 -0
- package/dist/core/v4/voice/diagnostics.js +66 -0
- package/dist/core/v4/voice/ttsStream.js +193 -0
- package/dist/core/version.js +1 -1
- package/dist/core/visionAnalyze.js +291 -90
- package/dist/core/voice/audio.js +61 -5
- package/dist/core/voice/audioBackend.js +134 -0
- package/dist/core/voice/stt.js +61 -6
- package/dist/core/voice/tts.js +19 -3
- package/dist/tools/v4/index.js +32 -1
- package/dist/tools/v4/subagent/subagentFanout.js +166 -0
- package/package.json +11 -2
|
@@ -0,0 +1,154 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
// ============================================================
|
|
3
|
+
// DevOS — Autonomous AI Execution System
|
|
4
|
+
// Copyright (c) 2026 Shiva Deore. All rights reserved.
|
|
5
|
+
// ============================================================
|
|
6
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
7
|
+
exports.TelegramCommandRouter = void 0;
|
|
8
|
+
const logger_1 = require("../v4/logger");
|
|
9
|
+
class TelegramCommandRouter {
|
|
10
|
+
constructor(opts) {
|
|
11
|
+
this.store = opts.store;
|
|
12
|
+
this.log = opts.logger ?? (0, logger_1.noopLogger)();
|
|
13
|
+
this.botUsername = opts.botUsername ?? (() => null);
|
|
14
|
+
this.fetchGroupAdmins = opts.fetchGroupAdmins;
|
|
15
|
+
}
|
|
16
|
+
/**
|
|
17
|
+
* Inspect a Telegram message and decide whether to consume it as a
|
|
18
|
+
* command or pass it through to the agent. Pure dispatch — never
|
|
19
|
+
* sends messages itself; the caller renders the reply text and
|
|
20
|
+
* applies state changes.
|
|
21
|
+
*/
|
|
22
|
+
async route(msg) {
|
|
23
|
+
const text = (msg.text ?? '').trim();
|
|
24
|
+
if (!text.startsWith('/'))
|
|
25
|
+
return { kind: 'agent' };
|
|
26
|
+
const { cmd, args } = this.parseCommand(text);
|
|
27
|
+
if (!cmd)
|
|
28
|
+
return { kind: 'agent' };
|
|
29
|
+
const chatId = String(msg.chat.id);
|
|
30
|
+
const chatType = msg.chat.type;
|
|
31
|
+
const senderId = msg.from?.id ? String(msg.from.id) : '';
|
|
32
|
+
switch (cmd) {
|
|
33
|
+
case '/help':
|
|
34
|
+
case '/start':
|
|
35
|
+
return { kind: 'reply', text: this.helpText(chatType) };
|
|
36
|
+
case '/status':
|
|
37
|
+
return { kind: 'reply', text: '✓ Online' };
|
|
38
|
+
case '/clear': {
|
|
39
|
+
// DMs: anyone may /clear their own chat. Groups: admin only.
|
|
40
|
+
if (chatType !== 'private' && !(await this.isAdmin(senderId, chatId))) {
|
|
41
|
+
this.log.info(`/clear ignored — non-admin in group`, { chatId, senderId });
|
|
42
|
+
return { kind: 'handled' };
|
|
43
|
+
}
|
|
44
|
+
if (chatType !== 'private')
|
|
45
|
+
this.store.recordAdminAction(chatId, 'clear', senderId);
|
|
46
|
+
return { kind: 'cleared' };
|
|
47
|
+
}
|
|
48
|
+
case '/pause': {
|
|
49
|
+
if (!this.requireGroup(chatType) || !(await this.isAdmin(senderId, chatId))) {
|
|
50
|
+
this.log.info(`/pause ignored`, { chatId, senderId, chatType });
|
|
51
|
+
return { kind: 'handled' };
|
|
52
|
+
}
|
|
53
|
+
this.store.setPaused(chatId, true, senderId);
|
|
54
|
+
return { kind: 'paused', groupId: chatId };
|
|
55
|
+
}
|
|
56
|
+
case '/resume': {
|
|
57
|
+
if (!this.requireGroup(chatType) || !(await this.isAdmin(senderId, chatId))) {
|
|
58
|
+
this.log.info(`/resume ignored`, { chatId, senderId, chatType });
|
|
59
|
+
return { kind: 'handled' };
|
|
60
|
+
}
|
|
61
|
+
this.store.setPaused(chatId, false, senderId);
|
|
62
|
+
return { kind: 'resumed', groupId: chatId };
|
|
63
|
+
}
|
|
64
|
+
case '/allowusers': {
|
|
65
|
+
if (!this.requireGroup(chatType) || !(await this.isAdmin(senderId, chatId))) {
|
|
66
|
+
this.log.info(`/allowusers ignored`, { chatId, senderId, chatType });
|
|
67
|
+
return { kind: 'handled' };
|
|
68
|
+
}
|
|
69
|
+
// Comma- or space-separated. `/allowusers reset` clears the list.
|
|
70
|
+
const raw = args.join(' ').trim();
|
|
71
|
+
if (raw === '' || raw === 'reset' || raw === 'clear') {
|
|
72
|
+
this.store.setAllowedUsers(chatId, [], senderId);
|
|
73
|
+
return { kind: 'reply', text: '✓ Cleared user allowlist for this group.' };
|
|
74
|
+
}
|
|
75
|
+
const ids = raw.split(/[,\s]+/).map((s) => s.trim()).filter(Boolean);
|
|
76
|
+
this.store.setAllowedUsers(chatId, ids, senderId);
|
|
77
|
+
return {
|
|
78
|
+
kind: 'reply',
|
|
79
|
+
text: `✓ User allowlist updated: ${ids.length} id(s) — only these users may chat with the bot here.`,
|
|
80
|
+
};
|
|
81
|
+
}
|
|
82
|
+
default:
|
|
83
|
+
// Unknown slash command — fall through to the agent. The
|
|
84
|
+
// model can decide whether to interpret it as natural input.
|
|
85
|
+
return { kind: 'agent' };
|
|
86
|
+
}
|
|
87
|
+
}
|
|
88
|
+
// ── Admin checks ────────────────────────────────────────────────
|
|
89
|
+
/**
|
|
90
|
+
* True when `senderId` is allowed to issue admin-only commands here.
|
|
91
|
+
* Owner takes priority; TELEGRAM_ADMIN_USERS escalates additional
|
|
92
|
+
* ids; TELEGRAM_TRUST_GROUP_ADMINS=true (off by default) accepts
|
|
93
|
+
* Telegram-side group admins.
|
|
94
|
+
*/
|
|
95
|
+
async isAdmin(senderId, chatId) {
|
|
96
|
+
if (!senderId)
|
|
97
|
+
return false;
|
|
98
|
+
const ownerId = (process.env.TELEGRAM_OWNER_ID ?? '').trim();
|
|
99
|
+
if (ownerId && senderId === ownerId)
|
|
100
|
+
return true;
|
|
101
|
+
const adminCsv = (process.env.TELEGRAM_ADMIN_USERS ?? '').trim();
|
|
102
|
+
if (adminCsv) {
|
|
103
|
+
const admins = adminCsv.split(',').map((s) => s.trim()).filter(Boolean);
|
|
104
|
+
if (admins.includes(senderId))
|
|
105
|
+
return true;
|
|
106
|
+
}
|
|
107
|
+
const trustGroupAdmins = (process.env.TELEGRAM_TRUST_GROUP_ADMINS ?? '').toLowerCase() === 'true';
|
|
108
|
+
if (trustGroupAdmins && this.fetchGroupAdmins) {
|
|
109
|
+
try {
|
|
110
|
+
const admins = await this.fetchGroupAdmins(chatId);
|
|
111
|
+
if (admins.includes(senderId))
|
|
112
|
+
return true;
|
|
113
|
+
}
|
|
114
|
+
catch (err) {
|
|
115
|
+
this.log.warn(`getChatAdministrators failed: ${err?.message ?? err}`, { chatId });
|
|
116
|
+
}
|
|
117
|
+
}
|
|
118
|
+
return false;
|
|
119
|
+
}
|
|
120
|
+
// ── Internals ───────────────────────────────────────────────────
|
|
121
|
+
/**
|
|
122
|
+
* Extract `/cmd` and the args list from raw text.
|
|
123
|
+
* Strips `@bot_username` suffixes Telegram appends in groups so
|
|
124
|
+
* `/clear@aiden_test_bot` resolves to `/clear`.
|
|
125
|
+
*/
|
|
126
|
+
parseCommand(raw) {
|
|
127
|
+
const parts = raw.split(/\s+/);
|
|
128
|
+
if (!parts[0] || !parts[0].startsWith('/'))
|
|
129
|
+
return { cmd: null, args: [] };
|
|
130
|
+
const username = (this.botUsername() ?? '').toLowerCase();
|
|
131
|
+
let head = parts[0].toLowerCase();
|
|
132
|
+
if (username && head.endsWith(`@${username}`)) {
|
|
133
|
+
head = head.slice(0, head.length - username.length - 1);
|
|
134
|
+
}
|
|
135
|
+
return { cmd: head, args: parts.slice(1) };
|
|
136
|
+
}
|
|
137
|
+
requireGroup(chatType) {
|
|
138
|
+
return chatType === 'group' || chatType === 'supergroup';
|
|
139
|
+
}
|
|
140
|
+
helpText(chatType) {
|
|
141
|
+
const groupExtras = chatType !== 'private'
|
|
142
|
+
? '`/pause` admin: stop bot in this group\n' +
|
|
143
|
+
'`/resume` admin: resume bot\n' +
|
|
144
|
+
'`/allowusers` admin: restrict who may chat\n'
|
|
145
|
+
: '';
|
|
146
|
+
return ('*Aiden* — your local AI assistant.\n\n' +
|
|
147
|
+
'Send any message (or @mention me in a group) to start. Built-in commands:\n' +
|
|
148
|
+
'`/help` show this message\n' +
|
|
149
|
+
'`/status` bot health check\n' +
|
|
150
|
+
'`/clear` wipe this chat\'s memory\n' +
|
|
151
|
+
groupExtras);
|
|
152
|
+
}
|
|
153
|
+
}
|
|
154
|
+
exports.TelegramCommandRouter = TelegramCommandRouter;
|
|
@@ -0,0 +1,198 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
// ============================================================
|
|
3
|
+
// DevOS — Autonomous AI Execution System
|
|
4
|
+
// Copyright (c) 2026 Shiva Deore. All rights reserved.
|
|
5
|
+
// ============================================================
|
|
6
|
+
var __importDefault = (this && this.__importDefault) || function (mod) {
|
|
7
|
+
return (mod && mod.__esModule) ? mod : { "default": mod };
|
|
8
|
+
};
|
|
9
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
10
|
+
exports.TelegramGroupStore = void 0;
|
|
11
|
+
// core/channels/telegram-groups.ts — Phase v4.1-2.
|
|
12
|
+
//
|
|
13
|
+
// Persistent per-group state for the Telegram channel:
|
|
14
|
+
// - paused — admin /pause stops the bot from replying
|
|
15
|
+
// - allowedUsers — opt-in restriction set by /allowusers
|
|
16
|
+
// - title — group display name (cached for /channel
|
|
17
|
+
// telegram groups list — Telegram's getChat
|
|
18
|
+
// costs an HTTP call per query)
|
|
19
|
+
// - lastMessageAt — wall-clock of the last seen inbound msg
|
|
20
|
+
// - lastAdminAction — when an admin last touched the state
|
|
21
|
+
// - firstSeenAt — when the bot first observed this group
|
|
22
|
+
//
|
|
23
|
+
// State lives at `<aidenRoot>/state/telegram-groups.json`. Atomic
|
|
24
|
+
// writes (tmp → rename) keep the file consistent across process
|
|
25
|
+
// crashes. Loaded once at adapter start; mutations debounce flushes
|
|
26
|
+
// at 1 s so a burst of admin commands doesn't hammer the disk.
|
|
27
|
+
//
|
|
28
|
+
// All diagnostics route through the v4.1-1.3a Logger contract.
|
|
29
|
+
// No console.* anywhere in this module.
|
|
30
|
+
const node_fs_1 = require("node:fs");
|
|
31
|
+
const node_fs_2 = require("node:fs");
|
|
32
|
+
const node_path_1 = __importDefault(require("node:path"));
|
|
33
|
+
const logger_1 = require("../v4/logger");
|
|
34
|
+
/**
|
|
35
|
+
* In-memory + disk-backed store of per-group state. Read paths are
|
|
36
|
+
* always synchronous reads of the in-memory map; mutations schedule
|
|
37
|
+
* a debounced flush so a burst of admin commands collapses to one
|
|
38
|
+
* write.
|
|
39
|
+
*/
|
|
40
|
+
class TelegramGroupStore {
|
|
41
|
+
constructor(opts) {
|
|
42
|
+
this.groups = new Map();
|
|
43
|
+
this.flushTimer = null;
|
|
44
|
+
this.loaded = false;
|
|
45
|
+
this.stateDir = node_path_1.default.join(opts.paths.root, 'state');
|
|
46
|
+
this.statePath = node_path_1.default.join(this.stateDir, 'telegram-groups.json');
|
|
47
|
+
this.log = opts.logger ?? (0, logger_1.noopLogger)();
|
|
48
|
+
this.flushDebounceMs = opts.flushDebounceMs ?? 1000;
|
|
49
|
+
}
|
|
50
|
+
/**
|
|
51
|
+
* Synchronously load on first call. Subsequent calls are no-ops.
|
|
52
|
+
* Failure to read is treated as "fresh state" — better than crashing
|
|
53
|
+
* the adapter on a malformed file.
|
|
54
|
+
*/
|
|
55
|
+
async load() {
|
|
56
|
+
if (this.loaded)
|
|
57
|
+
return;
|
|
58
|
+
this.loaded = true;
|
|
59
|
+
if (!(0, node_fs_2.existsSync)(this.statePath))
|
|
60
|
+
return;
|
|
61
|
+
try {
|
|
62
|
+
const raw = await node_fs_1.promises.readFile(this.statePath, 'utf8');
|
|
63
|
+
const parsed = JSON.parse(raw);
|
|
64
|
+
if (parsed?.version !== 1 || !parsed.groups)
|
|
65
|
+
return;
|
|
66
|
+
for (const [id, g] of Object.entries(parsed.groups)) {
|
|
67
|
+
if (g && typeof g === 'object' && 'groupId' in g) {
|
|
68
|
+
this.groups.set(id, normalizeOnLoad(g));
|
|
69
|
+
}
|
|
70
|
+
}
|
|
71
|
+
this.log.info(`loaded ${this.groups.size} group(s)`);
|
|
72
|
+
}
|
|
73
|
+
catch (err) {
|
|
74
|
+
this.log.warn(`could not load state: ${err?.message ?? err}`);
|
|
75
|
+
}
|
|
76
|
+
}
|
|
77
|
+
/** True when this group is allowed to interact with the bot. */
|
|
78
|
+
isPaused(groupId) {
|
|
79
|
+
return this.groups.get(groupId)?.paused === true;
|
|
80
|
+
}
|
|
81
|
+
/**
|
|
82
|
+
* When an allowed-users list is set on a group, only those users may
|
|
83
|
+
* converse. Empty list (the default) → everyone in the group is OK.
|
|
84
|
+
* Returns true when the user is allowed.
|
|
85
|
+
*/
|
|
86
|
+
userIsAllowed(groupId, userId) {
|
|
87
|
+
const g = this.groups.get(groupId);
|
|
88
|
+
if (!g || g.allowedUsers.length === 0)
|
|
89
|
+
return true;
|
|
90
|
+
return g.allowedUsers.includes(userId);
|
|
91
|
+
}
|
|
92
|
+
/** Public accessor for /channel telegram groups list. */
|
|
93
|
+
list() {
|
|
94
|
+
return Array.from(this.groups.values()).sort((a, b) => (b.lastMessageAt ?? 0) - (a.lastMessageAt ?? 0));
|
|
95
|
+
}
|
|
96
|
+
get(groupId) {
|
|
97
|
+
return this.groups.get(groupId);
|
|
98
|
+
}
|
|
99
|
+
/** Record an inbound observation — bumps lastMessageAt + caches title. */
|
|
100
|
+
observeMessage(groupId, opts) {
|
|
101
|
+
const existing = this.groups.get(groupId);
|
|
102
|
+
const now = Date.now();
|
|
103
|
+
if (existing) {
|
|
104
|
+
existing.lastMessageAt = now;
|
|
105
|
+
if (opts.title && existing.title !== opts.title)
|
|
106
|
+
existing.title = opts.title;
|
|
107
|
+
}
|
|
108
|
+
else {
|
|
109
|
+
this.groups.set(groupId, {
|
|
110
|
+
groupId,
|
|
111
|
+
title: opts.title,
|
|
112
|
+
paused: false,
|
|
113
|
+
allowedUsers: [],
|
|
114
|
+
firstSeenAt: now,
|
|
115
|
+
lastMessageAt: now,
|
|
116
|
+
});
|
|
117
|
+
}
|
|
118
|
+
this.scheduleFlush();
|
|
119
|
+
}
|
|
120
|
+
setPaused(groupId, paused, actor) {
|
|
121
|
+
const g = this.ensureGroup(groupId);
|
|
122
|
+
g.paused = paused;
|
|
123
|
+
g.lastAdminAction = { actor, cmd: paused ? 'pause' : 'resume', at: Date.now() };
|
|
124
|
+
this.scheduleFlush();
|
|
125
|
+
}
|
|
126
|
+
setAllowedUsers(groupId, userIds, actor) {
|
|
127
|
+
const g = this.ensureGroup(groupId);
|
|
128
|
+
g.allowedUsers = [...new Set(userIds.map(s => s.trim()).filter(Boolean))];
|
|
129
|
+
g.lastAdminAction = { actor, cmd: 'allowusers', at: Date.now() };
|
|
130
|
+
this.scheduleFlush();
|
|
131
|
+
}
|
|
132
|
+
recordAdminAction(groupId, cmd, actor) {
|
|
133
|
+
const g = this.ensureGroup(groupId);
|
|
134
|
+
g.lastAdminAction = { actor, cmd, at: Date.now() };
|
|
135
|
+
this.scheduleFlush();
|
|
136
|
+
}
|
|
137
|
+
/** Force-flush + clear debounce timer (called on adapter teardown). */
|
|
138
|
+
async flushNow() {
|
|
139
|
+
if (this.flushTimer) {
|
|
140
|
+
clearTimeout(this.flushTimer);
|
|
141
|
+
this.flushTimer = null;
|
|
142
|
+
}
|
|
143
|
+
await this.writeFile();
|
|
144
|
+
}
|
|
145
|
+
// ── Internals ─────────────────────────────────────────────────
|
|
146
|
+
ensureGroup(groupId) {
|
|
147
|
+
let g = this.groups.get(groupId);
|
|
148
|
+
if (!g) {
|
|
149
|
+
g = {
|
|
150
|
+
groupId,
|
|
151
|
+
paused: false,
|
|
152
|
+
allowedUsers: [],
|
|
153
|
+
firstSeenAt: Date.now(),
|
|
154
|
+
};
|
|
155
|
+
this.groups.set(groupId, g);
|
|
156
|
+
}
|
|
157
|
+
return g;
|
|
158
|
+
}
|
|
159
|
+
scheduleFlush() {
|
|
160
|
+
if (this.flushTimer)
|
|
161
|
+
return;
|
|
162
|
+
this.flushTimer = setTimeout(() => {
|
|
163
|
+
this.flushTimer = null;
|
|
164
|
+
this.writeFile().catch((err) => this.log.warn(`flush failed: ${err?.message ?? err}`));
|
|
165
|
+
}, this.flushDebounceMs);
|
|
166
|
+
if (typeof this.flushTimer.unref === 'function')
|
|
167
|
+
this.flushTimer.unref();
|
|
168
|
+
}
|
|
169
|
+
async writeFile() {
|
|
170
|
+
const payload = {
|
|
171
|
+
version: 1,
|
|
172
|
+
groups: Object.fromEntries(this.groups),
|
|
173
|
+
};
|
|
174
|
+
await node_fs_1.promises.mkdir(this.stateDir, { recursive: true });
|
|
175
|
+
const tmp = `${this.statePath}.${process.pid}.tmp`;
|
|
176
|
+
await node_fs_1.promises.writeFile(tmp, JSON.stringify(payload, null, 2), 'utf8');
|
|
177
|
+
await node_fs_1.promises.rename(tmp, this.statePath);
|
|
178
|
+
}
|
|
179
|
+
}
|
|
180
|
+
exports.TelegramGroupStore = TelegramGroupStore;
|
|
181
|
+
/**
|
|
182
|
+
* Defensive load-time normaliser — older state files may be missing
|
|
183
|
+
* fields we've since added; fall back to safe defaults instead of
|
|
184
|
+
* propagating undefined into the rest of the adapter.
|
|
185
|
+
*/
|
|
186
|
+
function normalizeOnLoad(raw) {
|
|
187
|
+
return {
|
|
188
|
+
groupId: String(raw.groupId),
|
|
189
|
+
title: typeof raw.title === 'string' ? raw.title : undefined,
|
|
190
|
+
paused: raw.paused === true,
|
|
191
|
+
allowedUsers: Array.isArray(raw.allowedUsers) ? raw.allowedUsers.map(String) : [],
|
|
192
|
+
firstSeenAt: typeof raw.firstSeenAt === 'number' ? raw.firstSeenAt : Date.now(),
|
|
193
|
+
lastMessageAt: typeof raw.lastMessageAt === 'number' ? raw.lastMessageAt : undefined,
|
|
194
|
+
lastAdminAction: raw.lastAdminAction && typeof raw.lastAdminAction === 'object'
|
|
195
|
+
? raw.lastAdminAction
|
|
196
|
+
: undefined,
|
|
197
|
+
};
|
|
198
|
+
}
|
|
@@ -0,0 +1,124 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
// ============================================================
|
|
3
|
+
// DevOS — Autonomous AI Execution System
|
|
4
|
+
// Copyright (c) 2026 Shiva Deore. All rights reserved.
|
|
5
|
+
// ============================================================
|
|
6
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
7
|
+
exports.TelegramRateLimiter = void 0;
|
|
8
|
+
// core/channels/telegram-rate-limit.ts — Phase v4.1-2.
|
|
9
|
+
//
|
|
10
|
+
// Sliding-window per-user rate limiter for the Telegram channel.
|
|
11
|
+
//
|
|
12
|
+
// Design:
|
|
13
|
+
// - In-memory only — resets on aiden restart. The threat model is
|
|
14
|
+
// "stop a single chatter from burning the bot owner's quota in
|
|
15
|
+
// one sitting"; a restart-bypass that costs the abuser an entire
|
|
16
|
+
// restart-window's worth of messages is acceptable.
|
|
17
|
+
// - Single user-id keyspace across all chats — a spammer can't dodge
|
|
18
|
+
// by hopping between groups.
|
|
19
|
+
// - 1-minute sliding window, default 5 messages. Both knobs are
|
|
20
|
+
// configurable via env (`TELEGRAM_USER_RATE_LIMIT`,
|
|
21
|
+
// `TELEGRAM_USER_RATE_WINDOW_MS`) so an op who hosts the bot for a
|
|
22
|
+
// bigger community can bump them without code changes.
|
|
23
|
+
// - `shouldThrottle(userId)` is the only consumer-facing method —
|
|
24
|
+
// records the access *and* reports whether the caller should drop
|
|
25
|
+
// the message. One lookup, one mutation, one decision.
|
|
26
|
+
// - A coalescing sweeper trims stale buckets every 5 minutes so an
|
|
27
|
+
// adversary can't blow the heap by hammering with fresh user ids.
|
|
28
|
+
//
|
|
29
|
+
// `TelegramRateLimiter` accepts an injected logger from the unified
|
|
30
|
+
// `Logger` contract (Phase v4.1-1.3a) — diagnostics file-only, REPL
|
|
31
|
+
// stays sacred. No console.* anywhere in this module.
|
|
32
|
+
const logger_1 = require("../v4/logger");
|
|
33
|
+
const DEFAULT_LIMIT = 5;
|
|
34
|
+
const DEFAULT_WINDOW_MS = 60000;
|
|
35
|
+
const SWEEP_INTERVAL_MS = 5 * 60 * 1000; // prune buckets idle > sweep+window
|
|
36
|
+
class TelegramRateLimiter {
|
|
37
|
+
constructor(opts = {}) {
|
|
38
|
+
this.buckets = new Map();
|
|
39
|
+
this.sweepTimer = null;
|
|
40
|
+
this.limit = readPositiveInt(process.env.TELEGRAM_USER_RATE_LIMIT, opts.limit ?? DEFAULT_LIMIT);
|
|
41
|
+
this.windowMs = readPositiveInt(process.env.TELEGRAM_USER_RATE_WINDOW_MS, opts.windowMs ?? DEFAULT_WINDOW_MS);
|
|
42
|
+
this.now = opts.now ?? Date.now;
|
|
43
|
+
this.log = opts.logger ?? (0, logger_1.noopLogger)();
|
|
44
|
+
}
|
|
45
|
+
/**
|
|
46
|
+
* Record an attempted message + return true when the caller should
|
|
47
|
+
* drop it. The bucket is updated on every call (even ones that get
|
|
48
|
+
* throttled), so a sustained over-limit user stays throttled until
|
|
49
|
+
* the oldest message in their window ages out.
|
|
50
|
+
*/
|
|
51
|
+
shouldThrottle(userId) {
|
|
52
|
+
if (!userId)
|
|
53
|
+
return false;
|
|
54
|
+
const cutoff = this.now() - this.windowMs;
|
|
55
|
+
const bucket = this.buckets.get(userId) ?? [];
|
|
56
|
+
// Drop expired entries from the front (oldest-first list).
|
|
57
|
+
while (bucket.length > 0 && bucket[0] <= cutoff)
|
|
58
|
+
bucket.shift();
|
|
59
|
+
if (bucket.length >= this.limit) {
|
|
60
|
+
// Don't append on throttle so the window can age out cleanly —
|
|
61
|
+
// appending here would reset the user's clock every time they
|
|
62
|
+
// try to spam, locking them in indefinitely.
|
|
63
|
+
this.buckets.set(userId, bucket);
|
|
64
|
+
this.log.warn(`rate-limited`, { userId, count: bucket.length, limit: this.limit });
|
|
65
|
+
return true;
|
|
66
|
+
}
|
|
67
|
+
bucket.push(this.now());
|
|
68
|
+
this.buckets.set(userId, bucket);
|
|
69
|
+
this.scheduleSweep();
|
|
70
|
+
return false;
|
|
71
|
+
}
|
|
72
|
+
/** Test / diagnostic accessor — current bucket size for a user. */
|
|
73
|
+
getCount(userId) {
|
|
74
|
+
const bucket = this.buckets.get(userId);
|
|
75
|
+
if (!bucket)
|
|
76
|
+
return 0;
|
|
77
|
+
const cutoff = this.now() - this.windowMs;
|
|
78
|
+
return bucket.filter((t) => t > cutoff).length;
|
|
79
|
+
}
|
|
80
|
+
/** Stop the sweep timer (called on adapter teardown). */
|
|
81
|
+
dispose() {
|
|
82
|
+
if (this.sweepTimer) {
|
|
83
|
+
clearInterval(this.sweepTimer);
|
|
84
|
+
this.sweepTimer = null;
|
|
85
|
+
}
|
|
86
|
+
}
|
|
87
|
+
// ── Internal: prune fully-expired buckets so memory doesn't drift. ──
|
|
88
|
+
scheduleSweep() {
|
|
89
|
+
if (this.sweepTimer)
|
|
90
|
+
return;
|
|
91
|
+
this.sweepTimer = setInterval(() => this.sweep(), SWEEP_INTERVAL_MS);
|
|
92
|
+
// Don't keep the process alive just for the sweeper.
|
|
93
|
+
if (typeof this.sweepTimer.unref === 'function')
|
|
94
|
+
this.sweepTimer.unref();
|
|
95
|
+
}
|
|
96
|
+
sweep() {
|
|
97
|
+
const cutoff = this.now() - this.windowMs;
|
|
98
|
+
let pruned = 0;
|
|
99
|
+
for (const [userId, bucket] of this.buckets) {
|
|
100
|
+
const filtered = bucket.filter((t) => t > cutoff);
|
|
101
|
+
if (filtered.length === 0) {
|
|
102
|
+
this.buckets.delete(userId);
|
|
103
|
+
pruned += 1;
|
|
104
|
+
}
|
|
105
|
+
else {
|
|
106
|
+
this.buckets.set(userId, filtered);
|
|
107
|
+
}
|
|
108
|
+
}
|
|
109
|
+
if (pruned > 0)
|
|
110
|
+
this.log.debug(`swept ${pruned} stale buckets`);
|
|
111
|
+
}
|
|
112
|
+
}
|
|
113
|
+
exports.TelegramRateLimiter = TelegramRateLimiter;
|
|
114
|
+
/**
|
|
115
|
+
* Parse a positive integer env var with a fallback. Negative / NaN /
|
|
116
|
+
* empty values fall back to the default — better than crashing the
|
|
117
|
+
* adapter because someone fat-fingered an env var.
|
|
118
|
+
*/
|
|
119
|
+
function readPositiveInt(envValue, fallback) {
|
|
120
|
+
if (typeof envValue !== 'string' || envValue.trim() === '')
|
|
121
|
+
return fallback;
|
|
122
|
+
const n = Number.parseInt(envValue, 10);
|
|
123
|
+
return Number.isFinite(n) && n > 0 ? n : fallback;
|
|
124
|
+
}
|