aura-code 0.5.0 → 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/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 -23
- 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 +49 -13
- 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 -146
- 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 +1001 -788
- 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 -39
- 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 -36
- package/dist/dream/dream.js +327 -117
- 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/dream/okf.d.ts +10 -0
- package/dist/dream/okf.js +280 -0
- package/dist/dream/okf.js.map +1 -0
- package/dist/dream/parser.d.ts +60 -0
- package/dist/dream/parser.js +176 -0
- package/dist/dream/parser.js.map +1 -0
- package/dist/dream/reconcile.d.ts +69 -0
- package/dist/dream/reconcile.js +303 -0
- package/dist/dream/reconcile.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/index.d.ts +16 -0
- package/dist/machina/index.js +61 -0
- package/dist/machina/index.js.map +1 -0
- package/dist/machina/render-html.d.ts +2 -0
- package/dist/machina/render-html.js +191 -0
- package/dist/machina/render-html.js.map +1 -0
- package/dist/machina/render-terminal.d.ts +2 -0
- package/dist/machina/render-terminal.js +99 -0
- package/dist/machina/render-terminal.js.map +1 -0
- 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.d.ts +61 -0
- package/dist/machina/spec.js +124 -0
- package/dist/machina/spec.js.map +1 -0
- package/dist/machina/verify.d.ts +39 -0
- package/dist/machina/verify.js +95 -0
- package/dist/machina/verify.js.map +1 -0
- 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 +34 -6
- package/dist/providers/anthropic.js.map +1 -1
- package/dist/providers/factory.d.ts +71 -14
- package/dist/providers/factory.js +205 -124
- package/dist/providers/factory.js.map +1 -1
- package/dist/providers/google.js +8 -21
- 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/rem/graph.d.ts +72 -0
- package/dist/rem/graph.js +139 -0
- package/dist/rem/graph.js.map +1 -0
- package/dist/rem/index.d.ts +16 -0
- package/dist/rem/index.js +60 -0
- package/dist/rem/index.js.map +1 -0
- package/dist/rem/render-html.d.ts +2 -0
- package/dist/rem/render-html.js +189 -0
- package/dist/rem/render-html.js.map +1 -0
- package/dist/rem/render-terminal.d.ts +4 -0
- package/dist/rem/render-terminal.js +94 -0
- package/dist/rem/render-terminal.js.map +1 -0
- 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 +5 -2
- package/dist/research/council.js +73 -10
- 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 +6 -4
- 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/gmail-tool.d.ts +5 -1
- package/dist/tools/gmail-tool.js +151 -11
- package/dist/tools/gmail-tool.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 -539
- 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.js +2 -3
- 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/dist/viz/reader.d.ts +33 -0
- package/dist/viz/reader.js +343 -0
- package/dist/viz/reader.js.map +1 -0
- package/package.json +13 -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,206 +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 = process.env.AURA_BASE_URL
|
|
116
|
-
?? fileConfig.baseUrl
|
|
117
|
-
?? globalCfg?.baseUrl;
|
|
118
|
-
// Deliberately NOT falling back to the wizard's saved baseUrl here —
|
|
119
|
-
// createProvider() already applies it internally, but only when the
|
|
120
|
-
// saved config's model actually matches TASK_MODEL. Resolving it here
|
|
121
|
-
// too bypasses that check entirely, which is exactly how a MiMo model
|
|
122
|
-
// string previously ended up paired with a DeepSeek endpoint.
|
|
123
|
-
return (0, factory_js_1.createProvider)({
|
|
124
|
-
model: TASK_MODEL,
|
|
125
|
-
apiKey,
|
|
126
|
-
baseUrl,
|
|
127
|
-
});
|
|
128
|
-
}
|
|
129
|
-
// ─────────────────────────────────────────────────────────────────────────────
|
|
130
|
-
// Stub display for Telegram — silent, no terminal output
|
|
131
|
-
// ─────────────────────────────────────────────────────────────────────────────
|
|
132
|
-
function createSilentDisplay() {
|
|
133
|
-
return {
|
|
134
|
-
agentThinking() { },
|
|
135
|
-
streamText() { },
|
|
136
|
-
streamEnd() { },
|
|
137
|
-
toolStart() { },
|
|
138
|
-
toolCall() { },
|
|
139
|
-
toolResult() { },
|
|
140
|
-
toolBlocked() { },
|
|
141
|
-
warning() { },
|
|
142
|
-
success() { },
|
|
143
|
-
error() { },
|
|
144
|
-
header() { },
|
|
145
|
-
summary() { },
|
|
146
|
-
showPlan() { },
|
|
147
|
-
stepStarted() { },
|
|
148
|
-
stepCompleted() { },
|
|
149
|
-
};
|
|
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));
|
|
150
85
|
}
|
|
151
|
-
//
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
console.log("DEBUG-EXEC: executeTask entered");
|
|
156
|
-
let provider;
|
|
157
|
-
try {
|
|
158
|
-
console.log("DEBUG-EXEC: creating provider...");
|
|
159
|
-
provider = createLLMProvider();
|
|
160
|
-
console.log("DEBUG-EXEC: provider created");
|
|
161
|
-
}
|
|
162
|
-
catch (e) {
|
|
163
|
-
return `❌ Failed to create LLM provider: ${e.message}`;
|
|
164
|
-
}
|
|
165
|
-
let context;
|
|
166
|
-
try {
|
|
167
|
-
console.log("DEBUG-EXEC: loading context...");
|
|
168
|
-
context = await (0, context_js_1.loadProjectContext)(PROJECT_ROOT);
|
|
169
|
-
console.log("DEBUG-EXEC: context loaded");
|
|
170
|
-
}
|
|
171
|
-
catch (e) {
|
|
172
|
-
return `❌ Failed to load project context: ${e.message}`;
|
|
173
|
-
}
|
|
174
|
-
// Permissions:
|
|
175
|
-
// safety ON → 'normal' (triggers needsConfirm for destructive ops)
|
|
176
|
-
// safety OFF → 'auto' (no confirm needed)
|
|
177
|
-
const permLevel = safetyState.safetyOn ? 'normal' : 'auto';
|
|
178
|
-
const permissions = new permissions_js_1.PermissionSystem(permLevel, context.root);
|
|
179
|
-
const display = createSilentDisplay();
|
|
180
|
-
let confirmFn;
|
|
181
|
-
if (safetyState.safetyOn) {
|
|
182
|
-
confirmFn = async (message) => {
|
|
183
|
-
// Parse the description from the confirm message
|
|
184
|
-
// "Allow: $ <command>?" or "Allow: overwrite <path>?" or "Allow: toolName({...})?"
|
|
185
|
-
const desc = message
|
|
186
|
-
.replace(/^Allow:\s*/, '')
|
|
187
|
-
.replace(/\?$/, '')
|
|
188
|
-
.trim();
|
|
189
|
-
console.log(`Confirm requested: ${desc}`);
|
|
190
|
-
const { promise, message: approvalMsg } = await new Promise((resolve) => {
|
|
191
|
-
// We need to figure out which tool this is for from the message
|
|
192
|
-
let toolName = 'unknown';
|
|
193
|
-
let description = desc;
|
|
194
|
-
if (desc.startsWith('$ ')) {
|
|
195
|
-
toolName = 'run_shell';
|
|
196
|
-
description = desc.slice(2);
|
|
197
|
-
}
|
|
198
|
-
else if (desc.startsWith('overwrite ')) {
|
|
199
|
-
toolName = 'write_file';
|
|
200
|
-
description = desc;
|
|
201
|
-
}
|
|
202
|
-
const result = confirmManager.waitForApproval(chatId, toolName, description);
|
|
203
|
-
resolve(result);
|
|
204
|
-
});
|
|
205
|
-
// Send the approval request to the user
|
|
206
|
-
try {
|
|
207
|
-
await sendMessage(chatId, approvalMsg);
|
|
208
|
-
}
|
|
209
|
-
catch {
|
|
210
|
-
// If we can't send, deny for safety
|
|
211
|
-
return false;
|
|
212
|
-
}
|
|
213
|
-
return promise;
|
|
214
|
-
};
|
|
215
|
-
}
|
|
216
|
-
try {
|
|
217
|
-
console.log("Starting agent loop...");
|
|
218
|
-
const result = await (0, loop_js_1.runAgentLoop)({
|
|
219
|
-
provider,
|
|
220
|
-
task,
|
|
221
|
-
context,
|
|
222
|
-
permissions,
|
|
223
|
-
display,
|
|
224
|
-
maxTurns: 50,
|
|
225
|
-
disableSpawn: true, // no sub-agents via Telegram for now
|
|
226
|
-
confirmFn,
|
|
227
|
-
initialHistory: getChatHistory(chatId),
|
|
228
|
-
});
|
|
229
|
-
console.log("Agent loop completed");
|
|
230
|
-
setChatHistory(chatId, result.history);
|
|
231
|
-
// Build a readable response
|
|
232
|
-
const lines = [];
|
|
233
|
-
const tag = (0, telegram_safety_js_1.safetyLabel)(safetyState.safetyOn);
|
|
234
|
-
if (result.success) {
|
|
235
|
-
lines.push(`✅ Done (${result.turns} turn${result.turns !== 1 ? 's' : ''}) ${tag}`);
|
|
236
|
-
if (result.summary) {
|
|
237
|
-
// Trim the summary to a reasonable length
|
|
238
|
-
const summary = result.summary.length > 3000
|
|
239
|
-
? result.summary.slice(0, 3000) + '\n…(truncated)'
|
|
240
|
-
: result.summary;
|
|
241
|
-
lines.push('');
|
|
242
|
-
lines.push(summary);
|
|
243
|
-
}
|
|
244
|
-
}
|
|
245
|
-
else {
|
|
246
|
-
lines.push(`❌ Failed (${result.turns} turns) ${tag}`);
|
|
247
|
-
if (result.summary) {
|
|
248
|
-
lines.push('');
|
|
249
|
-
lines.push(result.summary);
|
|
250
|
-
}
|
|
251
|
-
}
|
|
252
|
-
return lines.join('\n');
|
|
253
|
-
}
|
|
254
|
-
catch (e) {
|
|
255
|
-
console.error("Agent loop error:", e);
|
|
256
|
-
return `❌ Task error: ${e.message}`;
|
|
257
|
-
}
|
|
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);
|
|
258
90
|
}
|
|
259
|
-
//
|
|
260
|
-
//
|
|
261
|
-
|
|
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 });
|
|
262
94
|
function loadOffset() {
|
|
263
95
|
try {
|
|
264
96
|
return parseInt(fs.readFileSync(OFFSET_FILE, 'utf8').trim(), 10) || 0;
|
|
@@ -270,73 +102,278 @@ function loadOffset() {
|
|
|
270
102
|
function saveOffset(offset) {
|
|
271
103
|
fs.writeFileSync(OFFSET_FILE, String(offset), 'utf8');
|
|
272
104
|
}
|
|
273
|
-
|
|
274
|
-
const data = body ? JSON.stringify(body) : '';
|
|
275
|
-
const url = `https://api.telegram.org/bot${TOKEN}/${method}`;
|
|
276
|
-
const escaped = data.replace(/'/g, "'\\''");
|
|
105
|
+
function apiPost(method, body) {
|
|
277
106
|
return new Promise((resolve, reject) => {
|
|
278
|
-
|
|
279
|
-
|
|
280
|
-
|
|
281
|
-
|
|
282
|
-
}
|
|
283
|
-
|
|
284
|
-
|
|
285
|
-
|
|
286
|
-
|
|
287
|
-
|
|
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
|
+
}
|
|
288
132
|
}
|
|
289
|
-
|
|
290
|
-
|
|
291
|
-
|
|
292
|
-
|
|
293
|
-
}
|
|
133
|
+
catch (e) {
|
|
134
|
+
reject(new Error(`Parse error: ${responseData.slice(0, 200)}`));
|
|
135
|
+
}
|
|
136
|
+
});
|
|
294
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();
|
|
295
146
|
});
|
|
296
147
|
}
|
|
297
|
-
|
|
298
|
-
const qs = params ? '?' + new URLSearchParams(params).toString() : '';
|
|
299
|
-
const url = `https://api.telegram.org/bot${TOKEN}/${method}${qs}`;
|
|
148
|
+
function apiGet(method, params) {
|
|
300
149
|
return new Promise((resolve, reject) => {
|
|
301
|
-
|
|
302
|
-
|
|
303
|
-
|
|
304
|
-
|
|
305
|
-
}
|
|
306
|
-
|
|
307
|
-
|
|
308
|
-
|
|
309
|
-
|
|
310
|
-
|
|
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
|
+
}
|
|
311
171
|
}
|
|
312
|
-
|
|
313
|
-
|
|
314
|
-
|
|
315
|
-
|
|
316
|
-
}
|
|
172
|
+
catch (e) {
|
|
173
|
+
reject(new Error(`Parse error: ${responseData.slice(0, 200)}`));
|
|
174
|
+
}
|
|
175
|
+
});
|
|
317
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();
|
|
318
183
|
});
|
|
319
184
|
}
|
|
320
|
-
async function sendMessage(chatId, text
|
|
185
|
+
async function sendMessage(chatId, text) {
|
|
321
186
|
const chunks = splitMessage(text, 4000);
|
|
322
187
|
for (const chunk of chunks) {
|
|
323
|
-
|
|
324
|
-
|
|
325
|
-
|
|
326
|
-
|
|
188
|
+
await apiPost('sendMessage', { chat_id: chatId, text: chunk });
|
|
189
|
+
}
|
|
190
|
+
}
|
|
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}`);
|
|
327
267
|
}
|
|
328
268
|
}
|
|
329
|
-
// Voice
|
|
330
|
-
|
|
331
|
-
|
|
332
|
-
|
|
333
|
-
|
|
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;
|
|
334
295
|
}
|
|
335
|
-
|
|
336
|
-
|
|
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})`);
|
|
337
363
|
}
|
|
338
|
-
|
|
339
|
-
|
|
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;
|
|
340
377
|
}
|
|
341
378
|
function splitMessage(text, maxLen) {
|
|
342
379
|
if (text.length <= maxLen)
|
|
@@ -350,27 +387,419 @@ function splitMessage(text, maxLen) {
|
|
|
350
387
|
return chunks;
|
|
351
388
|
}
|
|
352
389
|
// ─────────────────────────────────────────────────────────────────────────────
|
|
353
|
-
//
|
|
390
|
+
// LLM chat — answers free-form questions
|
|
354
391
|
// ─────────────────────────────────────────────────────────────────────────────
|
|
355
|
-
const
|
|
356
|
-
|
|
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
|
+
}
|
|
357
409
|
try {
|
|
358
|
-
const
|
|
359
|
-
|
|
360
|
-
|
|
361
|
-
|
|
362
|
-
|
|
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, []);
|
|
363
506
|
});
|
|
364
|
-
return
|
|
507
|
+
return chatHistory.get(chatId) || [];
|
|
365
508
|
}
|
|
366
|
-
|
|
367
|
-
|
|
368
|
-
|
|
369
|
-
|
|
370
|
-
|
|
371
|
-
|
|
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
|
+
}
|
|
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);
|
|
372
783
|
}
|
|
373
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
|
+
});
|
|
802
|
+
}
|
|
374
803
|
function readFileTool(filePath) {
|
|
375
804
|
const resolved = path.isAbsolute(filePath) ? filePath : path.join(DEFAULT_CWD, filePath);
|
|
376
805
|
if (!fs.existsSync(resolved))
|
|
@@ -401,124 +830,169 @@ function listDirTool(dirPath) {
|
|
|
401
830
|
return `❌ Error listing: ${e.message}`;
|
|
402
831
|
}
|
|
403
832
|
}
|
|
404
|
-
/** Escape a string for use inside double quotes in a shell command. */
|
|
405
|
-
function escapeShellDouble(s) {
|
|
406
|
-
return s.replace(/[\\"$]/g, '\\$&').replace(/`/g, '\\`');
|
|
407
|
-
}
|
|
408
833
|
function searchCodeTool(pattern, searchPath) {
|
|
409
834
|
const resolved = searchPath
|
|
410
835
|
? (path.isAbsolute(searchPath) ? searchPath : path.join(DEFAULT_CWD, searchPath))
|
|
411
836
|
: DEFAULT_CWD;
|
|
412
837
|
try {
|
|
413
|
-
const result = (0, child_process_1.execSync)(
|
|
414
|
-
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}"`;
|
|
415
840
|
}
|
|
416
841
|
catch {
|
|
417
|
-
return
|
|
842
|
+
return `No matches for "${pattern}" (or rg not installed)`;
|
|
418
843
|
}
|
|
419
844
|
}
|
|
420
|
-
|
|
421
|
-
|
|
422
|
-
|
|
423
|
-
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) {
|
|
424
852
|
const lower = text.toLowerCase().trim();
|
|
425
|
-
const tag = (0, telegram_safety_js_1.safetyLabel)(safetyState.safetyOn);
|
|
426
853
|
if (lower === '/start' || lower === '/help') {
|
|
427
854
|
return [
|
|
428
|
-
`💎 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`,
|
|
429
866
|
``,
|
|
430
|
-
|
|
431
|
-
`/
|
|
432
|
-
`/
|
|
433
|
-
`/
|
|
434
|
-
`/
|
|
435
|
-
`/
|
|
436
|
-
`/
|
|
437
|
-
`/ls <dir> — List directory`,
|
|
438
|
-
`/read <file> — Read file`,
|
|
439
|
-
`/search <pattern> — Search code`,
|
|
440
|
-
`/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)`,
|
|
441
874
|
`/git — Git status`,
|
|
442
|
-
`/cancel — Cancel stuck task`,
|
|
443
|
-
`/clear — Clear context`,
|
|
444
|
-
`/sur30 — Webcam surveillance 30 min`,
|
|
445
|
-
`/sur60 — Webcam surveillance 60 min`,
|
|
446
875
|
``,
|
|
447
|
-
|
|
448
|
-
|
|
449
|
-
|
|
450
|
-
if (lower === '/ping')
|
|
451
|
-
return `🏓 Pong! Aura is alive and running. ${tag}`;
|
|
452
|
-
if (lower === '/whoami') {
|
|
453
|
-
return [
|
|
454
|
-
`💎 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.`,
|
|
455
879
|
``,
|
|
456
|
-
|
|
457
|
-
|
|
458
|
-
|
|
459
|
-
`
|
|
460
|
-
`Tools: 22+`,
|
|
461
|
-
`Tests: 734+ passing`,
|
|
462
|
-
`Version: v0.3.0 (Aura rebrand)`,
|
|
463
|
-
`Mode: ${safetyState.safetyOn ? '🔒 Safe (asks before destructive ops)' : '🔓 Auto (no confirmation)'}`,
|
|
464
|
-
`Project: ${PROJECT_ROOT}`,
|
|
465
|
-
`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!`,
|
|
466
884
|
].join('\n');
|
|
467
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.
|
|
468
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
|
+
}
|
|
469
915
|
const uptime = process.uptime();
|
|
470
916
|
const hours = Math.floor(uptime / 3600);
|
|
471
917
|
const mins = Math.floor((uptime % 3600) / 60);
|
|
472
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
|
+
}
|
|
473
963
|
return [
|
|
474
|
-
|
|
475
|
-
`
|
|
476
|
-
|
|
477
|
-
|
|
478
|
-
|
|
479
|
-
|
|
480
|
-
|
|
481
|
-
|
|
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)`,
|
|
482
981
|
].join('\n');
|
|
483
982
|
}
|
|
484
983
|
if (lower === '/tools') {
|
|
485
984
|
return [
|
|
486
|
-
`🔧
|
|
985
|
+
`🔧 Dostupni alati:`,
|
|
487
986
|
``,
|
|
488
|
-
`📁 /ls <dir> —
|
|
489
|
-
`📄 /read <file> —
|
|
490
|
-
`🔍 /search <pattern> —
|
|
491
|
-
`⚡ /run <cmd> — shell
|
|
987
|
+
`📁 /ls <dir> — lista direktorijuma`,
|
|
988
|
+
`📄 /read <file> — čitanje fajla`,
|
|
989
|
+
`🔍 /search <pattern> — pretraga koda`,
|
|
990
|
+
`⚡ /run <cmd> — shell komanda`,
|
|
492
991
|
`🌿 /git — git status`,
|
|
493
|
-
`🧠 /
|
|
494
|
-
`🔒 /safety_on — enable safety`,
|
|
495
|
-
`🔓 /safety_off CONFIRM — disable safety`,
|
|
496
|
-
`🚫 /cancel — cancel stuck task`,
|
|
992
|
+
`🧠 /memory — pregled memorije`,
|
|
497
993
|
].join('\n');
|
|
498
994
|
}
|
|
499
|
-
// ──
|
|
500
|
-
if (lower === '/safety_on') {
|
|
501
|
-
safetyState.safetyOn = true;
|
|
502
|
-
saveState();
|
|
503
|
-
return `🔒 Safety mode enabled. ${(0, telegram_safety_js_1.safetyLabel)(true)} — destructive operations will ask for approval.`;
|
|
504
|
-
}
|
|
505
|
-
if (lower.startsWith('/safety_off')) {
|
|
506
|
-
const rest = text.slice('/safety_off'.length).trim();
|
|
507
|
-
if (rest !== 'CONFIRM') {
|
|
508
|
-
return [
|
|
509
|
-
`⚠️ To disable safety, send:`,
|
|
510
|
-
``,
|
|
511
|
-
`/safety_off CONFIRM`,
|
|
512
|
-
``,
|
|
513
|
-
`This prevents accidental toggling.`,
|
|
514
|
-
`Current state: ${(0, telegram_safety_js_1.safetyLabel)(safetyState.safetyOn)}`,
|
|
515
|
-
].join('\n');
|
|
516
|
-
}
|
|
517
|
-
safetyState.safetyOn = false;
|
|
518
|
-
saveState();
|
|
519
|
-
return `🔓 Safety mode disabled. ${(0, telegram_safety_js_1.safetyLabel)(false)} — all operations will execute without confirmation.`;
|
|
520
|
-
}
|
|
521
|
-
// ── Tool commands ───────────────────────────────────────────────────────
|
|
995
|
+
// ── Tool commands ──────────────────────────────────────────────────────
|
|
522
996
|
if (lower.startsWith('/ls')) {
|
|
523
997
|
const dir = text.slice(3).trim() || '.';
|
|
524
998
|
return `📁 ${dir}:\n${listDirTool(dir)}`;
|
|
@@ -539,246 +1013,370 @@ function handleCommand(chatId, text, from) {
|
|
|
539
1013
|
const cmd = text.slice(4).trim();
|
|
540
1014
|
if (!cmd)
|
|
541
1015
|
return '❌ Usage: /run <command>';
|
|
542
|
-
|
|
543
|
-
|
|
544
|
-
|
|
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.';
|
|
545
1024
|
}
|
|
546
|
-
const result = execShell(cmd);
|
|
1025
|
+
const result = await execShell(cmd);
|
|
547
1026
|
const output = result.stdout || result.stderr || '(no output)';
|
|
548
1027
|
const truncated = output.length > 3500 ? output.slice(0, 3500) + '\n... (truncated)' : output;
|
|
549
1028
|
return `⚡ ${cmd}\n${result.code === 0 ? '✅' : '❌'} exit ${result.code}\n${truncated}`;
|
|
550
1029
|
}
|
|
551
1030
|
if (lower === '/git') {
|
|
552
|
-
const result = execShell('git status --short && echo "---" && git log --oneline -5');
|
|
1031
|
+
const result = await execShell('git status --short && echo "---" && git log --oneline -5');
|
|
553
1032
|
return `🌿 Git:\n${result.stdout || '(not a git repo)'}`;
|
|
554
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
|
+
}
|
|
555
1063
|
if (lower === '/clear') {
|
|
556
|
-
|
|
557
|
-
|
|
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!';
|
|
558
1070
|
}
|
|
559
|
-
|
|
560
|
-
|
|
561
|
-
|
|
562
|
-
|
|
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}`;
|
|
563
1079
|
}
|
|
564
|
-
|
|
565
|
-
|
|
566
|
-
|
|
567
|
-
|
|
568
|
-
|
|
569
|
-
|
|
570
|
-
|
|
571
|
-
|
|
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}`;
|
|
572
1112
|
}
|
|
573
|
-
// Run in background — don't block the bot
|
|
574
|
-
(0, child_process_1.exec)(`nohup bash "${script}" ${duration} > /tmp/sur-${duration}-$(date +%s).log 2>&1 &`, {
|
|
575
|
-
cwd: PROJECT_ROOT,
|
|
576
|
-
timeout: 5_000,
|
|
577
|
-
encoding: 'utf8',
|
|
578
|
-
});
|
|
579
|
-
return `🔒 Surveillance started — ${duration} minutes, snapshots every 5 min.\nYou'll get photos in this chat. ${tag}`;
|
|
580
1113
|
}
|
|
581
|
-
|
|
582
|
-
|
|
583
|
-
|
|
584
|
-
|
|
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);
|
|
585
1155
|
const output = result.stdout || result.stderr || '(no output)';
|
|
586
1156
|
const truncated = output.length > 3500 ? output.slice(0, 3500) + '\n... (truncated)' : output;
|
|
587
1157
|
return `⚡ ${text}\n${truncated}`;
|
|
588
1158
|
}
|
|
589
|
-
//
|
|
590
|
-
|
|
591
|
-
|
|
592
|
-
//
|
|
593
|
-
//
|
|
594
|
-
|
|
595
|
-
function isAuthorized(chatId, fromUser) {
|
|
596
|
-
// No open-access fallback — if no IDs configured the bot won't start,
|
|
597
|
-
// but guard here too as defense-in-depth.
|
|
598
|
-
if (AUTHORIZED_USER_IDS.size === 0)
|
|
599
|
-
return false;
|
|
600
|
-
// Check by user ID
|
|
601
|
-
const userId = fromUser?.id;
|
|
602
|
-
if (userId && AUTHORIZED_USER_IDS.has(Number(userId)))
|
|
603
|
-
return true;
|
|
604
|
-
// Also check chat ID (for group chats where the user might be different)
|
|
605
|
-
if (AUTHORIZED_USER_IDS.has(chatId))
|
|
606
|
-
return true;
|
|
607
|
-
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);
|
|
608
1165
|
}
|
|
609
1166
|
// ─────────────────────────────────────────────────────────────────────────────
|
|
610
1167
|
// Main polling loop
|
|
611
1168
|
// ─────────────────────────────────────────────────────────────────────────────
|
|
612
|
-
|
|
613
|
-
|
|
614
|
-
* If a task is running, new non-command messages are queued.
|
|
615
|
-
*/
|
|
616
|
-
const runningTasks = new Set();
|
|
617
|
-
/**
|
|
618
|
-
* Per-chat conversation history. Without this, every Telegram message
|
|
619
|
-
* started a brand-new `runAgentLoop` call with no `initialHistory` — so
|
|
620
|
-
* replies like "B", "Ok", or "/approve" to a previous question had zero
|
|
621
|
-
* context, and Aura would respond as if the conversation had never
|
|
622
|
-
* happened. Keyed by chatId, threaded into runAgentLoop below, and updated
|
|
623
|
-
* with the loop's returned history after each task — same pattern the CLI
|
|
624
|
-
* REPL uses for `activeChatHistory`. Capped per-chat to avoid unbounded
|
|
625
|
-
* growth in a long-running bot process.
|
|
626
|
-
*/
|
|
627
|
-
const chatHistories = new Map();
|
|
628
|
-
const MAX_HISTORY_MESSAGES = 200;
|
|
629
|
-
function getChatHistory(chatId) {
|
|
630
|
-
return chatHistories.get(chatId) ?? [];
|
|
631
|
-
}
|
|
632
|
-
function setChatHistory(chatId, history) {
|
|
633
|
-
const trimmed = history.length > MAX_HISTORY_MESSAGES
|
|
634
|
-
? history.slice(history.length - MAX_HISTORY_MESSAGES)
|
|
635
|
-
: history;
|
|
636
|
-
chatHistories.set(chatId, trimmed);
|
|
1169
|
+
function ts() {
|
|
1170
|
+
return new Date().toISOString().replace('T', ' ').slice(0, 19);
|
|
637
1171
|
}
|
|
638
1172
|
async function poll() {
|
|
639
1173
|
let offset = loadOffset();
|
|
640
|
-
console.log(
|
|
641
|
-
console.log(` Bot: @
|
|
1174
|
+
console.log(`[${ts()}] 💎 Aura Telegram Bot started`);
|
|
1175
|
+
console.log(` Bot: @Aura_Code_bot`);
|
|
642
1176
|
console.log(` Offset: ${offset}`);
|
|
643
|
-
console.log(`
|
|
644
|
-
console.log(` Project root: ${PROJECT_ROOT}`);
|
|
645
|
-
console.log(` Task model: ${TASK_MODEL}`);
|
|
646
|
-
console.log(` Safety: ${safetyState.safetyOn ? 'ON' : 'OFF'}`);
|
|
647
|
-
console.log(` Authorized users: ${AUTHORIZED_USER_IDS.size > 0 ? [...AUTHORIZED_USER_IDS].join(', ') : 'ALL (no restriction)'}`);
|
|
1177
|
+
console.log(` Long-polling Telegram (30s)…`);
|
|
648
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
|
+
}
|
|
649
1189
|
// Clear old updates on first run
|
|
650
1190
|
if (offset === 0) {
|
|
651
1191
|
try {
|
|
652
|
-
const updates = await
|
|
1192
|
+
const updates = await apiGet('getUpdates', { offset: '0', limit: '100' });
|
|
653
1193
|
if (updates.length > 0) {
|
|
654
1194
|
offset = updates[updates.length - 1].update_id + 1;
|
|
655
1195
|
saveOffset(offset);
|
|
656
|
-
console.log(` Cleared ${updates.length} old update(s), offset: ${offset}`);
|
|
1196
|
+
console.log(`[${ts()}] Cleared ${updates.length} old update(s), offset: ${offset}`);
|
|
657
1197
|
}
|
|
658
1198
|
}
|
|
659
1199
|
catch (e) {
|
|
660
|
-
console.error(` ⚠️ Clear error: ${e.message}`);
|
|
1200
|
+
console.error(`[${ts()}] ⚠️ Clear error: ${e.message}`);
|
|
661
1201
|
}
|
|
662
1202
|
}
|
|
663
1203
|
let consecutiveErrors = 0;
|
|
1204
|
+
let lastHeartbeat = Date.now();
|
|
1205
|
+
const HEARTBEAT_MS = 5 * 60_000; // log "alive" every 5 min
|
|
664
1206
|
while (true) {
|
|
665
1207
|
try {
|
|
666
|
-
const updates = await
|
|
1208
|
+
const updates = await apiGet('getUpdates', {
|
|
667
1209
|
offset: String(offset),
|
|
668
|
-
limit: '
|
|
669
|
-
timeout: '
|
|
1210
|
+
limit: '1',
|
|
1211
|
+
timeout: '30', // long-poll up to 30s — reduces API calls
|
|
670
1212
|
});
|
|
671
1213
|
consecutiveErrors = 0;
|
|
672
1214
|
for (const update of updates) {
|
|
673
1215
|
offset = update.update_id + 1;
|
|
674
1216
|
saveOffset(offset);
|
|
675
|
-
|
|
676
|
-
|
|
677
|
-
|
|
678
|
-
|
|
679
|
-
|
|
680
|
-
|
|
681
|
-
|
|
682
|
-
|
|
683
|
-
|
|
684
|
-
|
|
685
|
-
|
|
686
|
-
|
|
687
|
-
|
|
688
|
-
|
|
689
|
-
// same pipeline as typed text ─────────────────────────────────────
|
|
690
|
-
let isVoiceInput = false;
|
|
691
|
-
let text = msg.text;
|
|
692
|
-
if (msg.voice) {
|
|
693
|
-
isVoiceInput = true;
|
|
694
|
-
try {
|
|
695
|
-
text = await transcribeVoiceMessage(msg.voice.file_id);
|
|
696
|
-
if (!text) {
|
|
697
|
-
await sendMessage(chatId, "🎤 Couldn't make out any words in that voice message — try again?");
|
|
698
|
-
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';
|
|
699
1231
|
}
|
|
700
|
-
console.log(`🎤 [${from}] transcribed: ${text}`);
|
|
701
1232
|
}
|
|
702
|
-
|
|
703
|
-
|
|
704
|
-
await sendMessage(chatId, `🎤 Couldn't transcribe that voice message: ${e.message}`);
|
|
705
|
-
continue;
|
|
1233
|
+
else if (!cbAuthorized) {
|
|
1234
|
+
note = '🚫 Not authorized.';
|
|
706
1235
|
}
|
|
707
|
-
|
|
708
|
-
if (!text)
|
|
709
|
-
continue;
|
|
710
|
-
console.log(`📩 [${from}]: ${text}`);
|
|
711
|
-
// ── Check pending approvals first ───────────────────────────────
|
|
712
|
-
const approvedId = confirmManager.handleReply(chatId, text);
|
|
713
|
-
if (approvedId) {
|
|
714
|
-
console.log(`📤 Approval resolved for ${approvedId}`);
|
|
715
|
-
continue;
|
|
716
|
-
}
|
|
717
|
-
// ── Handle commands (synchronous) ────────────────────────────────
|
|
718
|
-
const commandResult = handleCommand(chatId, text, from);
|
|
719
|
-
if (commandResult !== null) {
|
|
1236
|
+
// Acknowledge the tap (removes the spinner) and update the message.
|
|
720
1237
|
try {
|
|
721
|
-
await
|
|
722
|
-
console.log(`📤 Replied to ${from}`);
|
|
1238
|
+
await apiPost('answerCallbackQuery', { callback_query_id: cb.id, text: note });
|
|
723
1239
|
}
|
|
724
|
-
catch
|
|
725
|
-
|
|
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
|
+
});
|
|
726
1247
|
}
|
|
1248
|
+
catch { /* ignore */ }
|
|
727
1249
|
continue;
|
|
728
1250
|
}
|
|
729
|
-
|
|
730
|
-
if (
|
|
731
|
-
|
|
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 */ }
|
|
732
1264
|
continue;
|
|
733
1265
|
}
|
|
734
|
-
|
|
735
|
-
|
|
736
|
-
//
|
|
737
|
-
|
|
738
|
-
//
|
|
739
|
-
|
|
740
|
-
|
|
741
|
-
|
|
742
|
-
|
|
743
|
-
|
|
744
|
-
|
|
745
|
-
|
|
746
|
-
|
|
747
|
-
|
|
748
|
-
|
|
749
|
-
|
|
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);
|
|
750
1282
|
}
|
|
1283
|
+
catch { }
|
|
1284
|
+
console.log(`[${ts()}] 🎤 [${from}] (voice): ${text}`);
|
|
1285
|
+
if (text)
|
|
1286
|
+
await sendMessage(chatId, `🎤 “${text}”`);
|
|
751
1287
|
}
|
|
752
|
-
|
|
753
|
-
|
|
754
|
-
|
|
755
|
-
|
|
756
|
-
|
|
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
|
+
}
|
|
757
1339
|
}
|
|
1340
|
+
console.log(`[${ts()}] 📤 Replied to ${from}`);
|
|
758
1341
|
}
|
|
759
|
-
|
|
760
|
-
|
|
761
|
-
|
|
762
|
-
|
|
763
|
-
|
|
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 */ }
|
|
764
1348
|
}
|
|
765
|
-
|
|
766
|
-
|
|
767
|
-
|
|
768
|
-
|
|
769
|
-
|
|
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();
|
|
770
1359
|
}
|
|
771
1360
|
}
|
|
772
1361
|
catch (e) {
|
|
773
1362
|
consecutiveErrors++;
|
|
774
|
-
|
|
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}`);
|
|
775
1367
|
if (consecutiveErrors > 10) {
|
|
776
|
-
console.error(
|
|
777
|
-
await new Promise(
|
|
1368
|
+
console.error(`[${ts()}] 💀 Too many errors, waiting 30s…`);
|
|
1369
|
+
await new Promise(r => setTimeout(r, 30000));
|
|
778
1370
|
consecutiveErrors = 0;
|
|
779
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
|
+
}
|
|
780
1376
|
else {
|
|
781
|
-
|
|
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));
|
|
782
1380
|
}
|
|
783
1381
|
}
|
|
784
1382
|
}
|
|
@@ -786,17 +1384,16 @@ async function poll() {
|
|
|
786
1384
|
// ─────────────────────────────────────────────────────────────────────────────
|
|
787
1385
|
// Entry
|
|
788
1386
|
// ─────────────────────────────────────────────────────────────────────────────
|
|
789
|
-
//
|
|
790
|
-
|
|
791
|
-
console.error(
|
|
792
|
-
|
|
793
|
-
|
|
794
|
-
|
|
795
|
-
console.error(
|
|
796
|
-
|
|
797
|
-
|
|
798
|
-
|
|
799
|
-
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);
|
|
800
1397
|
process.exit(1);
|
|
801
1398
|
});
|
|
802
1399
|
//# sourceMappingURL=telegram-bot.js.map
|