aura-code 0.6.3 → 0.10.1

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