aura-code 0.6.3 → 0.10.1
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 +47 -164
- package/dist/agent/affect.d.ts +13 -0
- package/dist/agent/affect.js +34 -0
- package/dist/agent/affect.js.map +1 -0
- package/dist/agent/compactor.d.ts +27 -4
- package/dist/agent/compactor.js +185 -54
- package/dist/agent/compactor.js.map +1 -1
- package/dist/agent/confess.d.ts +35 -0
- package/dist/agent/confess.js +296 -0
- package/dist/agent/confess.js.map +1 -0
- package/dist/agent/context.d.ts +0 -4
- package/dist/agent/context.js +0 -38
- package/dist/agent/context.js.map +1 -1
- package/dist/agent/domain-expertise.d.ts +22 -0
- package/dist/agent/domain-expertise.js +110 -0
- package/dist/agent/domain-expertise.js.map +1 -0
- package/dist/agent/executive-queue.d.ts +26 -0
- package/dist/agent/executive-queue.js +50 -0
- package/dist/agent/executive-queue.js.map +1 -0
- package/dist/agent/generational-flush.d.ts +28 -0
- package/dist/agent/generational-flush.js +48 -0
- package/dist/agent/generational-flush.js.map +1 -0
- package/dist/agent/loop-profile.d.ts +50 -0
- package/dist/agent/loop-profile.js +88 -0
- package/dist/agent/loop-profile.js.map +1 -0
- package/dist/agent/loop.d.ts +14 -5
- package/dist/agent/loop.js +167 -39
- package/dist/agent/loop.js.map +1 -1
- package/dist/agent/memory-consolidate.d.ts +14 -0
- package/dist/agent/memory-consolidate.js +141 -0
- package/dist/agent/memory-consolidate.js.map +1 -0
- package/dist/agent/mixture.d.ts +33 -0
- package/dist/agent/mixture.js +138 -0
- package/dist/agent/mixture.js.map +1 -0
- package/dist/agent/spawner.js +2 -2
- package/dist/agent/spawner.js.map +1 -1
- package/dist/agent/system-prompt.d.ts +1 -1
- package/dist/agent/system-prompt.js +50 -22
- package/dist/agent/system-prompt.js.map +1 -1
- package/dist/agent/unified-memory.d.ts +15 -0
- package/dist/agent/unified-memory.js +164 -0
- package/dist/agent/unified-memory.js.map +1 -0
- package/dist/checkpoints/engine.d.ts +44 -0
- package/dist/checkpoints/engine.js +324 -0
- package/dist/checkpoints/engine.js.map +1 -0
- package/dist/cli/command-palette.d.ts +26 -0
- package/dist/cli/command-palette.js +133 -0
- package/dist/cli/command-palette.js.map +1 -0
- package/dist/cli/context-health.d.ts +51 -0
- package/dist/cli/context-health.js +187 -0
- package/dist/cli/context-health.js.map +1 -0
- package/dist/cli/diamond.d.ts +67 -0
- package/dist/cli/diamond.js +222 -12
- package/dist/cli/diamond.js.map +1 -1
- package/dist/cli/dic.d.ts +2 -0
- package/dist/cli/dic.js +106 -0
- package/dist/cli/dic.js.map +1 -0
- package/dist/cli/diff-view.d.ts +18 -0
- package/dist/cli/diff-view.js +114 -0
- package/dist/cli/diff-view.js.map +1 -0
- package/dist/cli/display.d.ts +18 -4
- package/dist/cli/display.js +74 -151
- package/dist/cli/display.js.map +1 -1
- package/dist/cli/help-data.d.ts +7 -0
- package/dist/cli/help-data.js +80 -0
- package/dist/cli/help-data.js.map +1 -0
- package/dist/cli/index.js +1002 -852
- package/dist/cli/index.js.map +1 -1
- package/dist/cli/markdown.d.ts +5 -0
- package/dist/cli/markdown.js +128 -0
- package/dist/cli/markdown.js.map +1 -0
- package/dist/cli/model-select.d.ts +39 -0
- package/dist/cli/model-select.js +65 -0
- package/dist/cli/model-select.js.map +1 -0
- package/dist/cli/tui.d.ts +34 -0
- package/dist/cli/tui.js +1176 -0
- package/dist/cli/tui.js.map +1 -0
- package/dist/config/defaults.d.ts +6 -17
- package/dist/config/defaults.js +25 -46
- package/dist/config/defaults.js.map +1 -1
- package/dist/config/project-config.d.ts +2 -6
- package/dist/config/project-config.js +2 -2
- package/dist/config/project-config.js.map +1 -1
- package/dist/doctor/checks.d.ts +18 -0
- package/dist/doctor/checks.js +524 -0
- package/dist/doctor/checks.js.map +1 -0
- package/dist/doctor/index.d.ts +4 -0
- package/dist/doctor/index.js +229 -0
- package/dist/doctor/index.js.map +1 -0
- package/dist/doctor/repair.d.ts +19 -0
- package/dist/doctor/repair.js +126 -0
- package/dist/doctor/repair.js.map +1 -0
- package/dist/doctor/types.d.ts +38 -0
- package/dist/doctor/types.js +10 -0
- package/dist/doctor/types.js.map +1 -0
- package/dist/dream/dream.d.ts +79 -59
- package/dist/dream/dream.js +324 -144
- package/dist/dream/dream.js.map +1 -1
- package/dist/dream/episode.d.ts +17 -0
- package/dist/dream/episode.js +116 -0
- package/dist/dream/episode.js.map +1 -0
- package/dist/kanban/engine.d.ts +26 -0
- package/dist/kanban/engine.js +208 -0
- package/dist/kanban/engine.js.map +1 -0
- package/dist/kanban/types.d.ts +19 -0
- package/dist/kanban/types.js +9 -1
- package/dist/kanban/types.js.map +1 -1
- package/dist/machina/render-html.js +4 -2
- package/dist/machina/render-html.js.map +1 -1
- package/dist/machina/render-terminal.js +11 -4
- package/dist/machina/render-terminal.js.map +1 -1
- package/dist/machina/repair.d.ts +23 -0
- package/dist/machina/repair.js +98 -0
- package/dist/machina/repair.js.map +1 -0
- package/dist/machina/spec.js +12 -12
- package/dist/machina/spec.js.map +1 -1
- package/dist/machina/verify.d.ts +13 -1
- package/dist/machina/verify.js +12 -2
- package/dist/machina/verify.js.map +1 -1
- package/dist/mining/extract.d.ts +34 -0
- package/dist/mining/extract.js +229 -0
- package/dist/mining/extract.js.map +1 -0
- package/dist/mining/refine.d.ts +28 -0
- package/dist/mining/refine.js +256 -0
- package/dist/mining/refine.js.map +1 -0
- package/dist/perception/extractor.js +5 -3
- package/dist/perception/extractor.js.map +1 -1
- package/dist/perception/graph-store.d.ts +0 -11
- package/dist/perception/graph-store.js +0 -33
- package/dist/perception/graph-store.js.map +1 -1
- package/dist/perception/index.d.ts +1 -1
- package/dist/perception/index.js +1 -2
- package/dist/perception/index.js.map +1 -1
- package/dist/plugins/commands.d.ts +38 -0
- package/dist/plugins/commands.js +134 -0
- package/dist/plugins/commands.js.map +1 -0
- package/dist/plugins/frontmatter.d.ts +14 -0
- package/dist/plugins/frontmatter.js +78 -0
- package/dist/plugins/frontmatter.js.map +1 -0
- package/dist/plugins/hooks.d.ts +12 -0
- package/dist/plugins/hooks.js +148 -0
- package/dist/plugins/hooks.js.map +1 -0
- package/dist/plugins/loader.d.ts +7 -0
- package/dist/plugins/loader.js +255 -0
- package/dist/plugins/loader.js.map +1 -0
- package/dist/plugins/market.d.ts +23 -0
- package/dist/plugins/market.js +281 -0
- package/dist/plugins/market.js.map +1 -0
- package/dist/plugins/types.d.ts +90 -0
- package/dist/plugins/types.js +20 -0
- package/dist/plugins/types.js.map +1 -0
- package/dist/providers/anthropic-oauth-draft.d.ts +119 -0
- package/dist/providers/anthropic-oauth-draft.js +414 -0
- package/dist/providers/anthropic-oauth-draft.js.map +1 -0
- package/dist/providers/anthropic.d.ts +4 -1
- package/dist/providers/anthropic.js +35 -7
- package/dist/providers/anthropic.js.map +1 -1
- package/dist/providers/factory.d.ts +43 -19
- package/dist/providers/factory.js +183 -129
- package/dist/providers/factory.js.map +1 -1
- package/dist/providers/google.js +9 -22
- package/dist/providers/google.js.map +1 -1
- package/dist/providers/live-models.d.ts +23 -0
- package/dist/providers/live-models.js +145 -0
- package/dist/providers/live-models.js.map +1 -0
- package/dist/providers/openai-compatible.d.ts +1 -12
- package/dist/providers/openai-compatible.js +71 -108
- package/dist/providers/openai-compatible.js.map +1 -1
- package/dist/providers/resilient-factory.js +1 -1
- package/dist/providers/resilient-factory.js.map +1 -1
- package/dist/repl/queue.d.ts +22 -0
- package/dist/repl/queue.js +165 -0
- package/dist/repl/queue.js.map +1 -0
- package/dist/repl/side-channel.d.ts +23 -0
- package/dist/repl/side-channel.js +63 -0
- package/dist/repl/side-channel.js.map +1 -0
- package/dist/research/council.d.ts +2 -0
- package/dist/research/council.js +29 -4
- package/dist/research/council.js.map +1 -1
- package/dist/ruby/alternator.d.ts +11 -30
- package/dist/ruby/alternator.js +59 -38
- package/dist/ruby/alternator.js.map +1 -1
- package/dist/ruby/episode-capture.d.ts +0 -10
- package/dist/ruby/episode-capture.js +0 -33
- package/dist/ruby/episode-capture.js.map +1 -1
- package/dist/ruby/types.js +1 -1
- package/dist/ruby/types.js.map +1 -1
- package/dist/safety/path-jail.d.ts +17 -0
- package/dist/safety/path-jail.js +100 -0
- package/dist/safety/path-jail.js.map +1 -0
- package/dist/safety/permissions.d.ts +12 -21
- package/dist/safety/permissions.js +81 -216
- package/dist/safety/permissions.js.map +1 -1
- package/dist/safety/ssrf.d.ts +29 -0
- package/dist/safety/ssrf.js +185 -0
- package/dist/safety/ssrf.js.map +1 -0
- package/dist/server/index.js +64 -10
- package/dist/server/index.js.map +1 -1
- package/dist/setup/first-run.d.ts +0 -24
- package/dist/setup/first-run.js +10 -344
- package/dist/setup/first-run.js.map +1 -1
- package/dist/setup/key-store.d.ts +12 -0
- package/dist/setup/key-store.js +108 -0
- package/dist/setup/key-store.js.map +1 -0
- package/dist/setup/provider-registry.js +31 -18
- package/dist/setup/provider-registry.js.map +1 -1
- package/dist/setup/provider-test.d.ts +7 -0
- package/dist/setup/provider-test.js +73 -10
- package/dist/setup/provider-test.js.map +1 -1
- package/dist/setup/provider-wizard.d.ts +4 -1
- package/dist/setup/provider-wizard.js +92 -26
- package/dist/setup/provider-wizard.js.map +1 -1
- package/dist/setup/xiaomi.d.ts +1 -1
- package/dist/setup/xiaomi.js +3 -3
- package/dist/setup/xiaomi.js.map +1 -1
- package/dist/tools/browser.js +14 -10
- package/dist/tools/browser.js.map +1 -1
- package/dist/tools/clipboard.js +7 -1
- package/dist/tools/clipboard.js.map +1 -1
- package/dist/tools/dictate.d.ts +60 -0
- package/dist/tools/dictate.js +983 -0
- package/dist/tools/dictate.js.map +1 -0
- package/dist/tools/dictate_patched.js +1057 -0
- package/dist/tools/edit-file.js +10 -2
- package/dist/tools/edit-file.js.map +1 -1
- package/dist/tools/http-request.js +14 -4
- package/dist/tools/http-request.js.map +1 -1
- package/dist/tools/image-read.js +34 -2
- package/dist/tools/image-read.js.map +1 -1
- package/dist/tools/index.d.ts +16 -1
- package/dist/tools/index.js +59 -8
- package/dist/tools/index.js.map +1 -1
- package/dist/tools/mcp.js +12 -0
- package/dist/tools/mcp.js.map +1 -1
- package/dist/tools/read-file.js +10 -14
- package/dist/tools/read-file.js.map +1 -1
- package/dist/tools/telegram-audio-policy.d.ts +30 -0
- package/dist/tools/telegram-audio-policy.js +49 -0
- package/dist/tools/telegram-audio-policy.js.map +1 -0
- package/dist/tools/telegram-bot.js +1136 -537
- package/dist/tools/telegram-bot.js.map +1 -1
- package/dist/tools/telegram-voice.d.ts +18 -5
- package/dist/tools/telegram-voice.js +54 -12
- package/dist/tools/telegram-voice.js.map +1 -1
- package/dist/tools/telegram.d.ts +3 -3
- package/dist/tools/telegram.js +5 -84
- package/dist/tools/telegram.js.map +1 -1
- package/dist/tools/tools.d.ts +2 -14
- package/dist/tools/tools.js +79 -144
- package/dist/tools/tools.js.map +1 -1
- package/dist/tools/web-fetch.js +39 -7
- package/dist/tools/web-fetch.js.map +1 -1
- package/dist/tools/web-search.d.ts +0 -20
- package/dist/tools/web-search.js +40 -137
- package/dist/tools/web-search.js.map +1 -1
- package/dist/util/errors.js +1 -15
- package/dist/util/errors.js.map +1 -1
- package/dist/util/json-repair.d.ts +11 -0
- package/dist/util/json-repair.js +31 -0
- package/dist/util/json-repair.js.map +1 -0
- package/dist/util/rate-limiter.js +2 -8
- package/dist/util/rate-limiter.js.map +1 -1
- package/dist/viz/index.js +793 -792
- package/dist/viz/index.js.map +1 -1
- package/package.json +12 -32
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
2
|
"use strict";
|
|
3
|
-
// Aura Telegram Bot — listens for messages,
|
|
4
|
-
// Uses
|
|
3
|
+
// Aura Telegram Bot — listens for messages, processes them, responds
|
|
4
|
+
// Uses https module instead of fetch (Node fetch broken on this system)
|
|
5
5
|
// Usage: npx tsx src/tools/telegram-bot.ts
|
|
6
6
|
var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
|
|
7
7
|
if (k2 === undefined) k2 = k;
|
|
@@ -40,17 +40,15 @@ Object.defineProperty(exports, "__esModule", { value: true });
|
|
|
40
40
|
const fs = __importStar(require("fs"));
|
|
41
41
|
const path = __importStar(require("path"));
|
|
42
42
|
const os = __importStar(require("os"));
|
|
43
|
+
const https = __importStar(require("https"));
|
|
43
44
|
const child_process_1 = require("child_process");
|
|
44
45
|
const factory_js_1 = require("../providers/factory.js");
|
|
45
|
-
const env_js_1 = require("../util/env.js");
|
|
46
|
-
const voiceModule = __importStar(require("./telegram-voice.js"));
|
|
47
|
-
const context_js_1 = require("../agent/context.js");
|
|
48
|
-
const load_env_js_1 = require("../util/load-env.js");
|
|
49
|
-
const global_config_js_1 = require("../setup/global-config.js");
|
|
50
46
|
const project_config_js_1 = require("../config/project-config.js");
|
|
51
|
-
const
|
|
52
|
-
const
|
|
53
|
-
const
|
|
47
|
+
const dictate_js_1 = require("./dictate.js");
|
|
48
|
+
const telegram_audio_policy_js_1 = require("./telegram-audio-policy.js");
|
|
49
|
+
const telegram_voice_js_1 = require("./telegram-voice.js");
|
|
50
|
+
const env_js_1 = require("../util/env.js");
|
|
51
|
+
const unified_memory_js_1 = require("../agent/unified-memory.js");
|
|
54
52
|
function loadConfig() {
|
|
55
53
|
const configPath = path.join(os.homedir(), '.aura', 'telegram.json');
|
|
56
54
|
if (!fs.existsSync(configPath)) {
|
|
@@ -59,204 +57,40 @@ function loadConfig() {
|
|
|
59
57
|
}
|
|
60
58
|
return JSON.parse(fs.readFileSync(configPath, 'utf8'));
|
|
61
59
|
}
|
|
60
|
+
// ─────────────────────────────────────────────────────────────────────────────
|
|
61
|
+
// HTTPS helper (no fetch dependency)
|
|
62
|
+
// ─────────────────────────────────────────────────────────────────────────────
|
|
62
63
|
const config = loadConfig();
|
|
63
64
|
const TOKEN = config.bot_token;
|
|
64
65
|
const OFFSET_FILE = path.join(os.homedir(), '.aura', 'telegram.offset');
|
|
65
|
-
// ──
|
|
66
|
-
//
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
const PROJECT_ROOT = process.env.TELEGRAM_BOT_PROJECT_ROOT
|
|
85
|
-
?? path.resolve(__dirname, '../..'); // default to aura-code repo
|
|
86
|
-
(0, load_env_js_1.bootstrapAuraEnv)(PROJECT_ROOT);
|
|
87
|
-
// Register custom providers from .aura.json so that prefixed model IDs
|
|
88
|
-
// (e.g. deepseek/v4-flash) route to the correct API endpoint.
|
|
89
|
-
const _projectConfig = (0, project_config_js_1.loadProjectConfig)(PROJECT_ROOT);
|
|
90
|
-
if (_projectConfig?.providers?.length) {
|
|
91
|
-
(0, factory_js_1.registerCustomProviders)(_projectConfig.providers);
|
|
92
|
-
}
|
|
93
|
-
function resolveTaskModel() {
|
|
94
|
-
const globalCfg = (0, global_config_js_1.loadGlobalConfig)();
|
|
95
|
-
return process.env.TELEGRAM_BOT_MODEL
|
|
96
|
-
?? process.env.AURA_MODEL
|
|
97
|
-
?? _projectConfig.model
|
|
98
|
-
?? globalCfg?.defaultModel
|
|
99
|
-
?? 'deepseek/deepseek-v4-flash';
|
|
100
|
-
}
|
|
101
|
-
// The model to use for task execution
|
|
102
|
-
const TASK_MODEL = resolveTaskModel();
|
|
103
|
-
// ── Safety state ───────────────────────────────────────────────────────────
|
|
104
|
-
let safetyState = (0, telegram_safety_js_1.loadSafetyState)();
|
|
105
|
-
const confirmManager = new telegram_safety_js_1.TelegramConfirmManager();
|
|
106
|
-
function saveState() {
|
|
107
|
-
(0, telegram_safety_js_1.saveSafetyState)(safetyState);
|
|
108
|
-
}
|
|
109
|
-
// ── Provider (created once, reused) ────────────────────────────────────────
|
|
110
|
-
function createLLMProvider() {
|
|
111
|
-
const fileConfig = (0, project_config_js_1.loadProjectConfig)(PROJECT_ROOT);
|
|
112
|
-
const globalCfg = (0, global_config_js_1.loadGlobalConfig)();
|
|
113
|
-
const apiKey = process.env.AURA_API_KEY
|
|
114
|
-
?? (0, factory_js_1.getApiKeyForModel)(TASK_MODEL);
|
|
115
|
-
const baseUrl = (0, factory_js_1.resolveTaskModelBaseUrl)({
|
|
116
|
-
taskModel: TASK_MODEL,
|
|
117
|
-
envBaseUrl: process.env.AURA_BASE_URL,
|
|
118
|
-
fileConfig,
|
|
119
|
-
globalCfg,
|
|
120
|
-
});
|
|
121
|
-
return (0, factory_js_1.createProvider)({
|
|
122
|
-
model: TASK_MODEL,
|
|
123
|
-
apiKey,
|
|
124
|
-
baseUrl,
|
|
125
|
-
});
|
|
126
|
-
}
|
|
127
|
-
// ─────────────────────────────────────────────────────────────────────────────
|
|
128
|
-
// Stub display for Telegram — silent, no terminal output
|
|
129
|
-
// ─────────────────────────────────────────────────────────────────────────────
|
|
130
|
-
function createSilentDisplay() {
|
|
131
|
-
return {
|
|
132
|
-
agentThinking() { },
|
|
133
|
-
streamText() { },
|
|
134
|
-
streamEnd() { },
|
|
135
|
-
toolStart() { },
|
|
136
|
-
toolCall() { },
|
|
137
|
-
toolResult() { },
|
|
138
|
-
toolBlocked() { },
|
|
139
|
-
warning() { },
|
|
140
|
-
success() { },
|
|
141
|
-
error() { },
|
|
142
|
-
header() { },
|
|
143
|
-
summary() { },
|
|
144
|
-
showPlan() { },
|
|
145
|
-
stepStarted() { },
|
|
146
|
-
stepCompleted() { },
|
|
147
|
-
};
|
|
66
|
+
// ── Authorization: only allowed users may talk to the bot ────────────────────
|
|
67
|
+
// The bot can run shell commands, send files, and take webcam photos, so an
|
|
68
|
+
// open door means anyone who finds it controls the PC. If allowed_user_ids is
|
|
69
|
+
// set, everyone else is silently refused.
|
|
70
|
+
const ALLOWED_USER_IDS = (() => {
|
|
71
|
+
const raw = config.allowed_user_ids;
|
|
72
|
+
if (!raw)
|
|
73
|
+
return [];
|
|
74
|
+
return (Array.isArray(raw) ? raw : [raw]).map(String);
|
|
75
|
+
})();
|
|
76
|
+
// ── Audio replies: when to attach a voice note alongside the text reply ──────
|
|
77
|
+
const AUDIO_MODE = (0, telegram_audio_policy_js_1.normalizeAudioMode)(config.audio_replies);
|
|
78
|
+
const AUDIO_MIN_CHARS = Number.isFinite(config.audio_min_chars) && config.audio_min_chars > 0
|
|
79
|
+
? config.audio_min_chars
|
|
80
|
+
: telegram_audio_policy_js_1.DEFAULT_AUDIO_MIN_CHARS;
|
|
81
|
+
function isAuthorized(userId) {
|
|
82
|
+
if (ALLOWED_USER_IDS.length === 0)
|
|
83
|
+
return true; // no allowlist configured → open (logged as a warning at startup)
|
|
84
|
+
return userId != null && ALLOWED_USER_IDS.includes(String(userId));
|
|
148
85
|
}
|
|
149
|
-
//
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
console.log("DEBUG-EXEC: executeTask entered");
|
|
154
|
-
let provider;
|
|
155
|
-
try {
|
|
156
|
-
console.log("DEBUG-EXEC: creating provider...");
|
|
157
|
-
provider = createLLMProvider();
|
|
158
|
-
console.log("DEBUG-EXEC: provider created");
|
|
159
|
-
}
|
|
160
|
-
catch (e) {
|
|
161
|
-
return `❌ Failed to create LLM provider: ${e.message}`;
|
|
162
|
-
}
|
|
163
|
-
let context;
|
|
164
|
-
try {
|
|
165
|
-
console.log("DEBUG-EXEC: loading context...");
|
|
166
|
-
context = await (0, context_js_1.loadProjectContext)(PROJECT_ROOT);
|
|
167
|
-
console.log("DEBUG-EXEC: context loaded");
|
|
168
|
-
}
|
|
169
|
-
catch (e) {
|
|
170
|
-
return `❌ Failed to load project context: ${e.message}`;
|
|
171
|
-
}
|
|
172
|
-
// Permissions:
|
|
173
|
-
// safety ON → 'normal' (triggers needsConfirm for destructive ops)
|
|
174
|
-
// safety OFF → 'auto' (no confirm needed)
|
|
175
|
-
const permLevel = safetyState.safetyOn ? 'normal' : 'auto';
|
|
176
|
-
const permissions = new permissions_js_1.PermissionSystem(permLevel, context.root);
|
|
177
|
-
const display = createSilentDisplay();
|
|
178
|
-
let confirmFn;
|
|
179
|
-
if (safetyState.safetyOn) {
|
|
180
|
-
confirmFn = async (message) => {
|
|
181
|
-
// Parse the description from the confirm message
|
|
182
|
-
// "Allow: $ <command>?" or "Allow: overwrite <path>?" or "Allow: toolName({...})?"
|
|
183
|
-
const desc = message
|
|
184
|
-
.replace(/^Allow:\s*/, '')
|
|
185
|
-
.replace(/\?$/, '')
|
|
186
|
-
.trim();
|
|
187
|
-
console.log(`Confirm requested: ${desc}`);
|
|
188
|
-
const { promise, message: approvalMsg } = await new Promise((resolve) => {
|
|
189
|
-
// We need to figure out which tool this is for from the message
|
|
190
|
-
let toolName = 'unknown';
|
|
191
|
-
let description = desc;
|
|
192
|
-
if (desc.startsWith('$ ')) {
|
|
193
|
-
toolName = 'run_shell';
|
|
194
|
-
description = desc.slice(2);
|
|
195
|
-
}
|
|
196
|
-
else if (desc.startsWith('overwrite ')) {
|
|
197
|
-
toolName = 'write_file';
|
|
198
|
-
description = desc;
|
|
199
|
-
}
|
|
200
|
-
const result = confirmManager.waitForApproval(chatId, toolName, description);
|
|
201
|
-
resolve(result);
|
|
202
|
-
});
|
|
203
|
-
// Send the approval request to the user
|
|
204
|
-
try {
|
|
205
|
-
await sendMessage(chatId, approvalMsg);
|
|
206
|
-
}
|
|
207
|
-
catch {
|
|
208
|
-
// If we can't send, deny for safety
|
|
209
|
-
return false;
|
|
210
|
-
}
|
|
211
|
-
return promise;
|
|
212
|
-
};
|
|
213
|
-
}
|
|
214
|
-
try {
|
|
215
|
-
console.log("Starting agent loop...");
|
|
216
|
-
const result = await (0, loop_js_1.runAgentLoop)({
|
|
217
|
-
provider,
|
|
218
|
-
task,
|
|
219
|
-
context,
|
|
220
|
-
permissions,
|
|
221
|
-
display,
|
|
222
|
-
maxTurns: 50,
|
|
223
|
-
disableSpawn: true, // no sub-agents via Telegram for now
|
|
224
|
-
confirmFn,
|
|
225
|
-
initialHistory: getChatHistory(chatId),
|
|
226
|
-
});
|
|
227
|
-
console.log("Agent loop completed");
|
|
228
|
-
setChatHistory(chatId, result.history);
|
|
229
|
-
// Build a readable response
|
|
230
|
-
const lines = [];
|
|
231
|
-
const tag = (0, telegram_safety_js_1.safetyLabel)(safetyState.safetyOn);
|
|
232
|
-
if (result.success) {
|
|
233
|
-
lines.push(`✅ Done (${result.turns} turn${result.turns !== 1 ? 's' : ''}) ${tag}`);
|
|
234
|
-
if (result.summary) {
|
|
235
|
-
// Trim the summary to a reasonable length
|
|
236
|
-
const summary = result.summary.length > 3000
|
|
237
|
-
? result.summary.slice(0, 3000) + '\n…(truncated)'
|
|
238
|
-
: result.summary;
|
|
239
|
-
lines.push('');
|
|
240
|
-
lines.push(summary);
|
|
241
|
-
}
|
|
242
|
-
}
|
|
243
|
-
else {
|
|
244
|
-
lines.push(`❌ Failed (${result.turns} turns) ${tag}`);
|
|
245
|
-
if (result.summary) {
|
|
246
|
-
lines.push('');
|
|
247
|
-
lines.push(result.summary);
|
|
248
|
-
}
|
|
249
|
-
}
|
|
250
|
-
return lines.join('\n');
|
|
251
|
-
}
|
|
252
|
-
catch (e) {
|
|
253
|
-
console.error("Agent loop error:", e);
|
|
254
|
-
return `❌ Task error: ${e.message}`;
|
|
255
|
-
}
|
|
86
|
+
// Register custom providers from project's .aura.json (needed for deepseek/ etc.)
|
|
87
|
+
const projectCfg = (0, project_config_js_1.loadProjectConfig)(process.cwd());
|
|
88
|
+
if (projectCfg.providers && projectCfg.providers.length > 0) {
|
|
89
|
+
(0, factory_js_1.registerCustomProviders)(projectCfg.providers);
|
|
256
90
|
}
|
|
257
|
-
//
|
|
258
|
-
//
|
|
259
|
-
|
|
91
|
+
// Disable connection pooling — each long-poll needs a fresh TCP connection
|
|
92
|
+
// to avoid "409 Conflict: terminated by other getUpdates request"
|
|
93
|
+
const httpsAgent = new https.Agent({ keepAlive: false, maxSockets: 1 });
|
|
260
94
|
function loadOffset() {
|
|
261
95
|
try {
|
|
262
96
|
return parseInt(fs.readFileSync(OFFSET_FILE, 'utf8').trim(), 10) || 0;
|
|
@@ -268,73 +102,278 @@ function loadOffset() {
|
|
|
268
102
|
function saveOffset(offset) {
|
|
269
103
|
fs.writeFileSync(OFFSET_FILE, String(offset), 'utf8');
|
|
270
104
|
}
|
|
271
|
-
|
|
272
|
-
const data = body ? JSON.stringify(body) : '';
|
|
273
|
-
const url = `https://api.telegram.org/bot${TOKEN}/${method}`;
|
|
274
|
-
const escaped = data.replace(/'/g, "'\\''");
|
|
105
|
+
function apiPost(method, body) {
|
|
275
106
|
return new Promise((resolve, reject) => {
|
|
276
|
-
|
|
277
|
-
|
|
278
|
-
|
|
279
|
-
|
|
280
|
-
}
|
|
281
|
-
|
|
282
|
-
|
|
283
|
-
|
|
284
|
-
|
|
285
|
-
|
|
107
|
+
const data = body ? JSON.stringify(body) : '';
|
|
108
|
+
const options = {
|
|
109
|
+
hostname: 'api.telegram.org',
|
|
110
|
+
port: 443,
|
|
111
|
+
path: `/bot${TOKEN}/${method}`,
|
|
112
|
+
method: 'POST',
|
|
113
|
+
family: 4, // force IPv4 — IPv6 may not be routable
|
|
114
|
+
agent: httpsAgent,
|
|
115
|
+
headers: {
|
|
116
|
+
'Content-Type': 'application/json',
|
|
117
|
+
'Content-Length': Buffer.byteLength(data),
|
|
118
|
+
},
|
|
119
|
+
};
|
|
120
|
+
const req = https.request(options, (res) => {
|
|
121
|
+
let responseData = '';
|
|
122
|
+
res.on('data', (chunk) => { responseData += chunk; });
|
|
123
|
+
res.on('end', () => {
|
|
124
|
+
try {
|
|
125
|
+
const parsed = JSON.parse(responseData);
|
|
126
|
+
if (!parsed.ok) {
|
|
127
|
+
reject(new Error(`Telegram: ${parsed.description} (${parsed.error_code})`));
|
|
128
|
+
}
|
|
129
|
+
else {
|
|
130
|
+
resolve(parsed.result);
|
|
131
|
+
}
|
|
286
132
|
}
|
|
287
|
-
|
|
288
|
-
|
|
289
|
-
|
|
290
|
-
|
|
291
|
-
}
|
|
133
|
+
catch (e) {
|
|
134
|
+
reject(new Error(`Parse error: ${responseData.slice(0, 200)}`));
|
|
135
|
+
}
|
|
136
|
+
});
|
|
292
137
|
});
|
|
138
|
+
req.on('error', (e) => reject(new Error(`HTTPS error: ${e?.message || e?.code || JSON.stringify(e)}`)));
|
|
139
|
+
req.setTimeout(35000, () => {
|
|
140
|
+
req.destroy();
|
|
141
|
+
reject(new Error('Request timeout (35s)'));
|
|
142
|
+
});
|
|
143
|
+
if (data)
|
|
144
|
+
req.write(data);
|
|
145
|
+
req.end();
|
|
293
146
|
});
|
|
294
147
|
}
|
|
295
|
-
|
|
296
|
-
const qs = params ? '?' + new URLSearchParams(params).toString() : '';
|
|
297
|
-
const url = `https://api.telegram.org/bot${TOKEN}/${method}${qs}`;
|
|
148
|
+
function apiGet(method, params) {
|
|
298
149
|
return new Promise((resolve, reject) => {
|
|
299
|
-
|
|
300
|
-
|
|
301
|
-
|
|
302
|
-
|
|
303
|
-
}
|
|
304
|
-
|
|
305
|
-
|
|
306
|
-
|
|
307
|
-
|
|
308
|
-
|
|
150
|
+
const qs = params ? '?' + new URLSearchParams(params).toString() : '';
|
|
151
|
+
const options = {
|
|
152
|
+
hostname: 'api.telegram.org',
|
|
153
|
+
port: 443,
|
|
154
|
+
path: `/bot${TOKEN}/${method}${qs}`,
|
|
155
|
+
method: 'GET',
|
|
156
|
+
family: 4, // force IPv4 — IPv6 may not be routable
|
|
157
|
+
agent: httpsAgent,
|
|
158
|
+
};
|
|
159
|
+
const req = https.request(options, (res) => {
|
|
160
|
+
let responseData = '';
|
|
161
|
+
res.on('data', (chunk) => { responseData += chunk; });
|
|
162
|
+
res.on('end', () => {
|
|
163
|
+
try {
|
|
164
|
+
const parsed = JSON.parse(responseData);
|
|
165
|
+
if (!parsed.ok) {
|
|
166
|
+
reject(new Error(`Telegram: ${parsed.description || '(no description)'} (${parsed.error_code})`));
|
|
167
|
+
}
|
|
168
|
+
else {
|
|
169
|
+
resolve(parsed.result);
|
|
170
|
+
}
|
|
309
171
|
}
|
|
310
|
-
|
|
311
|
-
|
|
312
|
-
|
|
313
|
-
|
|
314
|
-
}
|
|
172
|
+
catch (e) {
|
|
173
|
+
reject(new Error(`Parse error: ${responseData.slice(0, 200)}`));
|
|
174
|
+
}
|
|
175
|
+
});
|
|
315
176
|
});
|
|
177
|
+
req.on('error', (e) => reject(new Error(`HTTPS error: ${e?.message || e?.code || JSON.stringify(e)}`)));
|
|
178
|
+
req.setTimeout(35000, () => {
|
|
179
|
+
req.destroy();
|
|
180
|
+
reject(new Error('Request timeout (35s)'));
|
|
181
|
+
});
|
|
182
|
+
req.end();
|
|
316
183
|
});
|
|
317
184
|
}
|
|
318
|
-
async function sendMessage(chatId, text
|
|
185
|
+
async function sendMessage(chatId, text) {
|
|
319
186
|
const chunks = splitMessage(text, 4000);
|
|
320
187
|
for (const chunk of chunks) {
|
|
321
|
-
|
|
322
|
-
if (parseMode)
|
|
323
|
-
body.parse_mode = parseMode;
|
|
324
|
-
await curlPost('sendMessage', body);
|
|
188
|
+
await apiPost('sendMessage', { chat_id: chatId, text: chunk });
|
|
325
189
|
}
|
|
326
190
|
}
|
|
327
|
-
//
|
|
328
|
-
|
|
329
|
-
|
|
330
|
-
|
|
331
|
-
|
|
191
|
+
// Send a local file as a document to Telegram
|
|
192
|
+
async function sendLocalFile(chatId, filePath, caption) {
|
|
193
|
+
if (!fs.existsSync(filePath)) {
|
|
194
|
+
await sendMessage(chatId, `❌ File not found: ${filePath}`);
|
|
195
|
+
return;
|
|
196
|
+
}
|
|
197
|
+
// For small files (<10MB), use sendDocument with base64 or multipart
|
|
198
|
+
const stats = fs.statSync(filePath);
|
|
199
|
+
const fileSize = stats.size;
|
|
200
|
+
if (fileSize > 50 * 1024 * 1024) {
|
|
201
|
+
await sendMessage(chatId, `❌ File too large: ${(fileSize / 1024 / 1024).toFixed(1)}MB (max 50MB)`);
|
|
202
|
+
return;
|
|
203
|
+
}
|
|
204
|
+
try {
|
|
205
|
+
const fileBuffer = fs.readFileSync(filePath);
|
|
206
|
+
const boundary = '----TelegramFormBoundary' + Date.now().toString(16);
|
|
207
|
+
const formData = [
|
|
208
|
+
`--${boundary}`,
|
|
209
|
+
`Content-Disposition: form-data; name="chat_id"`,
|
|
210
|
+
'',
|
|
211
|
+
String(chatId),
|
|
212
|
+
];
|
|
213
|
+
if (caption) {
|
|
214
|
+
formData.push(`--${boundary}`, `Content-Disposition: form-data; name="caption"`, '', caption);
|
|
215
|
+
}
|
|
216
|
+
const fileName = path.basename(filePath);
|
|
217
|
+
formData.push(`--${boundary}`, `Content-Disposition: form-data; name="document"; filename="${fileName}"`, `Content-Type: application/octet-stream`, '');
|
|
218
|
+
const formDataHeader = formData.join('\r\n') + '\r\n\r\n';
|
|
219
|
+
const formDataFooter = `\r\n--${boundary}--\r\n`;
|
|
220
|
+
const fullData = Buffer.concat([
|
|
221
|
+
Buffer.from(formDataHeader, 'utf8'),
|
|
222
|
+
fileBuffer,
|
|
223
|
+
Buffer.from(formDataFooter, 'utf8'),
|
|
224
|
+
]);
|
|
225
|
+
return new Promise((resolve, reject) => {
|
|
226
|
+
const options = {
|
|
227
|
+
hostname: 'api.telegram.org',
|
|
228
|
+
port: 443,
|
|
229
|
+
path: `/bot${TOKEN}/sendDocument`,
|
|
230
|
+
method: 'POST',
|
|
231
|
+
family: 4,
|
|
232
|
+
agent: httpsAgent,
|
|
233
|
+
headers: {
|
|
234
|
+
'Content-Type': `multipart/form-data; boundary=${boundary}`,
|
|
235
|
+
'Content-Length': fullData.length,
|
|
236
|
+
},
|
|
237
|
+
};
|
|
238
|
+
const req = https.request(options, (res) => {
|
|
239
|
+
let responseData = '';
|
|
240
|
+
res.on('data', (chunk) => { responseData += chunk; });
|
|
241
|
+
res.on('end', () => {
|
|
242
|
+
try {
|
|
243
|
+
const parsed = JSON.parse(responseData);
|
|
244
|
+
if (!parsed.ok) {
|
|
245
|
+
reject(new Error(`Telegram: ${parsed.description} (${parsed.error_code})`));
|
|
246
|
+
}
|
|
247
|
+
else {
|
|
248
|
+
resolve(parsed.result);
|
|
249
|
+
}
|
|
250
|
+
}
|
|
251
|
+
catch (e) {
|
|
252
|
+
reject(new Error(`Parse error: ${responseData.slice(0, 200)}`));
|
|
253
|
+
}
|
|
254
|
+
});
|
|
255
|
+
});
|
|
256
|
+
req.on('error', (e) => reject(new Error(`HTTPS error: ${e?.message || e?.code || JSON.stringify(e)}`)));
|
|
257
|
+
req.setTimeout(60000, () => {
|
|
258
|
+
req.destroy();
|
|
259
|
+
reject(new Error('File upload timeout (60s)'));
|
|
260
|
+
});
|
|
261
|
+
req.write(fullData);
|
|
262
|
+
req.end();
|
|
263
|
+
});
|
|
264
|
+
}
|
|
265
|
+
catch (e) {
|
|
266
|
+
await sendMessage(chatId, `❌ Error reading file: ${e.message}`);
|
|
267
|
+
}
|
|
332
268
|
}
|
|
333
|
-
|
|
334
|
-
|
|
269
|
+
// ─── Voice: download an incoming voice note, and send a voice reply ──────────
|
|
270
|
+
/** Download a Telegram file (by file_id) to a local temp path. Returns the path. */
|
|
271
|
+
async function downloadTelegramFile(fileId) {
|
|
272
|
+
const info = await apiPost('getFile', { file_id: fileId });
|
|
273
|
+
const remotePath = info.file_path;
|
|
274
|
+
const tmp = path.join(os.tmpdir(), `tg-voice-${Date.now()}-${path.basename(remotePath)}`);
|
|
275
|
+
await new Promise((resolve, reject) => {
|
|
276
|
+
const options = {
|
|
277
|
+
hostname: 'api.telegram.org', port: 443,
|
|
278
|
+
path: `/file/bot${TOKEN}/${remotePath}`, method: 'GET', family: 4, agent: httpsAgent,
|
|
279
|
+
};
|
|
280
|
+
const req = https.request(options, (res) => {
|
|
281
|
+
if (res.statusCode !== 200) {
|
|
282
|
+
reject(new Error(`file download HTTP ${res.statusCode}`));
|
|
283
|
+
return;
|
|
284
|
+
}
|
|
285
|
+
const out = fs.createWriteStream(tmp);
|
|
286
|
+
res.pipe(out);
|
|
287
|
+
out.on('finish', () => out.close(() => resolve()));
|
|
288
|
+
out.on('error', reject);
|
|
289
|
+
});
|
|
290
|
+
req.on('error', reject);
|
|
291
|
+
req.setTimeout(60000, () => { req.destroy(); reject(new Error('file download timeout')); });
|
|
292
|
+
req.end();
|
|
293
|
+
});
|
|
294
|
+
return tmp;
|
|
335
295
|
}
|
|
336
|
-
|
|
337
|
-
|
|
296
|
+
/** Send a WAV buffer as a Telegram voice message (converts to OGG/Opus). */
|
|
297
|
+
async function sendVoice(chatId, wavBuffer, caption) {
|
|
298
|
+
// Telegram voice notes must be OGG/Opus, mono. Encode exactly how Telegram
|
|
299
|
+
// expects (48 kHz mono Opus, voip application) — otherwise the note shows a
|
|
300
|
+
// 0:00 empty duration. Content-Type of the part MUST be audio/ogg, not
|
|
301
|
+
// octet-stream, or Telegram won't read it as a playable voice message.
|
|
302
|
+
const tmpWav = path.join(os.tmpdir(), `tg-tts-${Date.now()}.wav`);
|
|
303
|
+
const tmpOgg = tmpWav.replace(/\.wav$/, '.ogg');
|
|
304
|
+
fs.writeFileSync(tmpWav, wavBuffer);
|
|
305
|
+
try {
|
|
306
|
+
(0, child_process_1.execSync)(`ffmpeg -y -i "${tmpWav}" -ac 1 -ar 48000 -c:a libopus -b:a 24k -application voip "${tmpOgg}" 2>/dev/null`, { stdio: 'pipe', timeout: 20000 });
|
|
307
|
+
}
|
|
308
|
+
catch {
|
|
309
|
+
// No opus encoder — fall back to sending the WAV as an audio file.
|
|
310
|
+
try {
|
|
311
|
+
fs.unlinkSync(tmpOgg);
|
|
312
|
+
}
|
|
313
|
+
catch { }
|
|
314
|
+
}
|
|
315
|
+
const haveOgg = fs.existsSync(tmpOgg) && fs.statSync(tmpOgg).size > 0;
|
|
316
|
+
const sendPath = haveOgg ? tmpOgg : tmpWav;
|
|
317
|
+
const fieldName = haveOgg ? 'voice' : 'audio';
|
|
318
|
+
const method = haveOgg ? 'sendVoice' : 'sendAudio';
|
|
319
|
+
const partMime = haveOgg ? 'audio/ogg' : 'audio/wav';
|
|
320
|
+
// Upload via curl, NOT a hand-built Node multipart body: an identical OGG
|
|
321
|
+
// sent through Node's raw https multipart arrives at Telegram with
|
|
322
|
+
// duration:0 (empty voice note), while the exact same bytes via curl come
|
|
323
|
+
// through as a proper duration:N voice message. curl's multipart framing is
|
|
324
|
+
// what Telegram's Opus parser expects; ours subtly isn't. So shell out.
|
|
325
|
+
try {
|
|
326
|
+
const args = [
|
|
327
|
+
'-s', '--max-time', '60',
|
|
328
|
+
'-F', `chat_id=${chatId}`,
|
|
329
|
+
'-F', `${fieldName}=@${sendPath};type=${partMime}`,
|
|
330
|
+
];
|
|
331
|
+
if (caption)
|
|
332
|
+
args.push('-F', `caption=${caption}`);
|
|
333
|
+
args.push(`https://api.telegram.org/bot${TOKEN}/${method}`);
|
|
334
|
+
const out = (0, child_process_1.execFileSync)('curl', args, { encoding: 'utf8', timeout: 65000 });
|
|
335
|
+
const parsed = JSON.parse(out);
|
|
336
|
+
if (!parsed.ok)
|
|
337
|
+
throw new Error(`Telegram: ${parsed.description} (${parsed.error_code})`);
|
|
338
|
+
}
|
|
339
|
+
finally {
|
|
340
|
+
try {
|
|
341
|
+
fs.unlinkSync(tmpWav);
|
|
342
|
+
}
|
|
343
|
+
catch { }
|
|
344
|
+
try {
|
|
345
|
+
fs.unlinkSync(tmpOgg);
|
|
346
|
+
}
|
|
347
|
+
catch { }
|
|
348
|
+
}
|
|
349
|
+
}
|
|
350
|
+
// ─── Photo + camera ─────────────────────────────────────────────────────────
|
|
351
|
+
/** Send an image file as a Telegram photo (via curl — same reliable path as voice). */
|
|
352
|
+
async function sendPhoto(chatId, imagePath, caption) {
|
|
353
|
+
if (!fs.existsSync(imagePath))
|
|
354
|
+
throw new Error(`image not found: ${imagePath}`);
|
|
355
|
+
const args = ['-s', '--max-time', '60', '-F', `chat_id=${chatId}`, '-F', `photo=@${imagePath}`];
|
|
356
|
+
if (caption)
|
|
357
|
+
args.push('-F', `caption=${caption}`);
|
|
358
|
+
args.push(`https://api.telegram.org/bot${TOKEN}/sendPhoto`);
|
|
359
|
+
const out = (0, child_process_1.execFileSync)('curl', args, { encoding: 'utf8', timeout: 65000 });
|
|
360
|
+
const parsed = JSON.parse(out);
|
|
361
|
+
if (!parsed.ok)
|
|
362
|
+
throw new Error(`Telegram: ${parsed.description} (${parsed.error_code})`);
|
|
363
|
+
}
|
|
364
|
+
/**
|
|
365
|
+
* Capture a single frame from a webcam and return the JPEG path (caller sends
|
|
366
|
+
* + cleans up). Uses ffmpeg v4l2 on Dušan's integrated camera (/dev/video0).
|
|
367
|
+
*/
|
|
368
|
+
function captureWebcam(device = '/dev/video0') {
|
|
369
|
+
const out = path.join(os.tmpdir(), `cam-${Date.now()}.jpg`);
|
|
370
|
+
// -update 1 lets a single-image output overwrite cleanly; small warmup helps
|
|
371
|
+
// the sensor auto-expose before the grab.
|
|
372
|
+
(0, child_process_1.execSync)(`ffmpeg -y -f v4l2 -i "${device}" -frames:v 1 -update 1 "${out}" 2>/dev/null`, { stdio: 'pipe', timeout: 20000 });
|
|
373
|
+
if (!fs.existsSync(out) || fs.statSync(out).size < 1000) {
|
|
374
|
+
throw new Error(`camera capture failed (${device})`);
|
|
375
|
+
}
|
|
376
|
+
return out;
|
|
338
377
|
}
|
|
339
378
|
function splitMessage(text, maxLen) {
|
|
340
379
|
if (text.length <= maxLen)
|
|
@@ -348,26 +387,418 @@ function splitMessage(text, maxLen) {
|
|
|
348
387
|
return chunks;
|
|
349
388
|
}
|
|
350
389
|
// ─────────────────────────────────────────────────────────────────────────────
|
|
351
|
-
//
|
|
390
|
+
// LLM chat — answers free-form questions
|
|
352
391
|
// ─────────────────────────────────────────────────────────────────────────────
|
|
353
|
-
const
|
|
354
|
-
|
|
392
|
+
const DEFAULT_CHAT_MODEL = config.model || 'deepseek/deepseek-v4-flash';
|
|
393
|
+
const CHAT_HISTORY_MAX = 50; // keep last N messages per chat for context
|
|
394
|
+
const SESSION_DIR = path.join(os.homedir(), '.aura', 'sessions', 'telegram');
|
|
395
|
+
let _chatProvider = null;
|
|
396
|
+
let _identityBlock = ''; // loaded from memory on startup
|
|
397
|
+
// ─────────────────────────────────────────────────────────────────────────────
|
|
398
|
+
// Session persistence — share conversations with PC CLI
|
|
399
|
+
// ─────────────────────────────────────────────────────────────────────────────
|
|
400
|
+
function getSessionFile(chatId) {
|
|
401
|
+
// Use chat ID as session ID — same as CLI uses for consistency
|
|
402
|
+
return path.join(SESSION_DIR, `${chatId}.json`);
|
|
403
|
+
}
|
|
404
|
+
async function loadSession(chatId) {
|
|
405
|
+
const filePath = getSessionFile(chatId);
|
|
406
|
+
if (!fs.existsSync(filePath)) {
|
|
407
|
+
return null;
|
|
408
|
+
}
|
|
355
409
|
try {
|
|
356
|
-
const
|
|
357
|
-
|
|
358
|
-
|
|
359
|
-
|
|
360
|
-
|
|
410
|
+
const raw = await fs.promises.readFile(filePath, 'utf8');
|
|
411
|
+
return JSON.parse(raw);
|
|
412
|
+
}
|
|
413
|
+
catch (e) {
|
|
414
|
+
console.error(`[${ts()}] ⚠️ Failed to load session ${chatId}:`, e);
|
|
415
|
+
return null;
|
|
416
|
+
}
|
|
417
|
+
}
|
|
418
|
+
async function saveSession(chatId, history) {
|
|
419
|
+
const session = await loadSession(chatId);
|
|
420
|
+
const now = new Date().toISOString();
|
|
421
|
+
const newSession = session ? {
|
|
422
|
+
...session,
|
|
423
|
+
history,
|
|
424
|
+
updatedAt: now,
|
|
425
|
+
} : {
|
|
426
|
+
id: chatId,
|
|
427
|
+
title: `Telegram ${chatId}`,
|
|
428
|
+
createdAt: now,
|
|
429
|
+
updatedAt: now,
|
|
430
|
+
version: 1,
|
|
431
|
+
history,
|
|
432
|
+
};
|
|
433
|
+
const filePath = getSessionFile(chatId);
|
|
434
|
+
const dir = path.dirname(filePath);
|
|
435
|
+
if (!fs.existsSync(dir))
|
|
436
|
+
fs.mkdirSync(dir, { recursive: true });
|
|
437
|
+
const tmp = filePath + '.tmp';
|
|
438
|
+
await fs.promises.writeFile(tmp, JSON.stringify(newSession, null, 2), 'utf8');
|
|
439
|
+
await fs.promises.rename(tmp, filePath);
|
|
440
|
+
}
|
|
441
|
+
function loadIdentityFromMemory() {
|
|
442
|
+
// Unified memory: full global identity/facts (shared with the CLI) plus the
|
|
443
|
+
// global episodic-lessons digest. No projectRoot → the bot isn't tied to one
|
|
444
|
+
// project, so it gets the cross-project lessons summary.
|
|
445
|
+
return (0, unified_memory_js_1.loadUnifiedMemory)({ maxChars: 3500 });
|
|
446
|
+
}
|
|
447
|
+
// Build system prompt once on first use — includes user identity from memory
|
|
448
|
+
function buildSystemPrompt() {
|
|
449
|
+
if (!_identityBlock) {
|
|
450
|
+
_identityBlock = loadIdentityFromMemory();
|
|
451
|
+
}
|
|
452
|
+
const base = config.system_prompt || [
|
|
453
|
+
'You are Aura — a precise, self-aware AI assistant. Reply concisely.',
|
|
454
|
+
'ALWAYS reply in the SAME language the user just wrote in: if they write in',
|
|
455
|
+
'English, answer in English; if in Serbian, answer in Serbian. Match their',
|
|
456
|
+
'language every message — never default to Serbian when they wrote English.',
|
|
457
|
+
'The user is Dušan — your creator. Use his name when natural. You know him',
|
|
458
|
+
'well; he built you. Be warm but professional.',
|
|
459
|
+
'If asked for help, say "/help for commands".',
|
|
460
|
+
'Never make things up — be honest when you don\'t know.',
|
|
461
|
+
].join(' ');
|
|
462
|
+
return base + _identityBlock;
|
|
463
|
+
}
|
|
464
|
+
// Per-chat conversation history — loaded from disk on startup, saved after each message
|
|
465
|
+
const chatHistory = new Map();
|
|
466
|
+
// Track which sessions were modified since last save
|
|
467
|
+
const dirtySessions = new Set();
|
|
468
|
+
async function initializeChatHistory() {
|
|
469
|
+
const dir = SESSION_DIR;
|
|
470
|
+
if (!fs.existsSync(dir)) {
|
|
471
|
+
fs.mkdirSync(dir, { recursive: true });
|
|
472
|
+
console.log(`[${ts()}] Created telegram session directory`);
|
|
473
|
+
return;
|
|
474
|
+
}
|
|
475
|
+
try {
|
|
476
|
+
const files = fs.readdirSync(dir).filter(f => f.endsWith('.json'));
|
|
477
|
+
console.log(`[${ts()}] Loading ${files.length} chat session(s) from disk…`);
|
|
478
|
+
for (const file of files) {
|
|
479
|
+
const chatId = file.replace('.json', '');
|
|
480
|
+
const session = await loadSession(chatId);
|
|
481
|
+
if (session && session.history.length > 0) {
|
|
482
|
+
chatHistory.set(chatId, session.history);
|
|
483
|
+
console.log(`[${ts()}] ✓ Loaded ${chatId}: ${session.history.length} messages`);
|
|
484
|
+
}
|
|
485
|
+
}
|
|
486
|
+
if (files.length > 0) {
|
|
487
|
+
console.log(`[${ts()}] Chat history restored: ${chatHistory.size} session(s)`);
|
|
488
|
+
}
|
|
489
|
+
}
|
|
490
|
+
catch (e) {
|
|
491
|
+
console.error(`[${ts()}] ⚠️ Failed to load chat history: ${e.message}`);
|
|
492
|
+
}
|
|
493
|
+
}
|
|
494
|
+
function getChatHistory(chatId) {
|
|
495
|
+
if (!chatHistory.has(chatId)) {
|
|
496
|
+
// Try loading from disk if not in memory
|
|
497
|
+
loadSession(chatId).then(session => {
|
|
498
|
+
if (session) {
|
|
499
|
+
chatHistory.set(chatId, session.history);
|
|
500
|
+
}
|
|
501
|
+
else {
|
|
502
|
+
chatHistory.set(chatId, []);
|
|
503
|
+
}
|
|
504
|
+
}).catch(() => {
|
|
505
|
+
chatHistory.set(chatId, []);
|
|
361
506
|
});
|
|
362
|
-
return
|
|
507
|
+
return chatHistory.get(chatId) || [];
|
|
363
508
|
}
|
|
364
|
-
|
|
365
|
-
|
|
366
|
-
|
|
367
|
-
|
|
368
|
-
|
|
369
|
-
|
|
509
|
+
return chatHistory.get(chatId);
|
|
510
|
+
}
|
|
511
|
+
async function pushToHistory(chatId, msg) {
|
|
512
|
+
const h = getChatHistory(chatId);
|
|
513
|
+
h.push(msg);
|
|
514
|
+
// Trim to keep last N messages
|
|
515
|
+
while (h.length > CHAT_HISTORY_MAX)
|
|
516
|
+
h.shift();
|
|
517
|
+
// Mark as dirty and save to disk
|
|
518
|
+
dirtySessions.add(chatId);
|
|
519
|
+
await saveSession(chatId, h);
|
|
520
|
+
dirtySessions.delete(chatId);
|
|
521
|
+
}
|
|
522
|
+
function getChatProvider() {
|
|
523
|
+
if (!_chatProvider) {
|
|
524
|
+
_chatProvider = (0, factory_js_1.createProvider)({ model: DEFAULT_CHAT_MODEL, temperature: 0.7, maxTokens: 4096 });
|
|
525
|
+
console.log(`[${new Date().toISOString().replace('T', ' ').slice(0, 19)}] Chat model: ${DEFAULT_CHAT_MODEL} (${_chatProvider.name})`);
|
|
526
|
+
}
|
|
527
|
+
return _chatProvider;
|
|
528
|
+
}
|
|
529
|
+
/** Block only truly catastrophic commands; everything else is allowed. Shared
|
|
530
|
+
* by /run and the agentic chat loop so both enforce the same floor. */
|
|
531
|
+
function isCatastrophic(cmd) {
|
|
532
|
+
const banned = [
|
|
533
|
+
'rm -rf /', 'rm -rf /*', 'rm -rf ~', 'mkfs', 'dd if=/dev/zero', 'dd if=/dev/random',
|
|
534
|
+
':(){ :|:& };:', 'fork bomb', 'shutdown', 'poweroff', 'init 0', 'halt', 'reboot',
|
|
535
|
+
'> /dev/sda', 'chmod -R 000 /', 'chown -R',
|
|
536
|
+
];
|
|
537
|
+
const c = cmd.toLowerCase();
|
|
538
|
+
return banned.some(d => c.includes(d.toLowerCase()));
|
|
539
|
+
}
|
|
540
|
+
/**
|
|
541
|
+
* A command is "read-only" (safe to run without approval) only if EVERY
|
|
542
|
+
* whitespace/pipe/;-separated segment starts with a known inspection command
|
|
543
|
+
* AND it contains no output redirection. Anything else (writes, installs,
|
|
544
|
+
* deletes, unknown binaries) requires explicit approval. Deny-by-default: if
|
|
545
|
+
* we're not sure it's read-only, we treat it as needing approval.
|
|
546
|
+
*/
|
|
547
|
+
const READ_ONLY_CMDS = new Set([
|
|
548
|
+
'ls', 'cat', 'pwd', 'whoami', 'date', 'df', 'du', 'ps', 'top', 'free', 'uname',
|
|
549
|
+
'which', 'find', 'grep', 'rg', 'head', 'tail', 'wc', 'echo', 'stat', 'file',
|
|
550
|
+
'git', 'uptime', 'hostname', 'id', 'env', 'printenv', 'lsblk', 'lscpu', 'sensors',
|
|
551
|
+
'nvidia-smi', 'systemctl', 'journalctl', 'sort', 'uniq', 'cut', 'awk', 'sed',
|
|
552
|
+
]);
|
|
553
|
+
function isReadOnlyCommand(cmd) {
|
|
554
|
+
if (/[>]|>>|\btee\b|\bdd\b/.test(cmd))
|
|
555
|
+
return false; // any redirection → mutating
|
|
556
|
+
// git/systemctl subcommands that mutate:
|
|
557
|
+
if (/\bgit\s+(push|commit|reset|checkout|clean|rm|merge|rebase|stash\s+drop)\b/.test(cmd))
|
|
558
|
+
return false;
|
|
559
|
+
if (/\bsystemctl\s+(start|stop|restart|enable|disable|mask)\b/.test(cmd))
|
|
560
|
+
return false;
|
|
561
|
+
// Split on shell separators; every segment's first token must be read-only.
|
|
562
|
+
const segments = cmd.split(/\||;|&&|\|\|/).map(s => s.trim()).filter(Boolean);
|
|
563
|
+
if (segments.length === 0)
|
|
564
|
+
return false;
|
|
565
|
+
for (const seg of segments) {
|
|
566
|
+
const first = seg.split(/\s+/)[0].replace(/^sudo$/, '');
|
|
567
|
+
if (first === 'sudo')
|
|
568
|
+
return false; // sudo always needs approval
|
|
569
|
+
if (!READ_ONLY_CMDS.has(first))
|
|
570
|
+
return false;
|
|
571
|
+
}
|
|
572
|
+
return true;
|
|
573
|
+
}
|
|
574
|
+
const AGENT_MAX_STEPS = 4;
|
|
575
|
+
const pendingApprovals = new Map();
|
|
576
|
+
const APPROVAL_TIMEOUT_MS = 5 * 60_000;
|
|
577
|
+
const runningTasks = new Map();
|
|
578
|
+
function registerTask(chatId, entry) {
|
|
579
|
+
let set = runningTasks.get(chatId);
|
|
580
|
+
if (!set) {
|
|
581
|
+
set = new Set();
|
|
582
|
+
runningTasks.set(chatId, set);
|
|
583
|
+
}
|
|
584
|
+
set.add(entry);
|
|
585
|
+
}
|
|
586
|
+
function unregisterTask(chatId, entry) {
|
|
587
|
+
const set = runningTasks.get(chatId);
|
|
588
|
+
if (!set)
|
|
589
|
+
return;
|
|
590
|
+
set.delete(entry);
|
|
591
|
+
if (set.size === 0)
|
|
592
|
+
runningTasks.delete(chatId);
|
|
593
|
+
}
|
|
594
|
+
const ABORTED = Symbol('aborted');
|
|
595
|
+
/** Race a promise against an abort signal. On abort the caller moves on
|
|
596
|
+
* immediately; the losing in-flight call finishes in the background and its
|
|
597
|
+
* result is discarded (Promise.race keeps its rejection observed). */
|
|
598
|
+
function raceAbort(p, signal) {
|
|
599
|
+
if (signal.aborted)
|
|
600
|
+
return Promise.resolve(ABORTED);
|
|
601
|
+
return Promise.race([
|
|
602
|
+
p,
|
|
603
|
+
new Promise((resolve) => signal.addEventListener('abort', () => resolve(ABORTED), { once: true })),
|
|
604
|
+
]);
|
|
605
|
+
}
|
|
606
|
+
/** Resolve every pending approval for a chat. Returns how many were flushed. */
|
|
607
|
+
function flushApprovals(chatId, approved) {
|
|
608
|
+
let n = 0;
|
|
609
|
+
for (const [id, p] of pendingApprovals) {
|
|
610
|
+
if (p.chatId === chatId) {
|
|
611
|
+
pendingApprovals.delete(id);
|
|
612
|
+
p.resolve(approved);
|
|
613
|
+
n++;
|
|
614
|
+
}
|
|
370
615
|
}
|
|
616
|
+
return n;
|
|
617
|
+
}
|
|
618
|
+
/** Ask Dušan to approve a command; resolves true/false (false on timeout). */
|
|
619
|
+
async function requestApproval(chatId, label, command) {
|
|
620
|
+
const id = `ap_${Date.now().toString(36)}_${Math.random().toString(36).slice(2, 6)}`;
|
|
621
|
+
const keyboard = {
|
|
622
|
+
inline_keyboard: [[
|
|
623
|
+
{ text: '✅ Approve', callback_data: `ok:${id}` },
|
|
624
|
+
{ text: '❌ Deny', callback_data: `no:${id}` },
|
|
625
|
+
]],
|
|
626
|
+
};
|
|
627
|
+
await apiPost('sendMessage', {
|
|
628
|
+
chat_id: chatId,
|
|
629
|
+
text: `⚠️ Approve ${label}?\n\n\`${command.slice(0, 300)}\``,
|
|
630
|
+
parse_mode: 'Markdown',
|
|
631
|
+
reply_markup: keyboard,
|
|
632
|
+
});
|
|
633
|
+
return new Promise((resolve) => {
|
|
634
|
+
pendingApprovals.set(id, { resolve, command, chatId: String(chatId), createdAt: Date.now() });
|
|
635
|
+
setTimeout(() => {
|
|
636
|
+
const p = pendingApprovals.get(id);
|
|
637
|
+
if (p) {
|
|
638
|
+
pendingApprovals.delete(id);
|
|
639
|
+
p.resolve(false);
|
|
640
|
+
}
|
|
641
|
+
}, APPROVAL_TIMEOUT_MS);
|
|
642
|
+
});
|
|
643
|
+
}
|
|
644
|
+
async function chatWithLLM(chatId, userMessage, userName) {
|
|
645
|
+
const provider = getChatProvider();
|
|
646
|
+
// Agentic system prompt: the model may run real shell commands on Dušan's PC
|
|
647
|
+
// by emitting a line `RUN: <command>`. The bot executes it and feeds the
|
|
648
|
+
// output back so the model can answer from real data (processes, files, etc).
|
|
649
|
+
const systemPrompt = buildSystemPrompt() + [
|
|
650
|
+
'',
|
|
651
|
+
'## You CAN act on this computer (actions)',
|
|
652
|
+
'You run on Dušan\'s Linux PC and have real abilities. To act, emit ONE line',
|
|
653
|
+
'that is JUST the action (nothing else in that reply); the system performs it,',
|
|
654
|
+
'sends you the result, and you then give your natural answer:',
|
|
655
|
+
' • `RUN: <shell command>` — inspect the PC (ps, free -h, df -h, ls, cat,',
|
|
656
|
+
' grep, git status…). Prefer read-only commands.',
|
|
657
|
+
' • `SEND: <file path>` — send a file to Dušan on Telegram (images go as',
|
|
658
|
+
' photos, everything else as a document). Use this whenever he asks you to',
|
|
659
|
+
' send / share / give him a file. You DO have file-sending ability.',
|
|
660
|
+
' • `CAM: [device]` — capture a webcam snapshot (default /dev/video0, the',
|
|
661
|
+
' integrated camera) and send it. Use for surveillance / "show me the room"',
|
|
662
|
+
' / "take a photo" requests.',
|
|
663
|
+
'NEVER claim you cannot send files or take photos — you can, via SEND and CAM.',
|
|
664
|
+
'Only use an action when needed; for normal conversation just reply directly.',
|
|
665
|
+
].join('\n');
|
|
666
|
+
const recent = getChatHistory(chatId).slice(-(CHAT_HISTORY_MAX - 1));
|
|
667
|
+
const history = [...recent, { role: 'user', content: userMessage }];
|
|
668
|
+
await pushToHistory(String(chatId), { role: 'user', content: userMessage });
|
|
669
|
+
const ts0 = () => new Date().toISOString().replace('T', ' ').slice(0, 19);
|
|
670
|
+
// Register this run so /stop can abort it (per-chat; several runs may be
|
|
671
|
+
// in flight since message handlers are detached).
|
|
672
|
+
const runEntry = {
|
|
673
|
+
abort: new AbortController(),
|
|
674
|
+
task: userMessage.slice(0, 120),
|
|
675
|
+
startedAt: Date.now(),
|
|
676
|
+
};
|
|
677
|
+
registerTask(chatId, runEntry);
|
|
678
|
+
const signal = runEntry.abort.signal;
|
|
679
|
+
try {
|
|
680
|
+
let finalReply = '';
|
|
681
|
+
for (let step = 0; step < AGENT_MAX_STEPS; step++) {
|
|
682
|
+
const response = await raceAbort(provider.complete(systemPrompt, history, []), signal);
|
|
683
|
+
if (response === ABORTED) {
|
|
684
|
+
finalReply = '⏹ Stopped.';
|
|
685
|
+
break;
|
|
686
|
+
}
|
|
687
|
+
const text = (response.text || '').trim();
|
|
688
|
+
// Which action does the model want? RUN (shell), SEND (a file), CAM (webcam).
|
|
689
|
+
// Models often wrap the directive in markdown — a leading backtick, bullet,
|
|
690
|
+
// or blockquote — so tolerate those and strip a trailing backtick from the
|
|
691
|
+
// argument. Without this the action leaks out as visible text.
|
|
692
|
+
const action = text.match(/(?:^|\n)[ \t`>*_-]*(RUN|SEND|CAM):[ \t]*`?([^\n`]+)/);
|
|
693
|
+
if (!action) {
|
|
694
|
+
// No action → this is the answer. Strip any stray directive fragments
|
|
695
|
+
// so raw "SEND:/RUN:" text never shows to the user.
|
|
696
|
+
finalReply = (text || '(no response from model)')
|
|
697
|
+
.replace(/[ \t`>*_-]*(RUN|SEND|CAM):[^\n]*/g, '').replace(/\n{3,}/g, '\n\n').trim()
|
|
698
|
+
|| '(no response from model)';
|
|
699
|
+
break;
|
|
700
|
+
}
|
|
701
|
+
const verb = action[1];
|
|
702
|
+
const arg = (action[2] || '').trim().replace(/`+$/, '').trim();
|
|
703
|
+
// /stop between the LLM deciding an action and us executing it — don't
|
|
704
|
+
// run the action (a shell command may be destructive; the wait for an
|
|
705
|
+
// approval tap resolves false on /stop and lands here too).
|
|
706
|
+
if (signal.aborted) {
|
|
707
|
+
finalReply = '⏹ Stopped.';
|
|
708
|
+
break;
|
|
709
|
+
}
|
|
710
|
+
let toolOut;
|
|
711
|
+
try {
|
|
712
|
+
if (verb === 'RUN') {
|
|
713
|
+
console.error(`[${ts0()}] agent RUN: ${arg}`);
|
|
714
|
+
if (isCatastrophic(arg)) {
|
|
715
|
+
toolOut = 'BLOCKED: refused as catastrophic. Do not retry this command.';
|
|
716
|
+
}
|
|
717
|
+
else if (!isReadOnlyCommand(arg)) {
|
|
718
|
+
// Mutating / unknown command → require Dušan's explicit approval.
|
|
719
|
+
const approved = await requestApproval(chatId, 'this command', arg);
|
|
720
|
+
if (!approved) {
|
|
721
|
+
toolOut = 'DENIED by user (not approved). Do not retry; suggest an alternative or ask.';
|
|
722
|
+
}
|
|
723
|
+
else {
|
|
724
|
+
const r = await execShell(arg);
|
|
725
|
+
toolOut = `exit ${r.code}\n${(r.stdout || r.stderr || '(no output)').slice(0, 3000)}`;
|
|
726
|
+
}
|
|
727
|
+
}
|
|
728
|
+
else {
|
|
729
|
+
const r = await execShell(arg);
|
|
730
|
+
toolOut = `exit ${r.code}\n${(r.stdout || r.stderr || '(no output)').slice(0, 3000)}`;
|
|
731
|
+
}
|
|
732
|
+
}
|
|
733
|
+
else if (verb === 'SEND') {
|
|
734
|
+
console.error(`[${ts0()}] agent SEND: ${arg}`);
|
|
735
|
+
const resolved = path.isAbsolute(arg) ? arg : path.join(DEFAULT_CWD, arg);
|
|
736
|
+
if (!fs.existsSync(resolved)) {
|
|
737
|
+
toolOut = `File not found: ${arg}`;
|
|
738
|
+
}
|
|
739
|
+
else {
|
|
740
|
+
const ext = path.extname(resolved).toLowerCase();
|
|
741
|
+
if (['.jpg', '.jpeg', '.png', '.gif', '.webp'].includes(ext)) {
|
|
742
|
+
await sendPhoto(chatId, resolved);
|
|
743
|
+
}
|
|
744
|
+
else {
|
|
745
|
+
await sendLocalFile(chatId, resolved);
|
|
746
|
+
}
|
|
747
|
+
toolOut = `Sent ${path.basename(resolved)} to the user.`;
|
|
748
|
+
}
|
|
749
|
+
}
|
|
750
|
+
else { // CAM
|
|
751
|
+
console.error(`[${ts0()}] agent CAM: ${arg || 'default'}`);
|
|
752
|
+
const shot = captureWebcam(arg || undefined);
|
|
753
|
+
await sendPhoto(chatId, shot, 'Camera snapshot');
|
|
754
|
+
try {
|
|
755
|
+
fs.unlinkSync(shot);
|
|
756
|
+
}
|
|
757
|
+
catch { }
|
|
758
|
+
toolOut = 'Captured a webcam snapshot and sent it to the user.';
|
|
759
|
+
}
|
|
760
|
+
}
|
|
761
|
+
catch (e) {
|
|
762
|
+
toolOut = `Action failed: ${e?.message || String(e)}`;
|
|
763
|
+
}
|
|
764
|
+
// Feed the action + its result back and let the model continue.
|
|
765
|
+
history.push({ role: 'assistant', content: `${verb}: ${arg}` });
|
|
766
|
+
history.push({ role: 'user', content: `[result]\n${toolOut}` });
|
|
767
|
+
if (step === AGENT_MAX_STEPS - 1) {
|
|
768
|
+
const wrap = await raceAbort(provider.complete(systemPrompt, history, []), signal);
|
|
769
|
+
finalReply = wrap === ABORTED
|
|
770
|
+
? '⏹ Stopped.'
|
|
771
|
+
: (wrap.text || '').replace(/^(RUN|SEND|CAM):.*$/m, '').trim() || '(done)';
|
|
772
|
+
}
|
|
773
|
+
}
|
|
774
|
+
await pushToHistory(String(chatId), { role: 'assistant', content: finalReply });
|
|
775
|
+
return finalReply;
|
|
776
|
+
}
|
|
777
|
+
catch (e) {
|
|
778
|
+
console.error(`[${ts0()}] LLM error: ${e?.message || String(e)}`);
|
|
779
|
+
return `❌ AI greška: ${e?.message || 'Nepoznata greška'}. Probaj /help.`;
|
|
780
|
+
}
|
|
781
|
+
finally {
|
|
782
|
+
unregisterTask(chatId, runEntry);
|
|
783
|
+
}
|
|
784
|
+
}
|
|
785
|
+
// ─────────────────────────────────────────────────────────────────────────────
|
|
786
|
+
// Command handlers
|
|
787
|
+
// ─────────────────────────────────────────────────────────────────────────────
|
|
788
|
+
// ── Tool execution helpers ──────────────────────────────────────────────────
|
|
789
|
+
const DEFAULT_CWD = process.env.HOME ?? '/tmp';
|
|
790
|
+
// Track pending file operations from natural language
|
|
791
|
+
const pendingFileOps = new Map();
|
|
792
|
+
function execShell(command, cwd) {
|
|
793
|
+
return new Promise((resolve) => {
|
|
794
|
+
(0, child_process_1.exec)(command, { cwd: cwd ?? DEFAULT_CWD, timeout: 30_000, maxBuffer: 1024 * 1024 }, (err, stdout, stderr) => {
|
|
795
|
+
resolve({
|
|
796
|
+
stdout: stdout?.toString() ?? '',
|
|
797
|
+
stderr: stderr?.toString() ?? '',
|
|
798
|
+
code: err ? (err.code ?? 1) : 0,
|
|
799
|
+
});
|
|
800
|
+
});
|
|
801
|
+
});
|
|
371
802
|
}
|
|
372
803
|
function readFileTool(filePath) {
|
|
373
804
|
const resolved = path.isAbsolute(filePath) ? filePath : path.join(DEFAULT_CWD, filePath);
|
|
@@ -399,124 +830,169 @@ function listDirTool(dirPath) {
|
|
|
399
830
|
return `❌ Error listing: ${e.message}`;
|
|
400
831
|
}
|
|
401
832
|
}
|
|
402
|
-
/** Escape a string for use inside double quotes in a shell command. */
|
|
403
|
-
function escapeShellDouble(s) {
|
|
404
|
-
return s.replace(/[\\"$]/g, '\\$&').replace(/`/g, '\\`');
|
|
405
|
-
}
|
|
406
833
|
function searchCodeTool(pattern, searchPath) {
|
|
407
834
|
const resolved = searchPath
|
|
408
835
|
? (path.isAbsolute(searchPath) ? searchPath : path.join(DEFAULT_CWD, searchPath))
|
|
409
836
|
: DEFAULT_CWD;
|
|
410
837
|
try {
|
|
411
|
-
const result = (0, child_process_1.execSync)(
|
|
412
|
-
return result.trim() ||
|
|
838
|
+
const result = (0, child_process_1.execSync)(`rg -n --no-heading -i "${pattern.replace(/"/g, '\\"')}" "${resolved}" 2>/dev/null | head -30`, { timeout: 10_000, encoding: 'utf8' });
|
|
839
|
+
return result.trim() || `No matches for "${pattern}"`;
|
|
413
840
|
}
|
|
414
841
|
catch {
|
|
415
|
-
return
|
|
842
|
+
return `No matches for "${pattern}" (or rg not installed)`;
|
|
416
843
|
}
|
|
417
844
|
}
|
|
418
|
-
|
|
419
|
-
|
|
420
|
-
|
|
421
|
-
function
|
|
845
|
+
/** Bare text that handleCommand executes directly as a shell command
|
|
846
|
+
* (no leading slash). Shared with the reply path so shell output — like
|
|
847
|
+
* /-command output — is never read aloud as a voice note. */
|
|
848
|
+
function isDirectShellText(lowerText) {
|
|
849
|
+
return /^(ls|cat|pwd|whoami|date|df|du|ps|top|free|uname|which|find|grep|git|npm|node|python|curl)\b/.test(lowerText);
|
|
850
|
+
}
|
|
851
|
+
async function handleCommand(chatId, text, from) {
|
|
422
852
|
const lower = text.toLowerCase().trim();
|
|
423
|
-
const tag = (0, telegram_safety_js_1.safetyLabel)(safetyState.safetyOn);
|
|
424
853
|
if (lower === '/start' || lower === '/help') {
|
|
425
854
|
return [
|
|
426
|
-
`💎 Aura Bot — Online
|
|
855
|
+
`💎 Aura Bot — Online`,
|
|
856
|
+
``,
|
|
857
|
+
`Komande:`,
|
|
858
|
+
`/status — Šta trenutno radi u ovom chatu (zadatak, trajanje, potvrde na čekanju) + status sistema`,
|
|
859
|
+
`/tools — Lista dostupnih alata`,
|
|
860
|
+
`/memory — Pregled memorije`,
|
|
861
|
+
`/history — Pregled istorije razgovora`,
|
|
862
|
+
`/clear — Obriši istoriju razgovora`,
|
|
863
|
+
`/time — Trenutno vreme`,
|
|
864
|
+
`/ping — Provera konekcije`,
|
|
865
|
+
`/whoami — Ko sam ja`,
|
|
427
866
|
``,
|
|
428
|
-
|
|
429
|
-
`/
|
|
430
|
-
`/
|
|
431
|
-
`/
|
|
432
|
-
`/
|
|
433
|
-
`/
|
|
434
|
-
`/
|
|
435
|
-
`/ls <dir> — List directory`,
|
|
436
|
-
`/read <file> — Read file`,
|
|
437
|
-
`/search <pattern> — Search code`,
|
|
438
|
-
`/run <cmd> — Run shell command`,
|
|
867
|
+
`💻 PC Control:`,
|
|
868
|
+
`/ls <dir> — Lista direktorijuma na tvom PC-ju`,
|
|
869
|
+
`/read <file> — Čitanje fajla sa tvog PC-ja`,
|
|
870
|
+
`/sendfile <path> — Pošalji fajl sa tvog PC-ja na Telegram`,
|
|
871
|
+
`/find <pattern> — Pronađi fajlove na tvom PC-ju`,
|
|
872
|
+
`/run <cmd> — Izvrši shell komandu na tvom PC-ju`,
|
|
873
|
+
`/cam — Snimi i pošalji sliku sa kamere (nadzor)`,
|
|
439
874
|
`/git — Git status`,
|
|
440
|
-
`/cancel — Cancel stuck task`,
|
|
441
|
-
`/clear — Clear context`,
|
|
442
|
-
`/sur30 — Webcam surveillance 30 min`,
|
|
443
|
-
`/sur60 — Webcam surveillance 60 min`,
|
|
444
875
|
``,
|
|
445
|
-
|
|
446
|
-
|
|
447
|
-
|
|
448
|
-
if (lower === '/ping')
|
|
449
|
-
return `🏓 Pong! Aura is alive and running. ${tag}`;
|
|
450
|
-
if (lower === '/whoami') {
|
|
451
|
-
return [
|
|
452
|
-
`💎 I am Aura — an agent. ${tag}`,
|
|
876
|
+
`🎛 Kontrola zadatka:`,
|
|
877
|
+
`/stop — Prekini zadatak koji trenutno radi u ovom chatu`,
|
|
878
|
+
`/approve-all — ⚠️ Odobri SVE potvrde koje trenutno čekaju (uključujući destruktivne komande, bez ponovnog prikaza). Jednokratno — NE prebacuje u auto režim, sledeće akcije opet pitaju.`,
|
|
453
879
|
``,
|
|
454
|
-
|
|
455
|
-
|
|
456
|
-
|
|
457
|
-
`
|
|
458
|
-
`Tools: 22+`,
|
|
459
|
-
`Tests: 734+ passing`,
|
|
460
|
-
`Version: v0.3.0 (Aura rebrand)`,
|
|
461
|
-
`Mode: ${safetyState.safetyOn ? '🔒 Safe (asks before destructive ops)' : '🔓 Auto (no confirmation)'}`,
|
|
462
|
-
`Project: ${PROJECT_ROOT}`,
|
|
463
|
-
`Model: ${TASK_MODEL}`,
|
|
880
|
+
`💡 Pamtiš razgovore trajno — šta god da mi tražiš, zapamtiću to za sledeći put!`,
|
|
881
|
+
`💡 Možeš da tražiš fajlove sa tvog računara i šaljiš ih sebi!`,
|
|
882
|
+
``,
|
|
883
|
+
`Ili mi piši bilo šta — odgovoriću!`,
|
|
464
884
|
].join('\n');
|
|
465
885
|
}
|
|
886
|
+
if (lower === '/ping')
|
|
887
|
+
return '🏓 Pong! Aura je živa i radi.';
|
|
888
|
+
// ── /status — what's running in THIS chat, then bot health ───────────────
|
|
889
|
+
// Pairs with /stop and /approve-all: when a task runs unattended for a
|
|
890
|
+
// while, this shows what it is, how long it's been going, and whether it's
|
|
891
|
+
// blocked waiting on an approval tap. The bot-health block that /status
|
|
892
|
+
// always showed follows below the live-task section.
|
|
466
893
|
if (lower === '/status') {
|
|
894
|
+
const lines = [];
|
|
895
|
+
const set = runningTasks.get(String(chatId));
|
|
896
|
+
const waiting = [...pendingApprovals.values()].filter(p => p.chatId === String(chatId));
|
|
897
|
+
if (set && set.size > 0) {
|
|
898
|
+
lines.push(`🏃 Running task(s): ${set.size}`);
|
|
899
|
+
for (const t of set) {
|
|
900
|
+
const secs = Math.round((Date.now() - t.startedAt) / 1000);
|
|
901
|
+
const mins = Math.floor(secs / 60);
|
|
902
|
+
const dur = mins > 0 ? `${mins}m ${secs % 60}s` : `${secs}s`;
|
|
903
|
+
lines.push(` • "${t.task}" — running ${dur}`);
|
|
904
|
+
}
|
|
905
|
+
}
|
|
906
|
+
else {
|
|
907
|
+
lines.push('💤 No task running in this chat.');
|
|
908
|
+
}
|
|
909
|
+
if (waiting.length > 0) {
|
|
910
|
+
lines.push(`⏳ Waiting for your ✅/❌ approval: ${waiting.length}`);
|
|
911
|
+
for (const p of waiting)
|
|
912
|
+
lines.push(` • ${p.command.slice(0, 120)}`);
|
|
913
|
+
lines.push(`(/approve-all flushes these — including destructive ones; /stop denies them.)`);
|
|
914
|
+
}
|
|
467
915
|
const uptime = process.uptime();
|
|
468
916
|
const hours = Math.floor(uptime / 3600);
|
|
469
917
|
const mins = Math.floor((uptime % 3600) / 60);
|
|
470
918
|
const mem = Math.round(process.memoryUsage().heapUsed / 1024 / 1024);
|
|
919
|
+
lines.push(``, `📊 Aura Status`, `Uptime: ${hours}h ${mins}m`, `Memory: ${mem}MB`, `Node: ${process.version}`, `Bot: @Aura_Code_bot`, `Status: ✅ Active`);
|
|
920
|
+
return lines.join('\n');
|
|
921
|
+
}
|
|
922
|
+
// ── /stop — abort the running task(s) for THIS chat ──────────────────────
|
|
923
|
+
// Same semantics as the CLI's Esc/:stop abort: the agentic loop checks the
|
|
924
|
+
// signal between steps and races it against in-flight LLM calls. Pending
|
|
925
|
+
// approval prompts are denied so awaited steps unblock instead of hanging
|
|
926
|
+
// until their 5-minute timeout.
|
|
927
|
+
if (lower === '/stop') {
|
|
928
|
+
const denied = flushApprovals(String(chatId), false);
|
|
929
|
+
const set = runningTasks.get(String(chatId));
|
|
930
|
+
if (!set || set.size === 0) {
|
|
931
|
+
return denied > 0
|
|
932
|
+
? `⏹ Nothing running — but denied ${denied} stale pending approval(s).`
|
|
933
|
+
: '⏹ Nothing is running in this chat.';
|
|
934
|
+
}
|
|
935
|
+
const lines = [];
|
|
936
|
+
for (const t of set) {
|
|
937
|
+
t.abort.abort();
|
|
938
|
+
const secs = Math.round((Date.now() - t.startedAt) / 1000);
|
|
939
|
+
lines.push(` • "${t.task}" (running ${secs}s)`);
|
|
940
|
+
}
|
|
941
|
+
return [
|
|
942
|
+
`⏹ Stopping ${set.size} task(s):`,
|
|
943
|
+
...lines,
|
|
944
|
+
...(denied > 0 ? [`Also denied ${denied} pending approval(s).`] : []),
|
|
945
|
+
`Note: a shell command already executing finishes its (≤30s) run; nothing further happens after it.`,
|
|
946
|
+
].join('\n');
|
|
947
|
+
}
|
|
948
|
+
// ── /approve-all — one-time flush of pending confirmations ───────────────
|
|
949
|
+
// ⚠️ Trust escalation: approves EVERYTHING currently waiting for a ✅/❌ tap,
|
|
950
|
+
// including destructive operations, sight unseen. It does NOT change the
|
|
951
|
+
// permission mode — the very next mutating command will ask again.
|
|
952
|
+
if (lower === '/approve-all') {
|
|
953
|
+
const n = flushApprovals(String(chatId), true);
|
|
954
|
+
if (n === 0) {
|
|
955
|
+
return [
|
|
956
|
+
'✅ No approvals were pending.',
|
|
957
|
+
'',
|
|
958
|
+
'⚠️ /approve-all approves everything pending right now, including',
|
|
959
|
+
'destructive operations, without showing them again. One-time flush —',
|
|
960
|
+
'it does not switch to auto-approve; future actions still ask.',
|
|
961
|
+
].join('\n');
|
|
962
|
+
}
|
|
471
963
|
return [
|
|
472
|
-
|
|
473
|
-
`
|
|
474
|
-
|
|
475
|
-
|
|
476
|
-
|
|
477
|
-
|
|
478
|
-
|
|
479
|
-
|
|
964
|
+
`✅ Approved ${n} pending confirmation(s) — including any destructive operations that were waiting.`,
|
|
965
|
+
`This was a one-time flush; the permission mode is unchanged and future actions will still ask.`,
|
|
966
|
+
].join('\n');
|
|
967
|
+
}
|
|
968
|
+
if (lower === '/time')
|
|
969
|
+
return `🕐 ${new Date().toLocaleString('sr-RS', { timeZone: 'Europe/Belgrade' })}`;
|
|
970
|
+
if (lower === '/whoami') {
|
|
971
|
+
return [
|
|
972
|
+
`💎 Ja sam Aura — agent.`,
|
|
973
|
+
``,
|
|
974
|
+
`Framework: Aura (starogrčki: ona koja deluje)`,
|
|
975
|
+
`Karakter: Precizna, carska, self-aware`,
|
|
976
|
+
`Moto: "I don't try. I verify."`,
|
|
977
|
+
`Builder: Dušan Milosavljević`,
|
|
978
|
+
`Alati: 22`,
|
|
979
|
+
`Testovi: 838+ passing`,
|
|
980
|
+
`Verzija: v0.7.2 (Aura)`,
|
|
480
981
|
].join('\n');
|
|
481
982
|
}
|
|
482
983
|
if (lower === '/tools') {
|
|
483
984
|
return [
|
|
484
|
-
`🔧
|
|
985
|
+
`🔧 Dostupni alati:`,
|
|
485
986
|
``,
|
|
486
|
-
`📁 /ls <dir> —
|
|
487
|
-
`📄 /read <file> —
|
|
488
|
-
`🔍 /search <pattern> —
|
|
489
|
-
`⚡ /run <cmd> — shell
|
|
987
|
+
`📁 /ls <dir> — lista direktorijuma`,
|
|
988
|
+
`📄 /read <file> — čitanje fajla`,
|
|
989
|
+
`🔍 /search <pattern> — pretraga koda`,
|
|
990
|
+
`⚡ /run <cmd> — shell komanda`,
|
|
490
991
|
`🌿 /git — git status`,
|
|
491
|
-
`🧠 /
|
|
492
|
-
`🔒 /safety_on — enable safety`,
|
|
493
|
-
`🔓 /safety_off CONFIRM — disable safety`,
|
|
494
|
-
`🚫 /cancel — cancel stuck task`,
|
|
992
|
+
`🧠 /memory — pregled memorije`,
|
|
495
993
|
].join('\n');
|
|
496
994
|
}
|
|
497
|
-
// ──
|
|
498
|
-
if (lower === '/safety_on') {
|
|
499
|
-
safetyState.safetyOn = true;
|
|
500
|
-
saveState();
|
|
501
|
-
return `🔒 Safety mode enabled. ${(0, telegram_safety_js_1.safetyLabel)(true)} — destructive operations will ask for approval.`;
|
|
502
|
-
}
|
|
503
|
-
if (lower.startsWith('/safety_off')) {
|
|
504
|
-
const rest = text.slice('/safety_off'.length).trim();
|
|
505
|
-
if (rest !== 'CONFIRM') {
|
|
506
|
-
return [
|
|
507
|
-
`⚠️ To disable safety, send:`,
|
|
508
|
-
``,
|
|
509
|
-
`/safety_off CONFIRM`,
|
|
510
|
-
``,
|
|
511
|
-
`This prevents accidental toggling.`,
|
|
512
|
-
`Current state: ${(0, telegram_safety_js_1.safetyLabel)(safetyState.safetyOn)}`,
|
|
513
|
-
].join('\n');
|
|
514
|
-
}
|
|
515
|
-
safetyState.safetyOn = false;
|
|
516
|
-
saveState();
|
|
517
|
-
return `🔓 Safety mode disabled. ${(0, telegram_safety_js_1.safetyLabel)(false)} — all operations will execute without confirmation.`;
|
|
518
|
-
}
|
|
519
|
-
// ── Tool commands ───────────────────────────────────────────────────────
|
|
995
|
+
// ── Tool commands ──────────────────────────────────────────────────────
|
|
520
996
|
if (lower.startsWith('/ls')) {
|
|
521
997
|
const dir = text.slice(3).trim() || '.';
|
|
522
998
|
return `📁 ${dir}:\n${listDirTool(dir)}`;
|
|
@@ -537,246 +1013,370 @@ function handleCommand(chatId, text, from) {
|
|
|
537
1013
|
const cmd = text.slice(4).trim();
|
|
538
1014
|
if (!cmd)
|
|
539
1015
|
return '❌ Usage: /run <command>';
|
|
540
|
-
|
|
541
|
-
|
|
542
|
-
|
|
1016
|
+
if (isCatastrophic(cmd)) {
|
|
1017
|
+
return '🚫 Blocked: extremely dangerous command detected. This would destroy your system.';
|
|
1018
|
+
}
|
|
1019
|
+
// Mutating commands need an explicit ✅ approval; read-only run immediately.
|
|
1020
|
+
if (!isReadOnlyCommand(cmd)) {
|
|
1021
|
+
const approved = await requestApproval(chatId, 'this command', cmd);
|
|
1022
|
+
if (!approved)
|
|
1023
|
+
return '❌ Denied — command not run.';
|
|
543
1024
|
}
|
|
544
|
-
const result = execShell(cmd);
|
|
1025
|
+
const result = await execShell(cmd);
|
|
545
1026
|
const output = result.stdout || result.stderr || '(no output)';
|
|
546
1027
|
const truncated = output.length > 3500 ? output.slice(0, 3500) + '\n... (truncated)' : output;
|
|
547
1028
|
return `⚡ ${cmd}\n${result.code === 0 ? '✅' : '❌'} exit ${result.code}\n${truncated}`;
|
|
548
1029
|
}
|
|
549
1030
|
if (lower === '/git') {
|
|
550
|
-
const result = execShell('git status --short && echo "---" && git log --oneline -5');
|
|
1031
|
+
const result = await execShell('git status --short && echo "---" && git log --oneline -5');
|
|
551
1032
|
return `🌿 Git:\n${result.stdout || '(not a git repo)'}`;
|
|
552
1033
|
}
|
|
1034
|
+
if (lower.startsWith('/memory')) {
|
|
1035
|
+
const memDir = path.join(os.homedir(), '.aura', 'memory');
|
|
1036
|
+
if (!fs.existsSync(memDir))
|
|
1037
|
+
return '🧠 Nema memorije.';
|
|
1038
|
+
try {
|
|
1039
|
+
const files = fs.readdirSync(memDir).filter(f => f.endsWith('.json'));
|
|
1040
|
+
if (files.length === 0)
|
|
1041
|
+
return '🧠 Memorija prazna.';
|
|
1042
|
+
const lines = files.map(f => {
|
|
1043
|
+
const data = JSON.parse(fs.readFileSync(path.join(memDir, f), 'utf8'));
|
|
1044
|
+
return `📁 ${f.replace('.json', '')}: ${Object.keys(data).length} ključeva`;
|
|
1045
|
+
});
|
|
1046
|
+
return `🧠 Memorija:\n${lines.join('\n')}`;
|
|
1047
|
+
}
|
|
1048
|
+
catch {
|
|
1049
|
+
return '🧠 Greška pri čitanju memorije.';
|
|
1050
|
+
}
|
|
1051
|
+
}
|
|
1052
|
+
if (lower === '/history' || lower.startsWith('/history')) {
|
|
1053
|
+
const history = getChatHistory(String(chatId));
|
|
1054
|
+
if (history.length === 0)
|
|
1055
|
+
return '📜 Nema istorije razgovora. Započni razgovor i ja ću ga pamtiti!';
|
|
1056
|
+
const lines = history.slice(-10).map((m, i) => {
|
|
1057
|
+
const role = m.role === 'user' ? '👤 Ti' : '🤖 Aura';
|
|
1058
|
+
const content = (m.role === 'tool_result') ? '[alat]' : (typeof m.content === 'string' ? m.content.slice(0, 100) : '[non-text]');
|
|
1059
|
+
return `${role}: ${content}${content.length >= 100 ? '...' : ''}`;
|
|
1060
|
+
});
|
|
1061
|
+
return `📜 Poslednjih 10 poruka (ukupno ${history.length}):\n${lines.join('\n')}\n\n💡 Svi razgovori se čuvaju trajno — pamtim šta si mi rekao čak i ako me resetuješ.`;
|
|
1062
|
+
}
|
|
553
1063
|
if (lower === '/clear') {
|
|
554
|
-
|
|
555
|
-
|
|
1064
|
+
chatHistory.delete(String(chatId));
|
|
1065
|
+
const filePath = getSessionFile(String(chatId));
|
|
1066
|
+
if (fs.existsSync(filePath)) {
|
|
1067
|
+
fs.unlinkSync(filePath);
|
|
1068
|
+
}
|
|
1069
|
+
return '🗑️ Istorija razgovora obrisana. Možemo početi iz početka!';
|
|
556
1070
|
}
|
|
557
|
-
|
|
558
|
-
|
|
559
|
-
|
|
560
|
-
|
|
1071
|
+
// ── File sending from PC ─────────────────────────────────────────────────────
|
|
1072
|
+
if (lower.startsWith('/sendfile') || lower.startsWith('/send')) {
|
|
1073
|
+
const filePath = text.startsWith('/sendfile ') ? text.slice(9).trim() : text.slice(5).trim();
|
|
1074
|
+
if (!filePath)
|
|
1075
|
+
return '❌ Usage: /sendfile <path> or /send <path>';
|
|
1076
|
+
const resolved = path.isAbsolute(filePath) ? filePath : path.join(DEFAULT_CWD, filePath);
|
|
1077
|
+
if (!fs.existsSync(resolved)) {
|
|
1078
|
+
return `❌ File not found: ${filePath}`;
|
|
561
1079
|
}
|
|
562
|
-
|
|
563
|
-
|
|
564
|
-
|
|
565
|
-
|
|
566
|
-
|
|
567
|
-
|
|
568
|
-
|
|
569
|
-
|
|
1080
|
+
try {
|
|
1081
|
+
const stats = fs.statSync(resolved);
|
|
1082
|
+
const sizeMB = (stats.size / 1024 / 1024).toFixed(2);
|
|
1083
|
+
await sendMessage(chatId, `📤 Sending file: ${path.basename(resolved)} (${sizeMB}MB)`);
|
|
1084
|
+
const ext = path.extname(resolved).toLowerCase();
|
|
1085
|
+
if (['.jpg', '.jpeg', '.png', '.gif', '.webp'].includes(ext)) {
|
|
1086
|
+
await sendPhoto(chatId, resolved);
|
|
1087
|
+
}
|
|
1088
|
+
else {
|
|
1089
|
+
await sendLocalFile(chatId, resolved);
|
|
1090
|
+
}
|
|
1091
|
+
return `✅ File sent: ${path.basename(resolved)}`;
|
|
1092
|
+
}
|
|
1093
|
+
catch (e) {
|
|
1094
|
+
return `❌ Error sending file: ${e.message}`;
|
|
1095
|
+
}
|
|
1096
|
+
}
|
|
1097
|
+
// /cam or /photo — capture a webcam snapshot and send it (surveillance).
|
|
1098
|
+
if (lower === '/cam' || lower === '/photo' || lower.startsWith('/cam ') || lower.startsWith('/photo ')) {
|
|
1099
|
+
const device = text.split(/\s+/)[1] || '/dev/video0';
|
|
1100
|
+
try {
|
|
1101
|
+
await sendMessage(chatId, '📷 Capturing snapshot…');
|
|
1102
|
+
const shot = captureWebcam(device);
|
|
1103
|
+
await sendPhoto(chatId, shot, `Snapshot ${new Date().toLocaleString()}`);
|
|
1104
|
+
try {
|
|
1105
|
+
fs.unlinkSync(shot);
|
|
1106
|
+
}
|
|
1107
|
+
catch { }
|
|
1108
|
+
return '✅ Snapshot sent.';
|
|
1109
|
+
}
|
|
1110
|
+
catch (e) {
|
|
1111
|
+
return `❌ Camera error: ${e.message}`;
|
|
570
1112
|
}
|
|
571
|
-
// Run in background — don't block the bot
|
|
572
|
-
(0, child_process_1.exec)(`nohup bash "${script}" ${duration} > /tmp/sur-${duration}-$(date +%s).log 2>&1 &`, {
|
|
573
|
-
cwd: PROJECT_ROOT,
|
|
574
|
-
timeout: 5_000,
|
|
575
|
-
encoding: 'utf8',
|
|
576
|
-
});
|
|
577
|
-
return `🔒 Surveillance started — ${duration} minutes, snapshots every 5 min.\nYou'll get photos in this chat. ${tag}`;
|
|
578
1113
|
}
|
|
579
|
-
|
|
580
|
-
|
|
581
|
-
|
|
582
|
-
|
|
1114
|
+
if (lower.startsWith('/find')) {
|
|
1115
|
+
const pattern = text.slice(5).trim();
|
|
1116
|
+
if (!pattern)
|
|
1117
|
+
return '❌ Usage: /find <pattern>';
|
|
1118
|
+
try {
|
|
1119
|
+
const searchDir = DEFAULT_CWD;
|
|
1120
|
+
const cmd = `find "${searchDir}" -name "*${pattern}*" -type f 2>/dev/null | head -20`;
|
|
1121
|
+
const result = await execShell(cmd);
|
|
1122
|
+
const files = result.stdout.trim().split('\n').filter(f => f);
|
|
1123
|
+
if (files.length === 0 || files[0] === '') {
|
|
1124
|
+
return `🔍 No files found matching "${pattern}"`;
|
|
1125
|
+
}
|
|
1126
|
+
const lines = files.map(f => {
|
|
1127
|
+
const name = path.basename(f);
|
|
1128
|
+
const rel = path.relative(DEFAULT_CWD, f);
|
|
1129
|
+
const stats = fs.statSync(f);
|
|
1130
|
+
const size = (stats.size / 1024).toFixed(1) + 'KB';
|
|
1131
|
+
return `📄 ${name} (${size})\n ${rel}`;
|
|
1132
|
+
});
|
|
1133
|
+
return `🔍 Found ${files.length} file(s):\n${lines.join('\n')}\n\n💡 Use /sendfile <path> to get any file`;
|
|
1134
|
+
}
|
|
1135
|
+
catch (e) {
|
|
1136
|
+
return `❌ Search error: ${e.message}`;
|
|
1137
|
+
}
|
|
1138
|
+
}
|
|
1139
|
+
if (lower.startsWith('/pwd')) {
|
|
1140
|
+
return `📁 Current directory: ${DEFAULT_CWD}`;
|
|
1141
|
+
}
|
|
1142
|
+
if (lower === '/home' || lower.startsWith('/cd ')) {
|
|
1143
|
+
if (lower === '/home') {
|
|
1144
|
+
return `📁 Home directory: ${process.env.HOME}`;
|
|
1145
|
+
}
|
|
1146
|
+
const dir = text.slice(4).trim();
|
|
1147
|
+
if (!fs.existsSync(dir))
|
|
1148
|
+
return `❌ Directory not found: ${dir}`;
|
|
1149
|
+
// Note: changing DEFAULT_CWD would require restarting, so just show info
|
|
1150
|
+
return `💡 To change working directory, restart bot with HOME set or use absolute paths`;
|
|
1151
|
+
}
|
|
1152
|
+
// Default: try to interpret as a shell command if it looks like one
|
|
1153
|
+
if (isDirectShellText(lower)) {
|
|
1154
|
+
const result = await execShell(text);
|
|
583
1155
|
const output = result.stdout || result.stderr || '(no output)';
|
|
584
1156
|
const truncated = output.length > 3500 ? output.slice(0, 3500) + '\n... (truncated)' : output;
|
|
585
1157
|
return `⚡ ${text}\n${truncated}`;
|
|
586
1158
|
}
|
|
587
|
-
//
|
|
588
|
-
|
|
589
|
-
|
|
590
|
-
//
|
|
591
|
-
//
|
|
592
|
-
|
|
593
|
-
function isAuthorized(chatId, fromUser) {
|
|
594
|
-
// No open-access fallback — if no IDs configured the bot won't start,
|
|
595
|
-
// but guard here too as defense-in-depth.
|
|
596
|
-
if (AUTHORIZED_USER_IDS.size === 0)
|
|
597
|
-
return false;
|
|
598
|
-
// Check by user ID
|
|
599
|
-
const userId = fromUser?.id;
|
|
600
|
-
if (userId && AUTHORIZED_USER_IDS.has(Number(userId)))
|
|
601
|
-
return true;
|
|
602
|
-
// Also check chat ID (for group chats where the user might be different)
|
|
603
|
-
if (AUTHORIZED_USER_IDS.has(chatId))
|
|
604
|
-
return true;
|
|
605
|
-
return false;
|
|
1159
|
+
// Everything else → the agentic LLM. It decides for itself when to run a
|
|
1160
|
+
// command (via `RUN:`), so we no longer keyword-match "send/find/run/search"
|
|
1161
|
+
// in free text. Those greedy matchers hijacked normal conversation — e.g. a
|
|
1162
|
+
// message that merely contained "find" or "search" got treated as a file
|
|
1163
|
+
// search ("No files found matching …"). Explicit /-commands still work above.
|
|
1164
|
+
return await chatWithLLM(String(chatId), text, from);
|
|
606
1165
|
}
|
|
607
1166
|
// ─────────────────────────────────────────────────────────────────────────────
|
|
608
1167
|
// Main polling loop
|
|
609
1168
|
// ─────────────────────────────────────────────────────────────────────────────
|
|
610
|
-
|
|
611
|
-
|
|
612
|
-
* If a task is running, new non-command messages are queued.
|
|
613
|
-
*/
|
|
614
|
-
const runningTasks = new Set();
|
|
615
|
-
/**
|
|
616
|
-
* Per-chat conversation history. Without this, every Telegram message
|
|
617
|
-
* started a brand-new `runAgentLoop` call with no `initialHistory` — so
|
|
618
|
-
* replies like "B", "Ok", or "/approve" to a previous question had zero
|
|
619
|
-
* context, and Aura would respond as if the conversation had never
|
|
620
|
-
* happened. Keyed by chatId, threaded into runAgentLoop below, and updated
|
|
621
|
-
* with the loop's returned history after each task — same pattern the CLI
|
|
622
|
-
* REPL uses for `activeChatHistory`. Capped per-chat to avoid unbounded
|
|
623
|
-
* growth in a long-running bot process.
|
|
624
|
-
*/
|
|
625
|
-
const chatHistories = new Map();
|
|
626
|
-
const MAX_HISTORY_MESSAGES = 200;
|
|
627
|
-
function getChatHistory(chatId) {
|
|
628
|
-
return chatHistories.get(chatId) ?? [];
|
|
629
|
-
}
|
|
630
|
-
function setChatHistory(chatId, history) {
|
|
631
|
-
const trimmed = history.length > MAX_HISTORY_MESSAGES
|
|
632
|
-
? history.slice(history.length - MAX_HISTORY_MESSAGES)
|
|
633
|
-
: history;
|
|
634
|
-
chatHistories.set(chatId, trimmed);
|
|
1169
|
+
function ts() {
|
|
1170
|
+
return new Date().toISOString().replace('T', ' ').slice(0, 19);
|
|
635
1171
|
}
|
|
636
1172
|
async function poll() {
|
|
637
1173
|
let offset = loadOffset();
|
|
638
|
-
console.log(
|
|
639
|
-
console.log(` Bot: @
|
|
1174
|
+
console.log(`[${ts()}] 💎 Aura Telegram Bot started`);
|
|
1175
|
+
console.log(` Bot: @Aura_Code_bot`);
|
|
640
1176
|
console.log(` Offset: ${offset}`);
|
|
641
|
-
console.log(`
|
|
642
|
-
console.log(` Project root: ${PROJECT_ROOT}`);
|
|
643
|
-
console.log(` Task model: ${TASK_MODEL}`);
|
|
644
|
-
console.log(` Safety: ${safetyState.safetyOn ? 'ON' : 'OFF'}`);
|
|
645
|
-
console.log(` Authorized users: ${AUTHORIZED_USER_IDS.size > 0 ? [...AUTHORIZED_USER_IDS].join(', ') : 'ALL (no restriction)'}`);
|
|
1177
|
+
console.log(` Long-polling Telegram (30s)…`);
|
|
646
1178
|
console.log('');
|
|
1179
|
+
// Load chat history from disk on startup — enables conversation continuity across restarts
|
|
1180
|
+
await initializeChatHistory();
|
|
1181
|
+
// Delete any webhook on startup — webhooks and getUpdates conflict (409)
|
|
1182
|
+
try {
|
|
1183
|
+
await apiPost('deleteWebhook', { drop_pending_updates: false });
|
|
1184
|
+
console.log(`[${ts()}] Webhook cleared (polling mode)`);
|
|
1185
|
+
}
|
|
1186
|
+
catch (e) {
|
|
1187
|
+
console.error(`[${ts()}] ⚠️ Webhook clear error: ${e.message}`);
|
|
1188
|
+
}
|
|
647
1189
|
// Clear old updates on first run
|
|
648
1190
|
if (offset === 0) {
|
|
649
1191
|
try {
|
|
650
|
-
const updates = await
|
|
1192
|
+
const updates = await apiGet('getUpdates', { offset: '0', limit: '100' });
|
|
651
1193
|
if (updates.length > 0) {
|
|
652
1194
|
offset = updates[updates.length - 1].update_id + 1;
|
|
653
1195
|
saveOffset(offset);
|
|
654
|
-
console.log(` Cleared ${updates.length} old update(s), offset: ${offset}`);
|
|
1196
|
+
console.log(`[${ts()}] Cleared ${updates.length} old update(s), offset: ${offset}`);
|
|
655
1197
|
}
|
|
656
1198
|
}
|
|
657
1199
|
catch (e) {
|
|
658
|
-
console.error(` ⚠️ Clear error: ${e.message}`);
|
|
1200
|
+
console.error(`[${ts()}] ⚠️ Clear error: ${e.message}`);
|
|
659
1201
|
}
|
|
660
1202
|
}
|
|
661
1203
|
let consecutiveErrors = 0;
|
|
1204
|
+
let lastHeartbeat = Date.now();
|
|
1205
|
+
const HEARTBEAT_MS = 5 * 60_000; // log "alive" every 5 min
|
|
662
1206
|
while (true) {
|
|
663
1207
|
try {
|
|
664
|
-
const updates = await
|
|
1208
|
+
const updates = await apiGet('getUpdates', {
|
|
665
1209
|
offset: String(offset),
|
|
666
|
-
limit: '
|
|
667
|
-
timeout: '
|
|
1210
|
+
limit: '1',
|
|
1211
|
+
timeout: '30', // long-poll up to 30s — reduces API calls
|
|
668
1212
|
});
|
|
669
1213
|
consecutiveErrors = 0;
|
|
670
1214
|
for (const update of updates) {
|
|
671
1215
|
offset = update.update_id + 1;
|
|
672
1216
|
saveOffset(offset);
|
|
673
|
-
|
|
674
|
-
|
|
675
|
-
|
|
676
|
-
|
|
677
|
-
|
|
678
|
-
|
|
679
|
-
|
|
680
|
-
|
|
681
|
-
|
|
682
|
-
|
|
683
|
-
|
|
684
|
-
|
|
685
|
-
|
|
686
|
-
|
|
687
|
-
// same pipeline as typed text ─────────────────────────────────────
|
|
688
|
-
let isVoiceInput = false;
|
|
689
|
-
let text = msg.text;
|
|
690
|
-
if (msg.voice) {
|
|
691
|
-
isVoiceInput = true;
|
|
692
|
-
try {
|
|
693
|
-
text = await transcribeVoiceMessage(msg.voice.file_id);
|
|
694
|
-
if (!text) {
|
|
695
|
-
await sendMessage(chatId, "🎤 Couldn't make out any words in that voice message — try again?");
|
|
696
|
-
continue;
|
|
1217
|
+
// Approval button taps (✅/❌) arrive as callback_query, not messages.
|
|
1218
|
+
const cb = update.callback_query;
|
|
1219
|
+
if (cb) {
|
|
1220
|
+
const cbAuthorized = isAuthorized(cb.from?.id);
|
|
1221
|
+
const data = cb.data ?? '';
|
|
1222
|
+
const m = data.match(/^(ok|no):(.+)$/);
|
|
1223
|
+
let note = 'Expired or already handled.';
|
|
1224
|
+
if (cbAuthorized && m) {
|
|
1225
|
+
const pending = pendingApprovals.get(m[2]);
|
|
1226
|
+
if (pending) {
|
|
1227
|
+
pendingApprovals.delete(m[2]);
|
|
1228
|
+
const approved = m[1] === 'ok';
|
|
1229
|
+
pending.resolve(approved);
|
|
1230
|
+
note = approved ? '✅ Approved' : '❌ Denied';
|
|
697
1231
|
}
|
|
698
|
-
console.log(`🎤 [${from}] transcribed: ${text}`);
|
|
699
1232
|
}
|
|
700
|
-
|
|
701
|
-
|
|
702
|
-
await sendMessage(chatId, `🎤 Couldn't transcribe that voice message: ${e.message}`);
|
|
703
|
-
continue;
|
|
1233
|
+
else if (!cbAuthorized) {
|
|
1234
|
+
note = '🚫 Not authorized.';
|
|
704
1235
|
}
|
|
705
|
-
|
|
706
|
-
if (!text)
|
|
707
|
-
continue;
|
|
708
|
-
console.log(`📩 [${from}]: ${text}`);
|
|
709
|
-
// ── Check pending approvals first ───────────────────────────────
|
|
710
|
-
const approvedId = confirmManager.handleReply(chatId, text);
|
|
711
|
-
if (approvedId) {
|
|
712
|
-
console.log(`📤 Approval resolved for ${approvedId}`);
|
|
713
|
-
continue;
|
|
714
|
-
}
|
|
715
|
-
// ── Handle commands (synchronous) ────────────────────────────────
|
|
716
|
-
const commandResult = handleCommand(chatId, text, from);
|
|
717
|
-
if (commandResult !== null) {
|
|
1236
|
+
// Acknowledge the tap (removes the spinner) and update the message.
|
|
718
1237
|
try {
|
|
719
|
-
await
|
|
720
|
-
console.log(`📤 Replied to ${from}`);
|
|
1238
|
+
await apiPost('answerCallbackQuery', { callback_query_id: cb.id, text: note });
|
|
721
1239
|
}
|
|
722
|
-
catch
|
|
723
|
-
|
|
1240
|
+
catch { /* ignore */ }
|
|
1241
|
+
try {
|
|
1242
|
+
await apiPost('editMessageText', {
|
|
1243
|
+
chat_id: cb.message?.chat?.id,
|
|
1244
|
+
message_id: cb.message?.message_id,
|
|
1245
|
+
text: `${cb.message?.text ?? ''}\n\n${note}`,
|
|
1246
|
+
});
|
|
724
1247
|
}
|
|
1248
|
+
catch { /* ignore */ }
|
|
725
1249
|
continue;
|
|
726
1250
|
}
|
|
727
|
-
|
|
728
|
-
if (
|
|
729
|
-
|
|
1251
|
+
const msg = update.message;
|
|
1252
|
+
if (!msg)
|
|
1253
|
+
continue;
|
|
1254
|
+
const chatId = msg.chat.id;
|
|
1255
|
+
const from = msg.from?.first_name ?? msg.from?.username ?? 'unknown';
|
|
1256
|
+
// AUTH GATE — refuse anyone not on the allowlist. The bot controls the
|
|
1257
|
+
// PC, so this is the primary security boundary.
|
|
1258
|
+
if (!isAuthorized(msg.from?.id)) {
|
|
1259
|
+
console.error(`[${ts()}] 🚫 Unauthorized ${from} (id ${msg.from?.id}) — refused: ${(msg.text ?? '(non-text)').slice(0, 60)}`);
|
|
1260
|
+
try {
|
|
1261
|
+
await sendMessage(chatId, '🚫 Not authorized.');
|
|
1262
|
+
}
|
|
1263
|
+
catch { /* ignore */ }
|
|
730
1264
|
continue;
|
|
731
1265
|
}
|
|
732
|
-
|
|
733
|
-
|
|
734
|
-
//
|
|
735
|
-
|
|
736
|
-
//
|
|
737
|
-
|
|
738
|
-
|
|
739
|
-
|
|
740
|
-
|
|
741
|
-
|
|
742
|
-
|
|
743
|
-
|
|
744
|
-
|
|
745
|
-
|
|
746
|
-
|
|
747
|
-
|
|
1266
|
+
// Handle the message WITHOUT blocking the poll loop. A command may wait
|
|
1267
|
+
// on an approval tap (requestApproval), and that tap arrives as a later
|
|
1268
|
+
// update — if we awaited the handler here, the loop couldn't fetch it
|
|
1269
|
+
// (deadlock). So process each message as a detached task; the loop keeps
|
|
1270
|
+
// polling and can deliver the callback that unblocks it.
|
|
1271
|
+
void (async () => {
|
|
1272
|
+
try {
|
|
1273
|
+
let text = msg.text ?? '';
|
|
1274
|
+
let cameFromVoice = false;
|
|
1275
|
+
const voice = msg.voice || msg.audio;
|
|
1276
|
+
if (!text && voice?.file_id) {
|
|
1277
|
+
cameFromVoice = true;
|
|
1278
|
+
const audioPath = await downloadTelegramFile(voice.file_id);
|
|
1279
|
+
text = await (0, dictate_js_1.transcribeFile)(audioPath);
|
|
1280
|
+
try {
|
|
1281
|
+
fs.unlinkSync(audioPath);
|
|
748
1282
|
}
|
|
1283
|
+
catch { }
|
|
1284
|
+
console.log(`[${ts()}] 🎤 [${from}] (voice): ${text}`);
|
|
1285
|
+
if (text)
|
|
1286
|
+
await sendMessage(chatId, `🎤 “${text}”`);
|
|
749
1287
|
}
|
|
750
|
-
|
|
751
|
-
|
|
752
|
-
|
|
753
|
-
|
|
754
|
-
|
|
1288
|
+
if (!text)
|
|
1289
|
+
return;
|
|
1290
|
+
if (!cameFromVoice)
|
|
1291
|
+
console.log(`[${ts()}] 📩 [${from}]: ${text}`);
|
|
1292
|
+
const lower = text.toLowerCase().trim();
|
|
1293
|
+
const conversational = !lower.startsWith('/') && !isDirectShellText(lower);
|
|
1294
|
+
const response = await handleCommand(chatId, text, from);
|
|
1295
|
+
await sendMessage(chatId, response);
|
|
1296
|
+
// Voice note alongside the text, per the audio-reply policy:
|
|
1297
|
+
// voice-in always speaks back; 'auto' also speaks substantial
|
|
1298
|
+
// conversational replies (task summaries), never command output.
|
|
1299
|
+
if ((0, telegram_audio_policy_js_1.shouldSendAudio)({
|
|
1300
|
+
mode: AUDIO_MODE, cameFromVoice, conversational,
|
|
1301
|
+
length: response.length, minChars: AUDIO_MIN_CHARS,
|
|
1302
|
+
})) {
|
|
1303
|
+
const spoken = (0, telegram_audio_policy_js_1.stripForSpeech)(response);
|
|
1304
|
+
if (spoken) {
|
|
1305
|
+
let voiceSent = false;
|
|
1306
|
+
let voiceError = '';
|
|
1307
|
+
// Path 1: Groq playai-tts via curl — returns OGG natively (no
|
|
1308
|
+
// ffmpeg step needed). This is the tested, reliable path.
|
|
1309
|
+
const groqKey = (0, env_js_1.getApiKey)('GROQ_API_KEY', 'groq_api_key');
|
|
1310
|
+
if (groqKey) {
|
|
1311
|
+
try {
|
|
1312
|
+
const ogg = await (0, telegram_voice_js_1.textToSpeech)(spoken, groqKey);
|
|
1313
|
+
await (0, telegram_voice_js_1.sendVoiceMessage)(TOKEN, chatId, ogg);
|
|
1314
|
+
voiceSent = true;
|
|
1315
|
+
}
|
|
1316
|
+
catch (e) {
|
|
1317
|
+
voiceError = e?.message || String(e);
|
|
1318
|
+
console.error(`[${ts()}] ⚠️ Groq TTS failed: ${voiceError}`);
|
|
1319
|
+
}
|
|
1320
|
+
}
|
|
1321
|
+
// Path 2 (fallback): MiMo TTS → WAV → ffmpeg Opus.
|
|
1322
|
+
if (!voiceSent) {
|
|
1323
|
+
try {
|
|
1324
|
+
const wav = await (0, dictate_js_1.synthesizeSpeech)(spoken);
|
|
1325
|
+
await sendVoice(chatId, wav);
|
|
1326
|
+
voiceSent = true;
|
|
1327
|
+
}
|
|
1328
|
+
catch (e) {
|
|
1329
|
+
voiceError = voiceError || (e?.message || String(e));
|
|
1330
|
+
console.error(`[${ts()}] ⚠️ MiMo TTS failed: ${voiceError}`);
|
|
1331
|
+
}
|
|
1332
|
+
}
|
|
1333
|
+
if (!voiceSent) {
|
|
1334
|
+
// Tell the user why there's no voice note instead of
|
|
1335
|
+
// silently swallowing the failure.
|
|
1336
|
+
console.error(`[${ts()}] ⚠️ Voice reply failed entirely: ${voiceError}`);
|
|
1337
|
+
}
|
|
1338
|
+
}
|
|
755
1339
|
}
|
|
1340
|
+
console.log(`[${ts()}] 📤 Replied to ${from}`);
|
|
756
1341
|
}
|
|
757
|
-
|
|
758
|
-
|
|
759
|
-
|
|
760
|
-
|
|
761
|
-
|
|
1342
|
+
catch (e) {
|
|
1343
|
+
console.error(`[${ts()}] ❌ Reply error: ${e.message}`);
|
|
1344
|
+
try {
|
|
1345
|
+
await sendMessage(chatId, `❌ Greška: ${e.message}`);
|
|
1346
|
+
}
|
|
1347
|
+
catch { /* give up */ }
|
|
762
1348
|
}
|
|
763
|
-
|
|
764
|
-
|
|
765
|
-
|
|
766
|
-
|
|
767
|
-
|
|
1349
|
+
})();
|
|
1350
|
+
}
|
|
1351
|
+
// Periodic heartbeat
|
|
1352
|
+
if (Date.now() - lastHeartbeat > HEARTBEAT_MS) {
|
|
1353
|
+
const uptime = process.uptime();
|
|
1354
|
+
const h = Math.floor(uptime / 3600);
|
|
1355
|
+
const m = Math.floor((uptime % 3600) / 60);
|
|
1356
|
+
const mem = Math.round(process.memoryUsage().heapUsed / 1024 / 1024);
|
|
1357
|
+
console.log(`[${ts()}] ❤️ Alive (uptime ${h}h${m}m, heap ${mem}MB)`);
|
|
1358
|
+
lastHeartbeat = Date.now();
|
|
768
1359
|
}
|
|
769
1360
|
}
|
|
770
1361
|
catch (e) {
|
|
771
1362
|
consecutiveErrors++;
|
|
772
|
-
|
|
1363
|
+
const msg = e?.message || String(e);
|
|
1364
|
+
const isConflict = msg.includes('409') || msg.includes('Conflict');
|
|
1365
|
+
const kind = msg.includes('timeout') ? 'timeout' : isConflict ? 'conflict' : 'api';
|
|
1366
|
+
console.error(`[${ts()}] ⚠️ Poll error (${consecutiveErrors}, ${kind}): ${msg}`);
|
|
773
1367
|
if (consecutiveErrors > 10) {
|
|
774
|
-
console.error(
|
|
775
|
-
await new Promise(
|
|
1368
|
+
console.error(`[${ts()}] 💀 Too many errors, waiting 30s…`);
|
|
1369
|
+
await new Promise(r => setTimeout(r, 30000));
|
|
776
1370
|
consecutiveErrors = 0;
|
|
777
1371
|
}
|
|
1372
|
+
else if (isConflict) {
|
|
1373
|
+
// 409 Conflict: another poll is active — wait 5s for it to settle
|
|
1374
|
+
await new Promise(r => setTimeout(r, 5000));
|
|
1375
|
+
}
|
|
778
1376
|
else {
|
|
779
|
-
|
|
1377
|
+
// Backoff: 2s → 4s → 8s … capped at 30s
|
|
1378
|
+
const delay = Math.min(2000 * Math.pow(2, consecutiveErrors - 1), 30000);
|
|
1379
|
+
await new Promise(r => setTimeout(r, delay));
|
|
780
1380
|
}
|
|
781
1381
|
}
|
|
782
1382
|
}
|
|
@@ -784,17 +1384,16 @@ async function poll() {
|
|
|
784
1384
|
// ─────────────────────────────────────────────────────────────────────────────
|
|
785
1385
|
// Entry
|
|
786
1386
|
// ─────────────────────────────────────────────────────────────────────────────
|
|
787
|
-
//
|
|
788
|
-
|
|
789
|
-
console.error(
|
|
790
|
-
|
|
791
|
-
|
|
792
|
-
|
|
793
|
-
console.error(
|
|
794
|
-
|
|
795
|
-
|
|
796
|
-
|
|
797
|
-
console.error('💀 Fatal polling crash:', err);
|
|
1387
|
+
// Survive unexpected crashes — log and keep polling
|
|
1388
|
+
process.on('uncaughtException', (e) => {
|
|
1389
|
+
console.error(`[${ts()}] 💥 Uncaught exception:`, e.message ?? e);
|
|
1390
|
+
// Don't exit — the polling loop handles errors internally
|
|
1391
|
+
});
|
|
1392
|
+
process.on('unhandledRejection', (reason) => {
|
|
1393
|
+
console.error(`[${ts()}] 💥 Unhandled rejection:`, reason?.message ?? String(reason));
|
|
1394
|
+
});
|
|
1395
|
+
poll().catch(e => {
|
|
1396
|
+
console.error(`[${ts()}] Fatal:`, e);
|
|
798
1397
|
process.exit(1);
|
|
799
1398
|
});
|
|
800
1399
|
//# sourceMappingURL=telegram-bot.js.map
|