auramaxx 1.0.0-alpha.4

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 (363) hide show
  1. package/LICENSE +26 -0
  2. package/README.md +112 -0
  3. package/bin/aurawallet.js +121 -0
  4. package/docs/ADAPTERS.md +467 -0
  5. package/docs/API.md +2679 -0
  6. package/docs/APPS.md +198 -0
  7. package/docs/ARCHITECTURE.md +350 -0
  8. package/docs/AUTH.md +698 -0
  9. package/docs/BEST-PRACTICES.md +121 -0
  10. package/docs/CLI.md +61 -0
  11. package/docs/DEVELOPING-APPS.md +452 -0
  12. package/docs/EXTENSION.md +97 -0
  13. package/docs/JOBS.md +33 -0
  14. package/docs/MCP.md +76 -0
  15. package/docs/PROTOCOL.md +142 -0
  16. package/docs/SETUP.md +219 -0
  17. package/docs/WORKSPACE.md +672 -0
  18. package/docs/agent-auth.md +63 -0
  19. package/docs/aura-file.md +48 -0
  20. package/docs/credentials.md +53 -0
  21. package/docs/external/getting-started.md +65 -0
  22. package/docs/external/overview.md +45 -0
  23. package/docs/external/use-cases.md +48 -0
  24. package/docs/external/why-aura.md +35 -0
  25. package/docs/jobs/connect-agent.md +77 -0
  26. package/docs/jobs/migrate-from-dotenv.md +79 -0
  27. package/docs/jobs/recover-from-lockout.md +72 -0
  28. package/docs/jobs/secure-ci.md +63 -0
  29. package/docs/oauth2.md +42 -0
  30. package/docs/passkeys.md +60 -0
  31. package/docs/security.md +540 -0
  32. package/docs/specs/aura-open-protocol.md +61 -0
  33. package/docs/specs/aura-provider-plugin.md +24 -0
  34. package/docs/specs/aura-registry-model.md +31 -0
  35. package/docs/specs/fixtures/invalid-bad-key.aura +1 -0
  36. package/docs/specs/fixtures/invalid-bad-unicode-escape.aura +1 -0
  37. package/docs/specs/fixtures/invalid-duplicate-key.aura +2 -0
  38. package/docs/specs/fixtures/valid-basic.aura +4 -0
  39. package/docs/specs/fixtures/valid-provider-ref.aura +1 -0
  40. package/docs/specs/fixtures/valid-quoted-escapes.aura +2 -0
  41. package/docs/templates/RELEASE_NOTES_TEMPLATE.md +22 -0
  42. package/docs/totp.md +40 -0
  43. package/docs/wallet/AI.md +508 -0
  44. package/docs/wallet/DEVELOPING-STRATEGIES.md +713 -0
  45. package/docs/wallet/README.md +47 -0
  46. package/docs/wallet/STRATEGY.md +89 -0
  47. package/next.config.ts +21 -0
  48. package/package.json +151 -0
  49. package/postcss.config.mjs +8 -0
  50. package/prisma/migrations/20260214170000_baseline/migration.sql +511 -0
  51. package/prisma/migrations/20260216214537_add_passkey_model/migration.sql +18 -0
  52. package/prisma/migrations/20260217150500_add_credential_access_audit/migration.sql +31 -0
  53. package/prisma/migrations/migration_lock.toml +3 -0
  54. package/prisma/schema.prisma +447 -0
  55. package/public/logo-chevron.svg +31 -0
  56. package/public/logo-concentric.svg +31 -0
  57. package/public/logo-crosshatch.svg +39 -0
  58. package/public/logo-dashed.svg +39 -0
  59. package/public/logo-horizontal.svg +31 -0
  60. package/public/logo-m56.svg +64 -0
  61. package/public/logo.webp +0 -0
  62. package/scripts/add-app.js +245 -0
  63. package/scripts/init.sh +57 -0
  64. package/scripts/migrate-apikeys-to-credentials.ts +35 -0
  65. package/scripts/sandbox-agent-flow.sh +235 -0
  66. package/scripts/sandbox.sh +175 -0
  67. package/scripts/validate-job-docs.mjs +125 -0
  68. package/server/abi/SwapHelper.json +438 -0
  69. package/server/cli/approval.ts +447 -0
  70. package/server/cli/commands/app.ts +204 -0
  71. package/server/cli/commands/cron.ts +24 -0
  72. package/server/cli/commands/doctor.ts +1007 -0
  73. package/server/cli/commands/env.ts +456 -0
  74. package/server/cli/commands/init.ts +752 -0
  75. package/server/cli/commands/mcp.ts +125 -0
  76. package/server/cli/commands/restore.ts +314 -0
  77. package/server/cli/commands/shell-hook.ts +468 -0
  78. package/server/cli/commands/start.ts +62 -0
  79. package/server/cli/commands/status.ts +59 -0
  80. package/server/cli/commands/stop.ts +14 -0
  81. package/server/cli/commands/token.ts +180 -0
  82. package/server/cli/commands/unlock.ts +49 -0
  83. package/server/cli/commands/vault.ts +417 -0
  84. package/server/cli/index.ts +328 -0
  85. package/server/cli/lib/aura-parser.ts +64 -0
  86. package/server/cli/lib/credential-create.ts +74 -0
  87. package/server/cli/lib/credential-resolve.ts +254 -0
  88. package/server/cli/lib/dotenv-migrate.ts +116 -0
  89. package/server/cli/lib/dotenv-parser.ts +146 -0
  90. package/server/cli/lib/http.ts +91 -0
  91. package/server/cli/lib/init-steps.ts +76 -0
  92. package/server/cli/lib/local-agent-trust.ts +45 -0
  93. package/server/cli/lib/process.ts +136 -0
  94. package/server/cli/lib/prompt.ts +85 -0
  95. package/server/cli/lib/theme.ts +240 -0
  96. package/server/cli/socket.ts +570 -0
  97. package/server/cli/transport-client.ts +50 -0
  98. package/server/cron/index.ts +137 -0
  99. package/server/cron/job.ts +31 -0
  100. package/server/cron/jobs/balance-sync.ts +436 -0
  101. package/server/cron/jobs/incoming-scan.ts +506 -0
  102. package/server/cron/jobs/native-price.ts +70 -0
  103. package/server/cron/jobs/orphan-cleanup.ts +40 -0
  104. package/server/cron/jobs/strategy-runner.ts +175 -0
  105. package/server/cron/scheduler.ts +125 -0
  106. package/server/index.ts +406 -0
  107. package/server/lib/adapters/factory.ts +110 -0
  108. package/server/lib/adapters/index.ts +19 -0
  109. package/server/lib/adapters/router.ts +297 -0
  110. package/server/lib/adapters/telegram.ts +645 -0
  111. package/server/lib/adapters/types.ts +89 -0
  112. package/server/lib/adapters/webhook.ts +95 -0
  113. package/server/lib/address.ts +49 -0
  114. package/server/lib/agent-auth/contracts.ts +1194 -0
  115. package/server/lib/agent-profiles.ts +328 -0
  116. package/server/lib/ai.ts +285 -0
  117. package/server/lib/api-registry/contracts.ts +86 -0
  118. package/server/lib/api-registry/validation.ts +172 -0
  119. package/server/lib/apikey-migration.ts +189 -0
  120. package/server/lib/app-installer.ts +505 -0
  121. package/server/lib/app-tokens.ts +247 -0
  122. package/server/lib/auth.ts +314 -0
  123. package/server/lib/batch.ts +242 -0
  124. package/server/lib/cold.ts +874 -0
  125. package/server/lib/config.ts +381 -0
  126. package/server/lib/credential-access-audit.ts +85 -0
  127. package/server/lib/credential-access-policy.ts +110 -0
  128. package/server/lib/credential-health.ts +343 -0
  129. package/server/lib/credential-import.ts +487 -0
  130. package/server/lib/credential-scope.ts +87 -0
  131. package/server/lib/credential-shares.ts +190 -0
  132. package/server/lib/credential-transport.ts +342 -0
  133. package/server/lib/credential-vault.ts +77 -0
  134. package/server/lib/credentials.ts +333 -0
  135. package/server/lib/crypto.ts +8 -0
  136. package/server/lib/db.ts +15 -0
  137. package/server/lib/defaults.ts +366 -0
  138. package/server/lib/dex/index.ts +80 -0
  139. package/server/lib/dex/relay.ts +235 -0
  140. package/server/lib/dex/types.ts +59 -0
  141. package/server/lib/dex/uniswap.ts +370 -0
  142. package/server/lib/e2e-agent/artifacts.ts +36 -0
  143. package/server/lib/e2e-agent/contracts.ts +112 -0
  144. package/server/lib/e2e-agent/validation.ts +135 -0
  145. package/server/lib/encrypt.ts +128 -0
  146. package/server/lib/error.ts +20 -0
  147. package/server/lib/events.ts +205 -0
  148. package/server/lib/hot.ts +357 -0
  149. package/server/lib/key-fingerprint.ts +28 -0
  150. package/server/lib/logger.ts +331 -0
  151. package/server/lib/network.ts +137 -0
  152. package/server/lib/notifications.ts +219 -0
  153. package/server/lib/oauth2-refresh.ts +241 -0
  154. package/server/lib/oursecret.ts +54 -0
  155. package/server/lib/passkey-credential.ts +360 -0
  156. package/server/lib/passkey.ts +68 -0
  157. package/server/lib/permissions.ts +248 -0
  158. package/server/lib/pino.ts +24 -0
  159. package/server/lib/policy-preview.ts +138 -0
  160. package/server/lib/price.ts +338 -0
  161. package/server/lib/prices.ts +34 -0
  162. package/server/lib/project-scope.ts +239 -0
  163. package/server/lib/resolve-action.ts +427 -0
  164. package/server/lib/resolve.ts +36 -0
  165. package/server/lib/sessions.ts +632 -0
  166. package/server/lib/solana/connection.ts +26 -0
  167. package/server/lib/solana/jupiter.ts +128 -0
  168. package/server/lib/solana/transfer.ts +108 -0
  169. package/server/lib/solana/wallet.ts +136 -0
  170. package/server/lib/strategy/emits.ts +21 -0
  171. package/server/lib/strategy/engine.ts +1305 -0
  172. package/server/lib/strategy/executor.ts +115 -0
  173. package/server/lib/strategy/hook-context.ts +158 -0
  174. package/server/lib/strategy/hooks.ts +990 -0
  175. package/server/lib/strategy/index.ts +28 -0
  176. package/server/lib/strategy/installer.ts +305 -0
  177. package/server/lib/strategy/loader.ts +256 -0
  178. package/server/lib/strategy/message.ts +235 -0
  179. package/server/lib/strategy/repository.ts +218 -0
  180. package/server/lib/strategy/session-logger.ts +693 -0
  181. package/server/lib/strategy/sources.ts +288 -0
  182. package/server/lib/strategy/state.ts +189 -0
  183. package/server/lib/strategy/templates.ts +403 -0
  184. package/server/lib/strategy/tick.ts +404 -0
  185. package/server/lib/strategy/types.ts +230 -0
  186. package/server/lib/swap.ts +3 -0
  187. package/server/lib/temp.ts +86 -0
  188. package/server/lib/token-metadata.ts +86 -0
  189. package/server/lib/token-safety.ts +200 -0
  190. package/server/lib/token-search.ts +444 -0
  191. package/server/lib/totp.ts +194 -0
  192. package/server/lib/transactions.ts +123 -0
  193. package/server/lib/transport.ts +75 -0
  194. package/server/lib/txhistory/decoder.ts +262 -0
  195. package/server/lib/txhistory/enricher.ts +652 -0
  196. package/server/lib/txhistory/index.ts +391 -0
  197. package/server/lib/txhistory/signatures.ts +59 -0
  198. package/server/lib/verified-summary.ts +421 -0
  199. package/server/mcp/profile-policy.ts +30 -0
  200. package/server/mcp/server.ts +619 -0
  201. package/server/mcp/tools.ts +523 -0
  202. package/server/middleware/auth.ts +119 -0
  203. package/server/middleware/requestLogger.ts +84 -0
  204. package/server/routes/actions.ts +459 -0
  205. package/server/routes/adapters.ts +703 -0
  206. package/server/routes/addressbook.ts +113 -0
  207. package/server/routes/ai.ts +34 -0
  208. package/server/routes/apikeys.ts +295 -0
  209. package/server/routes/apps.ts +601 -0
  210. package/server/routes/auth.ts +457 -0
  211. package/server/routes/backup.ts +340 -0
  212. package/server/routes/batch.ts +270 -0
  213. package/server/routes/bookmarks.ts +162 -0
  214. package/server/routes/credential-shares.ts +198 -0
  215. package/server/routes/credential-vaults.ts +154 -0
  216. package/server/routes/credentials.ts +1290 -0
  217. package/server/routes/dashboard.ts +71 -0
  218. package/server/routes/defaults.ts +124 -0
  219. package/server/routes/fund.ts +229 -0
  220. package/server/routes/import.ts +352 -0
  221. package/server/routes/launch.ts +665 -0
  222. package/server/routes/lock.ts +54 -0
  223. package/server/routes/logs.ts +68 -0
  224. package/server/routes/nuke.ts +111 -0
  225. package/server/routes/passkey-credentials.ts +99 -0
  226. package/server/routes/passkey.ts +346 -0
  227. package/server/routes/portfolio.ts +217 -0
  228. package/server/routes/price.ts +63 -0
  229. package/server/routes/resolve.ts +31 -0
  230. package/server/routes/security.ts +45 -0
  231. package/server/routes/send-evm.ts +241 -0
  232. package/server/routes/send-solana.ts +281 -0
  233. package/server/routes/send.ts +178 -0
  234. package/server/routes/setup.ts +210 -0
  235. package/server/routes/strategy.ts +894 -0
  236. package/server/routes/swap-evm.ts +353 -0
  237. package/server/routes/swap-solana.ts +177 -0
  238. package/server/routes/swap.ts +356 -0
  239. package/server/routes/token.ts +247 -0
  240. package/server/routes/unlock.ts +403 -0
  241. package/server/routes/wallet-assets.ts +361 -0
  242. package/server/routes/wallet-transactions.ts +515 -0
  243. package/server/routes/wallet.ts +710 -0
  244. package/server/types.ts +146 -0
  245. package/skills/aurawallet/SKILL.md +739 -0
  246. package/skills/aurawallet-setup/SKILL.md +74 -0
  247. package/skills/security-review/SKILL.md +148 -0
  248. package/src/app/api/agent-requests/route.ts +30 -0
  249. package/src/app/api/apps/install/route.ts +126 -0
  250. package/src/app/api/apps/manifests/route.ts +16 -0
  251. package/src/app/api/apps/static/[...path]/route.ts +57 -0
  252. package/src/app/api/events/route.ts +92 -0
  253. package/src/app/api/page.tsx +212 -0
  254. package/src/app/api/workspace/[id]/apps/[wid]/route.ts +119 -0
  255. package/src/app/api/workspace/[id]/apps/route.ts +81 -0
  256. package/src/app/api/workspace/[id]/export/route.ts +67 -0
  257. package/src/app/api/workspace/[id]/route.ts +168 -0
  258. package/src/app/api/workspace/auth.ts +34 -0
  259. package/src/app/api/workspace/config/route.ts +106 -0
  260. package/src/app/api/workspace/import/route.ts +127 -0
  261. package/src/app/api/workspace/route.ts +116 -0
  262. package/src/app/app/page.tsx +2122 -0
  263. package/src/app/apple-icon.png +0 -0
  264. package/src/app/docs/page.tsx +178 -0
  265. package/src/app/favicon.ico +0 -0
  266. package/src/app/globals.css +572 -0
  267. package/src/app/health/page.tsx +5 -0
  268. package/src/app/hello/page.tsx +15 -0
  269. package/src/app/icon.png +0 -0
  270. package/src/app/layout.tsx +34 -0
  271. package/src/app/page.tsx +986 -0
  272. package/src/app/providers.tsx +90 -0
  273. package/src/app/share/[token]/page.tsx +295 -0
  274. package/src/components/ChainSelector.tsx +144 -0
  275. package/src/components/HumanActionBar.tsx +695 -0
  276. package/src/components/NotificationDrawer.tsx +129 -0
  277. package/src/components/apps/AgentKeysApp.tsx +490 -0
  278. package/src/components/apps/App.tsx +153 -0
  279. package/src/components/apps/AppGrid.tsx +15 -0
  280. package/src/components/apps/DetailedAddressDrawer.tsx +325 -0
  281. package/src/components/apps/DraggableApp.tsx +562 -0
  282. package/src/components/apps/IFrameApp.tsx +73 -0
  283. package/src/components/apps/LogsApp.tsx +360 -0
  284. package/src/components/apps/SendApp.tsx +394 -0
  285. package/src/components/apps/SetupWizardApp.tsx +1004 -0
  286. package/src/components/apps/SystemDefaultsApp.tsx +845 -0
  287. package/src/components/apps/ThirdPartyApp.tsx +428 -0
  288. package/src/components/apps/TokenApp.tsx +319 -0
  289. package/src/components/apps/TransactionsApp.tsx +438 -0
  290. package/src/components/apps/WalletDetailApp.tsx +1505 -0
  291. package/src/components/apps/index.ts +13 -0
  292. package/src/components/design-system/Button.tsx +53 -0
  293. package/src/components/design-system/ChainIndicator.tsx +65 -0
  294. package/src/components/design-system/ChainSelector.tsx +137 -0
  295. package/src/components/design-system/ConfirmationModal.tsx +106 -0
  296. package/src/components/design-system/ConfirmationPopover.tsx +81 -0
  297. package/src/components/design-system/Drawer.tsx +123 -0
  298. package/src/components/design-system/FilterDropdown.tsx +72 -0
  299. package/src/components/design-system/Modal.tsx +206 -0
  300. package/src/components/design-system/Popover.tsx +142 -0
  301. package/src/components/design-system/TextInput.tsx +85 -0
  302. package/src/components/design-system/Toggle.tsx +58 -0
  303. package/src/components/design-system/index.ts +11 -0
  304. package/src/components/docs/DocsThemeToggle.tsx +49 -0
  305. package/src/components/health/CredentialHealthDashboard.tsx +214 -0
  306. package/src/components/icons/ChainIcons.tsx +72 -0
  307. package/src/components/layout/AppStoreDrawer.tsx +369 -0
  308. package/src/components/layout/ContentArea.tsx +21 -0
  309. package/src/components/layout/TabBar.tsx +278 -0
  310. package/src/components/layout/WalletSidebar.tsx +1033 -0
  311. package/src/components/layout/index.ts +4 -0
  312. package/src/components/marketing/AuraWalletSpecOverlay.tsx +635 -0
  313. package/src/components/marketing/DeviceMorphExperience.tsx +216 -0
  314. package/src/components/vault/ApiKeysConsole.tsx +1080 -0
  315. package/src/components/vault/AuditConsole.tsx +584 -0
  316. package/src/components/vault/CredentialDetail.tsx +455 -0
  317. package/src/components/vault/CredentialEmpty.tsx +55 -0
  318. package/src/components/vault/CredentialField.tsx +361 -0
  319. package/src/components/vault/CredentialForm.tsx +1212 -0
  320. package/src/components/vault/CredentialList.tsx +165 -0
  321. package/src/components/vault/CredentialRow.tsx +97 -0
  322. package/src/components/vault/CredentialShareModal.tsx +178 -0
  323. package/src/components/vault/CredentialVault.tsx +754 -0
  324. package/src/components/vault/CredentialWalletWidget.tsx +103 -0
  325. package/src/components/vault/ImportCredentialsModal.tsx +515 -0
  326. package/src/components/vault/LargeTypeModal.tsx +64 -0
  327. package/src/components/vault/PasswordGenerator.tsx +224 -0
  328. package/src/components/vault/TOTPDisplay.tsx +123 -0
  329. package/src/components/vault/VaultSidebar.tsx +413 -0
  330. package/src/components/vault/types.ts +54 -0
  331. package/src/context/AuthContext.tsx +337 -0
  332. package/src/context/PriceContext.tsx +113 -0
  333. package/src/context/ThemeContext.tsx +164 -0
  334. package/src/context/WebSocketContext.tsx +269 -0
  335. package/src/context/WorkspaceContext.tsx +668 -0
  336. package/src/hooks/index.ts +3 -0
  337. package/src/hooks/useAgentActions.ts +368 -0
  338. package/src/hooks/useBalance.ts +103 -0
  339. package/src/hooks/useBalances.ts +129 -0
  340. package/src/instrumentation.ts +12 -0
  341. package/src/lib/api.ts +449 -0
  342. package/src/lib/app-loader.ts +148 -0
  343. package/src/lib/app-registry.ts +178 -0
  344. package/src/lib/app-sdk.ts +157 -0
  345. package/src/lib/audit-console-adapter.ts +151 -0
  346. package/src/lib/auth-client.ts +75 -0
  347. package/src/lib/config.ts +74 -0
  348. package/src/lib/crypto.ts +112 -0
  349. package/src/lib/db.ts +21 -0
  350. package/src/lib/docs.ts +390 -0
  351. package/src/lib/events.ts +361 -0
  352. package/src/lib/pino.ts +24 -0
  353. package/src/lib/theme-handlers.ts +168 -0
  354. package/src/lib/theme.ts +351 -0
  355. package/src/lib/tokenData.ts +378 -0
  356. package/src/lib/vault-crypto.ts +129 -0
  357. package/src/lib/websocket-server.ts +302 -0
  358. package/src/lib/websocket-setup.ts +79 -0
  359. package/src/lib/wordlist.ts +2050 -0
  360. package/src/lib/workspace-handlers.ts +285 -0
  361. package/start.sh +80 -0
  362. package/tailwind.config.ts +99 -0
  363. package/tsconfig.json +42 -0
@@ -0,0 +1,645 @@
1
+ /**
2
+ * Telegram adapter — full bidirectional approval via Telegram Bot API.
3
+ *
4
+ * Uses long-polling (getUpdates) with raw fetch calls — no npm dependencies.
5
+ * Sends inline keyboard buttons for Approve/Reject, edits messages on resolution.
6
+ */
7
+
8
+ import type {
9
+ ApprovalAdapter,
10
+ AdapterContext,
11
+ ActionNotification,
12
+ ActionResolution,
13
+ ChatMessage,
14
+ ChatReply,
15
+ } from './types';
16
+ import { getErrorMessage } from '../error';
17
+
18
+ /** Flavor text rotation for generic thinking time between tool calls */
19
+ const FLAVOR_TEXTS = [
20
+ 'auramaxxing...',
21
+ 'mogging prompt peasants...',
22
+ 'thinking with my big brain...',
23
+ 'controlling cortisol spike...',
24
+ 'channeling main character energy...',
25
+ 'ascending beyond mortal comprehension...',
26
+ 'i will not double text...',
27
+ 'consulting the blockchain elders...',
28
+ 'asking the smart contracts nicely...',
29
+ 'in my bag rn...',
30
+ 'downloading more RAM...',
31
+ 'mainnet mindset activated...',
32
+ 'reading the whitepaper backwards...',
33
+ 'gas fees for my thoughts...',
34
+ ];
35
+
36
+ export interface TelegramAdapterConfig {
37
+ /** Telegram Bot API token (from @BotFather) */
38
+ botToken: string;
39
+ /** Chat ID to send notifications to and accept callbacks from */
40
+ chatId: string | number;
41
+ /** Chat configuration — opt-in for agent chat */
42
+ chat?: { enabled?: boolean };
43
+ }
44
+
45
+ export class TelegramAdapter implements ApprovalAdapter {
46
+ readonly name = 'telegram';
47
+ private config: TelegramAdapterConfig;
48
+ private ctx: AdapterContext | null = null;
49
+ private pollAbort: AbortController | null = null;
50
+ private isRunning = false;
51
+ private updateOffset = 0;
52
+
53
+ /** Maps actionId → Telegram messageId for editing on resolution */
54
+ private actionMessages = new Map<string, number>();
55
+
56
+ constructor(config: TelegramAdapterConfig) {
57
+ this.config = config;
58
+ }
59
+
60
+ async start(ctx: AdapterContext): Promise<void> {
61
+ this.ctx = ctx;
62
+
63
+ // Verify bot token by calling getMe
64
+ const me = await this.apiCall('getMe', {});
65
+ if (!me?.result) {
66
+ console.error('[adapters] telegram: invalid bot token or network error — not starting');
67
+ return;
68
+ }
69
+ const username = (me.result as unknown as { username?: string }).username;
70
+ console.log(`[adapters] telegram: connected as @${username}`);
71
+
72
+ // Clear any existing webhook so long-polling works
73
+ await this.apiCall('deleteWebhook', { drop_pending_updates: false });
74
+
75
+ this.isRunning = true;
76
+ this.startPolling();
77
+ }
78
+
79
+ async notify(action: ActionNotification): Promise<void> {
80
+ // Notification-only: send plain message without inline keyboard
81
+ if (action.type === 'notify') {
82
+ const text = this.formatInfoNotification(action);
83
+ try {
84
+ await this.apiCall('sendMessage', {
85
+ chat_id: this.config.chatId,
86
+ text,
87
+ parse_mode: 'HTML',
88
+ });
89
+ } catch (err) {
90
+ const msg = getErrorMessage(err);
91
+ console.error(`[adapters] telegram notify error:`, msg);
92
+ }
93
+ return;
94
+ }
95
+
96
+ const text = this.formatNotification(action);
97
+ const inlineKeyboard = {
98
+ inline_keyboard: [[
99
+ { text: 'Approve', callback_data: `approve:${action.id}` },
100
+ { text: 'Reject', callback_data: `reject:${action.id}` },
101
+ ]],
102
+ };
103
+
104
+ try {
105
+ const result = await this.apiCall('sendMessage', {
106
+ chat_id: this.config.chatId,
107
+ text,
108
+ parse_mode: 'HTML',
109
+ reply_markup: JSON.stringify(inlineKeyboard),
110
+ });
111
+
112
+ if (result?.result?.message_id) {
113
+ this.actionMessages.set(action.id, result.result.message_id);
114
+ }
115
+ } catch (err) {
116
+ const msg = getErrorMessage(err);
117
+ console.error(`[adapters] telegram notify error:`, msg);
118
+ }
119
+ }
120
+
121
+ async resolved(resolution: ActionResolution): Promise<void> {
122
+ const messageId = this.actionMessages.get(resolution.id);
123
+ if (!messageId) return;
124
+
125
+ const status = resolution.approved ? 'APPROVED' : 'REJECTED';
126
+ const text = `${status} by ${resolution.resolvedBy}\nAction: ${resolution.id.slice(0, 8)}... (${resolution.type})`;
127
+
128
+ try {
129
+ await this.apiCall('editMessageText', {
130
+ chat_id: this.config.chatId,
131
+ message_id: messageId,
132
+ text,
133
+ parse_mode: 'HTML',
134
+ });
135
+ } catch (err) {
136
+ console.debug('[adapters] telegram: failed to edit resolved message:', getErrorMessage(err));
137
+ }
138
+
139
+ this.actionMessages.delete(resolution.id);
140
+ }
141
+
142
+ async stop(): Promise<void> {
143
+ console.log('[adapters] telegram: stopping');
144
+ this.isRunning = false;
145
+ if (this.pollAbort) {
146
+ this.pollAbort.abort();
147
+ this.pollAbort = null;
148
+ }
149
+ this.actionMessages.clear();
150
+ }
151
+
152
+ private formatInfoNotification(action: ActionNotification): string {
153
+ const meta = (action.metadata || {}) as Record<string, unknown>;
154
+ const lines: string[] = [
155
+ `<b>${escapeHtml(action.summary)}</b>`,
156
+ `<b>Source:</b> ${escapeHtml(action.source)}`,
157
+ ];
158
+
159
+ if (meta.contractAddress) {
160
+ lines.push(`<b>DexScreener:</b> <a href="https://dexscreener.com/base/${meta.contractAddress}">View Chart</a>`);
161
+ }
162
+
163
+ if (meta.symbol) {
164
+ lines.push(`<b>Symbol:</b> ${escapeHtml(String(meta.symbol))}`);
165
+ }
166
+ if (meta.marketCap) {
167
+ const mc = Number(meta.marketCap);
168
+ const formatted = mc >= 1_000_000
169
+ ? `$${(mc / 1_000_000).toFixed(1)}M`
170
+ : mc >= 1000
171
+ ? `$${(mc / 1000).toFixed(1)}K`
172
+ : `$${mc.toFixed(0)}`;
173
+ lines.push(`<b>Market Cap:</b> ${formatted}`);
174
+ }
175
+ if (meta.risk != null) {
176
+ lines.push(`<b>Risk:</b> ${meta.risk}/10`);
177
+ }
178
+
179
+ if (Array.isArray(meta.socialLinks)) {
180
+ const linkStrs: string[] = [];
181
+ for (const link of meta.socialLinks as Array<{ name?: string; link?: string }>) {
182
+ if (link.link) {
183
+ linkStrs.push(`<a href="${escapeHtml(link.link)}">${escapeHtml(link.name || 'Link')}</a>`);
184
+ }
185
+ }
186
+ if (linkStrs.length > 0) {
187
+ lines.push(`<b>Links:</b> ${linkStrs.join(' | ')}`);
188
+ }
189
+ }
190
+
191
+ if (meta.evaluation && typeof meta.evaluation === 'string') {
192
+ // Truncate very long evaluations for Telegram (max ~3500 chars to stay under 4096 limit)
193
+ let evalText = meta.evaluation as string;
194
+ if (evalText.length > 3000) {
195
+ evalText = evalText.slice(0, 3000) + '...';
196
+ }
197
+ lines.push('');
198
+ lines.push(escapeHtml(evalText));
199
+ }
200
+
201
+ return lines.join('\n');
202
+ }
203
+
204
+ private formatNotification(action: ActionNotification): string {
205
+ const vs = action.verifiedSummary || (action.metadata?.verifiedSummary as ActionNotification['verifiedSummary']);
206
+
207
+ const lines = [
208
+ `<b>New Action Request</b>`,
209
+ `<b>Type:</b> ${escapeHtml(action.type)}`,
210
+ `<b>Source:</b> ${escapeHtml(action.source)}`,
211
+ ];
212
+
213
+ // Use verified one-liner as primary summary when available
214
+ if (vs?.oneLiner) {
215
+ lines.push(`<b>Action:</b> ${escapeHtml(vs.oneLiner)}`);
216
+ if (action.summary !== vs.oneLiner) {
217
+ lines.push(`<b>Agent says:</b> <i>${escapeHtml(action.summary)}</i>`);
218
+ }
219
+ } else {
220
+ lines.push(`<b>Summary:</b> ${escapeHtml(action.summary)}`);
221
+ }
222
+
223
+ lines.push(`<b>ID:</b> <code>${action.id.slice(0, 12)}</code>`);
224
+
225
+ if (action.expiresAt) {
226
+ const expires = new Date(action.expiresAt).toISOString();
227
+ lines.push(`<b>Expires:</b> ${expires}`);
228
+ }
229
+
230
+ // Show discrepancy warnings
231
+ if (vs?.discrepancies && vs.discrepancies.length > 0) {
232
+ lines.push('');
233
+ for (const d of vs.discrepancies) {
234
+ const icon = d.severity === 'critical' ? '🚨' : d.severity === 'warning' ? '⚠️' : 'ℹ️';
235
+ lines.push(`${icon} <b>${escapeHtml(d.severity.toUpperCase())}</b>: ${escapeHtml(d.field)} — agent says "${escapeHtml(d.agentClaim)}", actual: ${escapeHtml(d.actual)}`);
236
+ }
237
+ }
238
+
239
+ return lines.join('\n');
240
+ }
241
+
242
+ /** Start long-polling loop for Telegram updates */
243
+ private startPolling(): void {
244
+ console.log('[adapters] telegram: polling started');
245
+ const poll = async () => {
246
+ while (this.isRunning) {
247
+ try {
248
+ this.pollAbort = new AbortController();
249
+ const result = await this.apiCall('getUpdates', {
250
+ offset: this.updateOffset,
251
+ timeout: 30,
252
+ }, this.pollAbort.signal);
253
+
254
+ if (!result?.result) continue;
255
+
256
+ for (const update of result.result) {
257
+ this.updateOffset = update.update_id + 1;
258
+
259
+ if (update.callback_query) {
260
+ await this.handleCallback(update.callback_query);
261
+ } else if (update.message?.text) {
262
+ await this.handleChatMessage(update.message as TelegramMessage);
263
+ }
264
+ }
265
+ } catch (err) {
266
+ if (err instanceof Error && err.name === 'AbortError') break;
267
+ const msg = getErrorMessage(err);
268
+ console.error('[adapters] telegram: polling error:', msg);
269
+ await new Promise(r => setTimeout(r, 3000));
270
+ }
271
+ }
272
+ console.log('[adapters] telegram: polling stopped');
273
+ };
274
+ poll();
275
+ }
276
+
277
+ /** Handle an inline keyboard callback */
278
+ private async handleCallback(query: TelegramCallbackQuery): Promise<void> {
279
+ // Security: only accept callbacks from the configured chat
280
+ const chatId = String(query.message?.chat?.id);
281
+ if (chatId !== String(this.config.chatId)) return;
282
+
283
+ const data = query.data;
284
+ if (!data) return;
285
+
286
+ const match = data.match(/^(approve|reject):(.+)$/);
287
+ if (!match) return;
288
+
289
+ const [, action, actionId] = match;
290
+ const approved = action === 'approve';
291
+
292
+ // Answer the callback to remove loading state
293
+ await this.apiCall('answerCallbackQuery', {
294
+ callback_query_id: query.id,
295
+ text: approved ? 'Approving...' : 'Rejecting...',
296
+ }).catch(() => {});
297
+
298
+ // Resolve the action
299
+ if (this.ctx) {
300
+ const result = await this.ctx.resolve(actionId, approved);
301
+ if (!result.success) {
302
+ // Notify the user of the error
303
+ await this.apiCall('sendMessage', {
304
+ chat_id: this.config.chatId,
305
+ text: `Failed to ${action}: ${result.error}`,
306
+ }).catch(() => {});
307
+ }
308
+ }
309
+ }
310
+
311
+ /** Handle incoming chat messages (adapter interface) */
312
+ async onMessage(message: ChatMessage): Promise<ChatReply | null> {
313
+ if (this.config.chat?.enabled !== true) return null;
314
+ if (!this.ctx) return null;
315
+
316
+ const appId = await this.ctx.resolveApp(message.targetApp);
317
+ if (!appId) {
318
+ return { text: 'No AI app configured. Set a default app in adapter settings.' };
319
+ }
320
+
321
+ const result = await this.ctx.sendMessage(appId, message.text, undefined, 'telegram');
322
+ if (result.error) {
323
+ return { text: `Error: ${result.error}` };
324
+ }
325
+
326
+ return result.reply ? { text: result.reply } : null;
327
+ }
328
+
329
+ /** Handle a text message from the Telegram polling loop */
330
+ private async handleChatMessage(message: TelegramMessage): Promise<void> {
331
+ // Chat must be opted in
332
+ if (this.config.chat?.enabled !== true) return;
333
+
334
+ // Security: only accept messages from the configured chat
335
+ if (!this.config.chatId) {
336
+ console.error('[adapters] telegram: chatId not configured — cannot process messages. Re-save adapter config with your chat ID.');
337
+ return;
338
+ }
339
+ const chatId = String(message.chat?.id);
340
+ if (chatId !== String(this.config.chatId)) {
341
+ console.warn(`[adapters] telegram: ignoring message from chat ${chatId} (expected ${this.config.chatId})`);
342
+ return;
343
+ }
344
+
345
+ const text = message.text || '';
346
+ if (!text.trim()) return;
347
+
348
+ console.log(`[adapters] telegram: chat message from ${chatId}: "${text.slice(0, 80)}${text.length > 80 ? '...' : ''}"`);
349
+
350
+ // Show "typing..." indicator while AI processes (re-send every 4s since it expires after ~5s)
351
+ const sendTyping = () => this.apiCall('sendChatAction', {
352
+ chat_id: this.config.chatId,
353
+ action: 'typing',
354
+ }).catch(() => { /* non-fatal */ });
355
+ await sendTyping();
356
+ const typingInterval = setInterval(sendTyping, 4000);
357
+
358
+ // Progress status message — editable in place.
359
+ // Delayed: wait a few seconds before showing the first flavor text so
360
+ // quick replies don't flash a status message at all.
361
+ let statusMessageId: number | null = null;
362
+ let creatingStatusMessage = false;
363
+ let flavorIndex = 0;
364
+
365
+ const createStatusMessage = (text: string) => {
366
+ if (statusMessageId || creatingStatusMessage) return;
367
+ creatingStatusMessage = true;
368
+ this.apiCall('sendMessage', {
369
+ chat_id: this.config.chatId,
370
+ text,
371
+ }).then((res) => {
372
+ statusMessageId = res?.result?.message_id ?? null;
373
+ }).catch(() => {
374
+ // Non-fatal — proceed without status message
375
+ }).finally(() => {
376
+ creatingStatusMessage = false;
377
+ });
378
+ };
379
+
380
+ const onProgress = (status: string) => {
381
+ const progressText = status || FLAVOR_TEXTS[flavorIndex++ % FLAVOR_TEXTS.length];
382
+ if (statusMessageId) {
383
+ this.apiCall('editMessageText', {
384
+ chat_id: this.config.chatId,
385
+ message_id: statusMessageId,
386
+ text: progressText,
387
+ }).catch(() => {});
388
+ } else if (status) {
389
+ // Explicit progress means work is ongoing; create status message immediately.
390
+ createStatusMessage(progressText);
391
+ }
392
+ };
393
+
394
+ // Delay the initial status message — no need to spam on quick replies
395
+ const statusDelay = setTimeout(async () => {
396
+ if (statusMessageId || creatingStatusMessage) return;
397
+ const initialText = FLAVOR_TEXTS[flavorIndex++ % FLAVOR_TEXTS.length];
398
+ createStatusMessage(initialText);
399
+ }, 3000);
400
+
401
+ // Rotate flavor text on a timer when no tool-call progress fires
402
+ const flavorInterval = setInterval(() => {
403
+ if (statusMessageId) onProgress('');
404
+ }, 10_000);
405
+
406
+ const cleanupTimers = () => {
407
+ clearTimeout(statusDelay);
408
+ clearInterval(typingInterval);
409
+ clearInterval(flavorInterval);
410
+ };
411
+
412
+ const deleteStatusMessage = async () => {
413
+ if (statusMessageId) {
414
+ await this.apiCall('deleteMessage', {
415
+ chat_id: this.config.chatId,
416
+ message_id: statusMessageId,
417
+ }).catch(() => {});
418
+ }
419
+ };
420
+
421
+ // Route through the message chain with the progress callback
422
+ if (!this.ctx) {
423
+ cleanupTimers();
424
+ return;
425
+ }
426
+
427
+ const appId = await this.ctx.resolveApp();
428
+ if (!appId) {
429
+ cleanupTimers();
430
+ await deleteStatusMessage();
431
+ await this.apiCall('sendMessage', {
432
+ chat_id: this.config.chatId,
433
+ text: 'No AI app configured. Set a default app in adapter settings.',
434
+ }).catch(() => {});
435
+ return;
436
+ }
437
+
438
+ let result: { reply: string | null; error?: string };
439
+ try {
440
+ result = await this.ctx.sendMessage(appId, text, onProgress, 'telegram');
441
+ } finally {
442
+ cleanupTimers();
443
+ }
444
+
445
+ // Delete status message before sending real reply (await to prevent double-texting)
446
+ await deleteStatusMessage();
447
+
448
+ if (result.error) {
449
+ await this.apiCall('sendMessage', {
450
+ chat_id: this.config.chatId,
451
+ text: `Error: ${result.error}`,
452
+ }).catch(() => {});
453
+ return;
454
+ }
455
+
456
+ if (!result.reply) {
457
+ console.log('[adapters] telegram: no reply generated for chat message');
458
+ return;
459
+ }
460
+
461
+ await this.sendLongMessage(result.reply);
462
+ }
463
+
464
+ /**
465
+ * Send a long message, converting markdown to Telegram HTML and splitting
466
+ * into chunks that respect the 4096-char limit. Falls back to plain text
467
+ * if HTML parsing fails on a chunk.
468
+ */
469
+ private async sendLongMessage(reply: string): Promise<void> {
470
+ const html = markdownToTelegramHtml(reply);
471
+ const chunks = splitMessage(html, 4000); // leave headroom under 4096
472
+ for (const chunk of chunks) {
473
+ // Try HTML first, fall back to plain text if Telegram rejects the markup
474
+ const sent = await this.apiCall('sendMessage', {
475
+ chat_id: this.config.chatId,
476
+ text: chunk,
477
+ parse_mode: 'HTML',
478
+ });
479
+ if (!sent) {
480
+ // HTML parse failed (unclosed tags from split) — retry as plain text
481
+ const plain = chunk.replace(/<[^>]+>/g, '');
482
+ const fallback = await this.apiCall('sendMessage', {
483
+ chat_id: this.config.chatId,
484
+ text: plain || '(message could not be rendered)',
485
+ });
486
+ if (!fallback) {
487
+ console.error('[adapters] telegram: failed to send chat reply chunk even as plain text');
488
+ }
489
+ }
490
+ }
491
+ }
492
+
493
+ /** Call a Telegram Bot API method */
494
+ private async apiCall(
495
+ method: string,
496
+ params: Record<string, unknown>,
497
+ signal?: AbortSignal,
498
+ ): Promise<TelegramApiResponse | null> {
499
+ const url = `https://api.telegram.org/bot${this.config.botToken}/${method}`;
500
+
501
+ const response = await fetch(url, {
502
+ method: 'POST',
503
+ headers: { 'Content-Type': 'application/json' },
504
+ body: JSON.stringify(params),
505
+ signal,
506
+ });
507
+
508
+ if (!response.ok) {
509
+ const text = await response.text().catch(() => '');
510
+ console.error(`[adapters] telegram API ${method} failed: ${response.status} ${text}`);
511
+ return null;
512
+ }
513
+
514
+ return response.json() as Promise<TelegramApiResponse>;
515
+ }
516
+ }
517
+
518
+ // Minimal Telegram types (no npm dependency)
519
+ interface TelegramCallbackQuery {
520
+ id: string;
521
+ data?: string;
522
+ message?: { chat?: { id: number }; message_id?: number };
523
+ }
524
+
525
+ interface TelegramMessage {
526
+ message_id: number;
527
+ chat: { id: number };
528
+ text?: string;
529
+ from?: { id: number; first_name?: string };
530
+ }
531
+
532
+ interface TelegramApiResponse {
533
+ ok: boolean;
534
+ result?: { message_id?: number; update_id?: number; callback_query?: TelegramCallbackQuery; message?: TelegramMessage }[] & { message_id?: number };
535
+ }
536
+
537
+ /**
538
+ * Split a message into chunks that fit within a max length.
539
+ * HTML-tag-aware: closes open tags at the end of each chunk and reopens
540
+ * them at the start of the next, so Telegram's HTML parser doesn't choke.
541
+ */
542
+ function splitMessage(text: string, maxLength: number): string[] {
543
+ if (text.length <= maxLength) return [text];
544
+
545
+ const chunks: string[] = [];
546
+ let remaining = text;
547
+
548
+ while (remaining.length > 0) {
549
+ if (remaining.length <= maxLength) {
550
+ chunks.push(remaining);
551
+ break;
552
+ }
553
+
554
+ // Find a split point: prefer newline near the limit, avoid splitting inside a tag
555
+ let splitAt = remaining.lastIndexOf('\n', maxLength);
556
+ if (splitAt <= 0) splitAt = remaining.lastIndexOf(' ', maxLength);
557
+ if (splitAt <= 0) splitAt = maxLength;
558
+
559
+ // Don't split inside an HTML tag — back up to before the '<'
560
+ const lastOpen = remaining.lastIndexOf('<', splitAt);
561
+ const lastClose = remaining.lastIndexOf('>', splitAt);
562
+ if (lastOpen > lastClose) {
563
+ // We're inside a tag — split before it
564
+ splitAt = lastOpen;
565
+ }
566
+
567
+ let chunk = remaining.slice(0, splitAt);
568
+ remaining = remaining.slice(splitAt).replace(/^\n/, '');
569
+
570
+ // Find unclosed tags in this chunk and close them
571
+ const openTags: string[] = [];
572
+ const tagRegex = /<\/?([a-z]+)[^>]*>/gi;
573
+ let m: RegExpExecArray | null;
574
+ while ((m = tagRegex.exec(chunk)) !== null) {
575
+ const tagName = m[1].toLowerCase();
576
+ if (m[0].startsWith('</')) {
577
+ // Closing tag — pop the last matching open tag
578
+ const idx = openTags.lastIndexOf(tagName);
579
+ if (idx !== -1) openTags.splice(idx, 1);
580
+ } else if (!m[0].endsWith('/>')) {
581
+ openTags.push(tagName);
582
+ }
583
+ }
584
+
585
+ // Close remaining open tags at end of chunk (reverse order)
586
+ for (let i = openTags.length - 1; i >= 0; i--) {
587
+ chunk += `</${openTags[i]}>`;
588
+ }
589
+
590
+ // Reopen them at the start of the next chunk
591
+ if (openTags.length > 0 && remaining.length > 0) {
592
+ remaining = openTags.map(t => `<${t}>`).join('') + remaining;
593
+ }
594
+
595
+ chunks.push(chunk);
596
+ }
597
+
598
+ return chunks;
599
+ }
600
+
601
+ function escapeHtml(text: string): string {
602
+ return text
603
+ .replace(/&/g, '&amp;')
604
+ .replace(/</g, '&lt;')
605
+ .replace(/>/g, '&gt;');
606
+ }
607
+
608
+ /** Convert markdown to Telegram-compatible HTML */
609
+ export function markdownToTelegramHtml(md: string): string {
610
+ // 1. Extract fenced code blocks — preserve content, no markdown processing inside
611
+ const codeBlocks: string[] = [];
612
+ let text = md.replace(/```\w*\n?([\s\S]*?)```/g, (_, content: string) => {
613
+ const idx = codeBlocks.length;
614
+ codeBlocks.push(`<pre>${escapeHtml(content.replace(/\n$/, ''))}</pre>`);
615
+ return `\x00CB${idx}\x00`;
616
+ });
617
+
618
+ // 2. Extract inline code
619
+ const inlineCodes: string[] = [];
620
+ text = text.replace(/`([^`\n]+)`/g, (_, content: string) => {
621
+ const idx = inlineCodes.length;
622
+ inlineCodes.push(`<code>${escapeHtml(content)}</code>`);
623
+ return `\x00IC${idx}\x00`;
624
+ });
625
+
626
+ // 3. Escape HTML in remaining text
627
+ text = escapeHtml(text);
628
+
629
+ // 4. Convert markdown patterns
630
+ // Bold: **text** or __text__
631
+ text = text.replace(/\*\*(.+?)\*\*/g, '<b>$1</b>');
632
+ text = text.replace(/__(.+?)__/g, '<b>$1</b>');
633
+ // Strikethrough: ~~text~~
634
+ text = text.replace(/~~(.+?)~~/g, '<s>$1</s>');
635
+ // Headers: # text → bold
636
+ text = text.replace(/^#{1,6}\s+(.+)$/gm, '<b>$1</b>');
637
+ // Markdown links: [text](url)
638
+ text = text.replace(/\[([^\]]+)\]\(([^)]+)\)/g, '<a href="$2">$1</a>');
639
+
640
+ // 5. Re-insert preserved blocks
641
+ text = text.replace(/\x00CB(\d+)\x00/g, (_, idx) => codeBlocks[Number(idx)]);
642
+ text = text.replace(/\x00IC(\d+)\x00/g, (_, idx) => inlineCodes[Number(idx)]);
643
+
644
+ return text;
645
+ }