aura-code 0.5.0 → 0.10.1

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