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,1004 @@
1
+ 'use client';
2
+
3
+ import React, { useState, useEffect, useCallback, useRef } from 'react';
4
+ import { Check, Loader2, ChevronDown, ChevronRight, Bot, Globe, MessageSquare, ExternalLink, Trash2, X, KeyRound, FlaskConical } from 'lucide-react';
5
+ import { Button, TextInput } from '@/components/design-system';
6
+ import { api, Api } from '@/lib/api';
7
+
8
+ interface ProviderInfo {
9
+ mode: string;
10
+ label: string;
11
+ available: boolean;
12
+ reason: string;
13
+ models: string[];
14
+ }
15
+
16
+ interface AiStatusResponse {
17
+ activeProvider: string;
18
+ defaultModel: string;
19
+ providers: ProviderInfo[];
20
+ }
21
+
22
+ /** Map provider mode to the API key service name it requires (if any) */
23
+ const PROVIDER_KEY_SERVICE: Record<string, string | null> = {
24
+ 'claude-cli': null,
25
+ 'claude-api': 'anthropic',
26
+ 'codex-cli': null,
27
+ 'openai-api': 'openai',
28
+ };
29
+
30
+ /** Human-readable label for API key services */
31
+ const KEY_SERVICE_LABEL: Record<string, string> = {
32
+ anthropic: 'Anthropic',
33
+ openai: 'OpenAI',
34
+ };
35
+
36
+ /** Placeholder hints for API key inputs */
37
+ const KEY_PLACEHOLDER: Record<string, string> = {
38
+ anthropic: 'sk-ant-...',
39
+ openai: 'sk-...',
40
+ };
41
+
42
+ /** CLI provider instructions */
43
+ const CLI_INSTRUCTIONS: Record<string, string> = {
44
+ 'claude-cli': 'Make sure the `claude` CLI is installed and you\'re logged in (`claude login`).',
45
+ 'codex-cli': 'Make sure the `codex` CLI is installed and you\'re authenticated.',
46
+ };
47
+
48
+ interface SetupStatus {
49
+ hasWallet: boolean;
50
+ unlocked: boolean;
51
+ address: string | null;
52
+ apiKeys: { alchemy: boolean; anthropic: boolean };
53
+ adapters: { telegram: boolean; webhook: boolean };
54
+ }
55
+
56
+ interface SetupWizardAppProps {
57
+ config?: Record<string, unknown>;
58
+ }
59
+
60
+ function StepItem({ number, title, subtitle, icon, done, expanded, onToggle, children, doneContent }: {
61
+ number: number;
62
+ title: string;
63
+ subtitle: string;
64
+ icon: React.ReactNode;
65
+ done: boolean;
66
+ expanded: boolean;
67
+ onToggle: () => void;
68
+ children: React.ReactNode;
69
+ doneContent?: React.ReactNode;
70
+ }) {
71
+ return (
72
+ <div style={{ border: '1px solid var(--color-border)', background: 'var(--color-surface)' }}>
73
+ <button
74
+ onClick={onToggle}
75
+ className="w-full flex items-center gap-3 p-3 text-left"
76
+ >
77
+ <div className="w-6 h-6 flex items-center justify-center flex-shrink-0" style={{
78
+ background: done ? 'var(--color-success)' : 'var(--color-background-alt)',
79
+ border: done ? 'none' : '1px solid var(--color-border)',
80
+ }}>
81
+ {done ? (
82
+ <Check size={12} style={{ color: 'white' }} />
83
+ ) : (
84
+ <span className="font-mono text-[10px] font-bold" style={{ color: 'var(--color-text-muted)' }}>{number}</span>
85
+ )}
86
+ </div>
87
+ <div className="flex-1 min-w-0">
88
+ <div className="flex items-center gap-2">
89
+ <span style={{ color: done ? 'var(--color-success)' : 'var(--color-text)' }}>{icon}</span>
90
+ <span className="font-mono text-[11px] font-bold" style={{ color: done ? 'var(--color-success)' : 'var(--color-text)' }}>
91
+ {title}
92
+ </span>
93
+ </div>
94
+ <div className="font-mono text-[9px]" style={{ color: 'var(--color-text-muted)' }}>{subtitle}</div>
95
+ </div>
96
+ {expanded ? <ChevronDown size={14} style={{ color: 'var(--color-text-muted)' }} /> : <ChevronRight size={14} style={{ color: 'var(--color-text-muted)' }} />}
97
+ </button>
98
+ {expanded && (
99
+ <div className="px-3 pb-3 border-t" style={{ borderColor: 'var(--color-border)' }}>
100
+ {done ? doneContent : children}
101
+ </div>
102
+ )}
103
+ </div>
104
+ );
105
+ }
106
+
107
+ const SetupWizardApp: React.FC<SetupWizardAppProps> = () => {
108
+ const [status, setStatus] = useState<SetupStatus | null>(null);
109
+ const [loading, setLoading] = useState(true);
110
+ const [expandedStep, setExpandedStep] = useState<number | null>(null);
111
+ const [dismissed, setDismissed] = useState(false);
112
+
113
+ // Step 1: AI Provider
114
+ const [aiStatus, setAiStatus] = useState<AiStatusResponse | null>(null);
115
+ const [aiLoading, setAiLoading] = useState(true);
116
+ const [selectedProvider, setSelectedProvider] = useState<string>('claude-cli');
117
+ const [providerKeyInput, setProviderKeyInput] = useState('');
118
+ const [providerKeyValidating, setProviderKeyValidating] = useState(false);
119
+ const [providerError, setProviderError] = useState('');
120
+ const [providerTesting, setProviderTesting] = useState(false);
121
+ const [providerTestResult, setProviderTestResult] = useState<'success' | 'fail' | null>(null);
122
+ const [providerSaving, setProviderSaving] = useState(false);
123
+
124
+ // Step 1 (continued): Permission Tier
125
+ const [agentTier, setAgentTier] = useState<string>('admin');
126
+ const [agentTierSaving, setAgentTierSaving] = useState(false);
127
+
128
+ // Step 2: Alchemy key
129
+ const [alchemyKey, setAlchemyKey] = useState('');
130
+ const [alchemyValidating, setAlchemyValidating] = useState(false);
131
+ const [alchemyError, setAlchemyError] = useState('');
132
+
133
+ // Step 3: Telegram
134
+ const [botToken, setBotToken] = useState('');
135
+ const [botUsername, setBotUsername] = useState('');
136
+ const [chatId, setChatId] = useState('');
137
+ const [chatEnabled, setChatEnabled] = useState(true);
138
+ const [telegramStep, setTelegramStep] = useState<'token' | 'detecting' | 'chatId' | 'testing' | 'done'>('token');
139
+ const [telegramValidating, setTelegramValidating] = useState(false);
140
+ const [telegramError, setTelegramError] = useState('');
141
+ const [deepLink, setDeepLink] = useState('');
142
+ const [, setSetupToken] = useState('');
143
+ const [detectedName, setDetectedName] = useState('');
144
+ const detectAbortRef = useRef<AbortController | null>(null);
145
+
146
+ // Editing state — when set, forces the form to show instead of doneContent
147
+ const [editingStep, setEditingStep] = useState<number | null>(null);
148
+
149
+ // Done-state test for AI provider
150
+ const [doneTestLoading, setDoneTestLoading] = useState(false);
151
+ const [doneTestResult, setDoneTestResult] = useState<'success' | 'fail' | null>(null);
152
+
153
+ // Removal state
154
+ const [removingAlchemy, setRemovingAlchemy] = useState(false);
155
+ const [removingTelegram, setRemovingTelegram] = useState(false);
156
+
157
+ // Step 1 two-path state
158
+ const [step1Tab, setStep1Tab] = useState<'provider' | 'agent'>('provider');
159
+ const [agentPairAcknowledged, setAgentPairAcknowledged] = useState(false);
160
+
161
+ /** Fetch AI status from /ai/status */
162
+ const fetchAiStatus = useCallback(async () => {
163
+ try {
164
+ const res = await api.get<AiStatusResponse>(Api.Wallet, '/ai/status');
165
+ setAiStatus(res);
166
+ setSelectedProvider(res.activeProvider);
167
+ return res;
168
+ } catch {
169
+ // AI status fetch failed
170
+ return null;
171
+ } finally {
172
+ setAiLoading(false);
173
+ }
174
+ }, []);
175
+
176
+ /** Check if AI provider step is complete: activeProvider is set AND available */
177
+ const isAiStepDone = useCallback((ai: AiStatusResponse | null): boolean => {
178
+ if (!ai) return false;
179
+ const active = ai.providers.find(p => p.mode === ai.activeProvider);
180
+ return !!active?.available;
181
+ }, []);
182
+
183
+ /** Fetch only setup status (wallet, apiKeys, adapters) — no /ai/status call */
184
+ const fetchSetupStatus = useCallback(async () => {
185
+ try {
186
+ const data = await api.get<SetupStatus>(Api.Wallet, '/setup');
187
+ setStatus(data);
188
+ return data;
189
+ } catch {
190
+ return null;
191
+ }
192
+ }, []);
193
+
194
+ /** Auto-expand the first incomplete step */
195
+ const autoExpand = useCallback((ai: AiStatusResponse | null, setup: SetupStatus | null, agentPaired?: boolean) => {
196
+ const aiDone = isAiStepDone(ai);
197
+ const step1Done = aiDone || (agentPaired ?? agentPairAcknowledged);
198
+ if (!step1Done) setExpandedStep(1);
199
+ else if (!setup?.apiKeys.alchemy) setExpandedStep(2);
200
+ else if (!setup?.adapters.telegram) setExpandedStep(3);
201
+ else setExpandedStep(null);
202
+ }, [isAiStepDone, agentPairAcknowledged]);
203
+
204
+ /** Initial load — fetch both endpoints + agent tier + localStorage */
205
+ useEffect(() => {
206
+ (async () => {
207
+ try {
208
+ const [setup, ai] = await Promise.all([
209
+ fetchSetupStatus(),
210
+ fetchAiStatus(),
211
+ // Fetch agent tier
212
+ api.get<Record<string, Array<{ key: string; value: unknown }>>>(Api.Wallet, '/defaults').then(grouped => {
213
+ const permsGroup = grouped.permissions || [];
214
+ const tierRow = permsGroup.find((r: { key: string }) => r.key === 'permissions.agent_tier');
215
+ if (tierRow) setAgentTier(tierRow.value as string);
216
+ }).catch(() => { /* use default */ }),
217
+ ]);
218
+ // Read agentPairAcknowledged from localStorage
219
+ const addr = setup?.address || 'unknown';
220
+ const paired = localStorage.getItem(`agentPaired:${addr}`) === 'true';
221
+ setAgentPairAcknowledged(paired);
222
+ if (paired) setStep1Tab('agent');
223
+ autoExpand(ai, setup, paired);
224
+ } finally {
225
+ setLoading(false);
226
+ }
227
+ })();
228
+ }, [fetchSetupStatus, fetchAiStatus, autoExpand]);
229
+
230
+ // Handle provider radio change
231
+ const handleProviderChange = (mode: string) => {
232
+ setSelectedProvider(mode);
233
+ setProviderKeyInput('');
234
+ setProviderError('');
235
+ setProviderTestResult(null);
236
+ };
237
+
238
+ // Handle permission tier change
239
+ const handleTierChange = async (tier: string) => {
240
+ setAgentTier(tier);
241
+ setAgentTierSaving(true);
242
+ try {
243
+ await api.patch(Api.Wallet, `/defaults/${encodeURIComponent('permissions.agent_tier')}`, { value: tier });
244
+ } catch { setAgentTier(tier === 'admin' ? 'restricted' : 'admin'); }
245
+ finally { setAgentTierSaving(false); }
246
+ };
247
+
248
+ // Validate + save API key for current provider, then save provider selection
249
+ const handleProviderKeySave = async () => {
250
+ const keyService = PROVIDER_KEY_SERVICE[selectedProvider];
251
+ if (!keyService) return;
252
+ setProviderError('');
253
+ setProviderKeyValidating(true);
254
+ try {
255
+ const validation = await api.post<{ valid: boolean; error?: string }>(Api.Wallet, '/apikeys/validate', { service: keyService, key: providerKeyInput.trim() });
256
+ if (!validation.valid) {
257
+ setProviderError(validation.error || 'Invalid key');
258
+ return;
259
+ }
260
+ await api.post(Api.Wallet, '/apikeys', { service: keyService, name: 'default', key: providerKeyInput.trim() });
261
+ setProviderKeyInput('');
262
+ // Save the provider selection
263
+ await api.patch(Api.Wallet, `/defaults/${encodeURIComponent('ai.provider')}`, { value: selectedProvider });
264
+ setEditingStep(null);
265
+ const ai = await fetchAiStatus();
266
+ autoExpand(ai, status);
267
+ } catch (err) {
268
+ setProviderError(err instanceof Error ? err.message : 'Failed to validate');
269
+ } finally {
270
+ setProviderKeyValidating(false);
271
+ }
272
+ };
273
+
274
+ // Test a CLI provider by re-fetching /ai/status
275
+ const handleProviderTest = async () => {
276
+ setProviderError('');
277
+ setProviderTesting(true);
278
+ setProviderTestResult(null);
279
+ try {
280
+ const res = await api.get<AiStatusResponse>(Api.Wallet, '/ai/status');
281
+ setAiStatus(res);
282
+ const provider = res.providers.find(p => p.mode === selectedProvider);
283
+ if (provider?.available) {
284
+ setProviderTestResult('success');
285
+ // Save the provider selection
286
+ setProviderSaving(true);
287
+ await api.patch(Api.Wallet, `/defaults/${encodeURIComponent('ai.provider')}`, { value: selectedProvider });
288
+ setEditingStep(null);
289
+ autoExpand(res, status);
290
+ setProviderSaving(false);
291
+ } else {
292
+ setProviderTestResult('fail');
293
+ setProviderError(provider?.reason || 'Provider not available');
294
+ }
295
+ } catch (err) {
296
+ setProviderTestResult('fail');
297
+ setProviderError(err instanceof Error ? err.message : 'Test failed');
298
+ } finally {
299
+ setProviderTesting(false);
300
+ }
301
+ };
302
+
303
+ // Validate + save Alchemy key
304
+ const handleAlchemySave = async () => {
305
+ setAlchemyError('');
306
+ setAlchemyValidating(true);
307
+ try {
308
+ const validation = await api.post<{ valid: boolean; error?: string }>(Api.Wallet, '/apikeys/validate', { service: 'alchemy', key: alchemyKey });
309
+ if (!validation.valid) {
310
+ setAlchemyError(validation.error || 'Invalid key');
311
+ return;
312
+ }
313
+ await api.post(Api.Wallet, '/apikeys', { service: 'alchemy', name: 'default', key: alchemyKey });
314
+ setAlchemyKey('');
315
+ setEditingStep(null);
316
+ const setup = await fetchSetupStatus();
317
+ autoExpand(aiStatus, setup);
318
+ } catch (err) {
319
+ setAlchemyError(err instanceof Error ? err.message : 'Failed to validate');
320
+ } finally {
321
+ setAlchemyValidating(false);
322
+ }
323
+ };
324
+
325
+ // Telegram flow: validate bot token, then start auto-detection
326
+ const handleTelegramValidate = async () => {
327
+ setTelegramError('');
328
+ setTelegramValidating(true);
329
+ try {
330
+ // Validate the token
331
+ const validation = await api.post<{ valid: boolean; error?: string; info?: { botUsername: string } }>(Api.Wallet, '/apikeys/validate', { service: 'adapter:telegram', key: botToken });
332
+ if (!validation.valid) {
333
+ setTelegramError(validation.error || 'Invalid bot token');
334
+ return;
335
+ }
336
+ setBotUsername(validation.info?.botUsername || '');
337
+
338
+ // Get deep link for auto-detection
339
+ const linkResult = await api.post<{ success: boolean; link: string; setupToken: string; botUsername: string; error?: string }>(Api.Wallet, '/adapters/telegram/setup-link', { botToken });
340
+ if (!linkResult.success) {
341
+ setTelegramError(linkResult.error || 'Failed to generate setup link');
342
+ setTelegramStep('chatId');
343
+ return;
344
+ }
345
+
346
+ setDeepLink(linkResult.link);
347
+ setSetupToken(linkResult.setupToken);
348
+ setBotUsername(linkResult.botUsername);
349
+ setTelegramStep('detecting');
350
+
351
+ // Start polling for chat ID detection (up to 2 attempts ~ 50s)
352
+ startDetection(linkResult.setupToken);
353
+ } catch (err) {
354
+ setTelegramError(err instanceof Error ? err.message : 'Failed to validate');
355
+ } finally {
356
+ setTelegramValidating(false);
357
+ }
358
+ };
359
+
360
+ // Poll detect-chat endpoint
361
+ const startDetection = async (token: string) => {
362
+ const abort = new AbortController();
363
+ detectAbortRef.current = abort;
364
+
365
+ for (let attempt = 0; attempt < 3; attempt++) {
366
+ if (abort.signal.aborted) return;
367
+ try {
368
+ const result = await api.post<{ chatId: string | null; firstName?: string; username?: string; verified?: boolean; timeout?: boolean }>(Api.Wallet, '/adapters/telegram/detect-chat', { setupToken: token });
369
+ if (abort.signal.aborted) return;
370
+
371
+ if (result.chatId) {
372
+ setChatId(result.chatId);
373
+ const name = result.username ? `@${result.username}` : result.firstName || '';
374
+ setDetectedName(name);
375
+ // Auto-proceed: save bot token, adapter config, restart, test, and finish
376
+ setTelegramStep('testing');
377
+ try {
378
+ await api.post(Api.Wallet, '/apikeys', { service: 'adapter:telegram', name: 'botToken', key: botToken });
379
+ await api.post(Api.Wallet, '/adapters', {
380
+ type: 'telegram',
381
+ enabled: true,
382
+ config: { chatId: result.chatId },
383
+ chat: { enabled: true },
384
+ });
385
+ await api.post(Api.Wallet, '/adapters/restart');
386
+ try {
387
+ await api.post(Api.Wallet, '/adapters/test', { type: 'telegram' });
388
+ } catch { /* non-critical */ }
389
+ setTelegramStep('done');
390
+ setBotToken('');
391
+ setEditingStep(null);
392
+ const setup = await fetchSetupStatus();
393
+ autoExpand(aiStatus, setup);
394
+ } catch {
395
+ // Fall back to manual chatId step on error
396
+ setTelegramStep('chatId');
397
+ }
398
+ return;
399
+ }
400
+ // timeout — try again
401
+ } catch {
402
+ if (abort.signal.aborted) return;
403
+ // Fall through to manual
404
+ break;
405
+ }
406
+ }
407
+
408
+ // Timed out — fall back to manual entry
409
+ if (!abort.signal.aborted) {
410
+ setTelegramStep('chatId');
411
+ setTelegramError('Auto-detection timed out. Enter your chat ID manually.');
412
+ }
413
+ };
414
+
415
+ // Clean up detection polling on unmount
416
+ useEffect(() => {
417
+ return () => {
418
+ detectAbortRef.current?.abort();
419
+ };
420
+ }, []);
421
+
422
+ // Telegram flow: save config + restart + test
423
+ const handleTelegramActivate = async () => {
424
+ setTelegramError('');
425
+ setTelegramValidating(true);
426
+ setTelegramStep('testing');
427
+ try {
428
+ // Save bot token
429
+ await api.post(Api.Wallet, '/apikeys', { service: 'adapter:telegram', name: 'botToken', key: botToken });
430
+ // Save adapter config (include chat opt-in)
431
+ await api.post(Api.Wallet, '/adapters', {
432
+ type: 'telegram',
433
+ enabled: true,
434
+ config: { chatId },
435
+ ...(chatEnabled ? { chat: { enabled: true } } : {}),
436
+ });
437
+ // Restart adapters
438
+ await api.post(Api.Wallet, '/adapters/restart');
439
+ // Test
440
+ const testResult = await api.post<{ success: boolean; error?: string }>(Api.Wallet, '/adapters/test', { type: 'telegram' });
441
+ if (!testResult.success) {
442
+ setTelegramError(testResult.error || 'Test message failed');
443
+ setTelegramStep('chatId');
444
+ return;
445
+ }
446
+ setTelegramStep('done');
447
+ setBotToken('');
448
+ setChatId('');
449
+ setEditingStep(null);
450
+ const setup = await fetchSetupStatus();
451
+ autoExpand(aiStatus, setup);
452
+ } catch (err) {
453
+ setTelegramError(err instanceof Error ? err.message : 'Activation failed');
454
+ setTelegramStep('chatId');
455
+ } finally {
456
+ setTelegramValidating(false);
457
+ }
458
+ };
459
+
460
+ // Remove an API key by service name
461
+ const removeApiKey = async (service: string) => {
462
+ const { apiKeys } = await api.get<{ apiKeys: { id: string; service: string }[] }>(Api.Wallet, '/apikeys');
463
+ const key = apiKeys.find(k => k.service === service);
464
+ if (key) {
465
+ await api.delete(Api.Wallet, `/apikeys/${key.id}`);
466
+ }
467
+ };
468
+
469
+ const handleRemoveProvider = () => {
470
+ // Just clear local state and show the form — no API calls needed.
471
+ // The old provider is only replaced when the user saves a new one.
472
+ setProviderKeyInput('');
473
+ setProviderError('');
474
+ setProviderTestResult(null);
475
+ setDoneTestResult(null);
476
+ setEditingStep(1);
477
+ };
478
+
479
+ const handleRemoveAlchemy = async () => {
480
+ setRemovingAlchemy(true);
481
+ try {
482
+ await removeApiKey('alchemy');
483
+ setEditingStep(2);
484
+ const setup = await fetchSetupStatus();
485
+ autoExpand(aiStatus, setup);
486
+ } finally {
487
+ setRemovingAlchemy(false);
488
+ }
489
+ };
490
+
491
+ const handleRemoveTelegram = async () => {
492
+ setRemovingTelegram(true);
493
+ try {
494
+ // Abort any in-flight detection
495
+ detectAbortRef.current?.abort();
496
+ // Delete the adapter
497
+ await api.delete(Api.Wallet, '/adapters/telegram');
498
+ // Delete the bot token API key
499
+ await removeApiKey('adapter:telegram');
500
+ // Restart adapters
501
+ await api.post(Api.Wallet, '/adapters/restart');
502
+ // Clear all telegram state
503
+ setBotToken('');
504
+ setBotUsername('');
505
+ setChatId('');
506
+ setChatEnabled(false);
507
+ setDeepLink('');
508
+ setSetupToken('');
509
+ setDetectedName('');
510
+ setTelegramStep('token');
511
+ setTelegramError('');
512
+ setEditingStep(3);
513
+ const setup = await fetchSetupStatus();
514
+ autoExpand(aiStatus, setup);
515
+ } finally {
516
+ setRemovingTelegram(false);
517
+ }
518
+ };
519
+
520
+ const handleDismiss = () => {
521
+ const addr = status?.address || 'unknown';
522
+ localStorage.setItem(`setupWizardDismissed:${addr}`, 'true');
523
+ setDismissed(true);
524
+ };
525
+
526
+ if (dismissed) {
527
+ return (
528
+ <div className="flex items-center justify-center py-8">
529
+ <div className="font-mono text-[10px]" style={{ color: 'var(--color-text-muted)' }}>
530
+ Dismissed. Close this app with the X button above.
531
+ </div>
532
+ </div>
533
+ );
534
+ }
535
+
536
+ if (loading) {
537
+ return (
538
+ <div className="flex items-center justify-center py-8">
539
+ <Loader2 size={20} className="animate-spin" style={{ color: 'var(--color-text-muted)' }} />
540
+ </div>
541
+ );
542
+ }
543
+
544
+ const aiStepDone = isAiStepDone(aiStatus);
545
+ const step1Done = aiStepDone || agentPairAcknowledged;
546
+ const allDone = step1Done && status?.apiKeys.alchemy && status?.adapters.telegram;
547
+
548
+ // Derive selected provider info for rendering
549
+ const selectedProviderInfo = aiStatus?.providers.find(p => p.mode === selectedProvider);
550
+ const activeProviderInfo = aiStatus?.providers.find(p => p.mode === aiStatus.activeProvider);
551
+ const keyService = PROVIDER_KEY_SERVICE[selectedProvider];
552
+ const isCliProvider = keyService === null;
553
+
554
+ return (
555
+ <div className="space-y-1 p-1">
556
+ {/* Header */}
557
+ <div className="text-center mb-3">
558
+ <div className="font-mono text-[10px] tracking-widest" style={{ color: 'var(--color-text-muted)' }}>
559
+ {allDone ? 'SETUP COMPLETE' : 'FINISH YOUR SETUP'}
560
+ </div>
561
+ </div>
562
+
563
+ {allDone && (
564
+ <div className="text-center py-4 space-y-3">
565
+ <div className="w-10 h-10 mx-auto flex items-center justify-center" style={{ background: 'var(--color-success)', color: 'white' }}>
566
+ <Check size={20} />
567
+ </div>
568
+ <div className="font-mono text-xs" style={{ color: 'var(--color-text)' }}>All set! Your wallet is fully configured.</div>
569
+ </div>
570
+ )}
571
+
572
+ {/* Step 1: AI Agent */}
573
+ <StepItem
574
+ number={1}
575
+ title="AI Agent"
576
+ subtitle="Connect an AI provider or pair with an agent"
577
+ icon={<Bot size={14} />}
578
+ done={(aiStepDone || agentPairAcknowledged) && editingStep !== 1}
579
+ expanded={expandedStep === 1}
580
+ onToggle={() => setExpandedStep(expandedStep === 1 ? null : 1)}
581
+ doneContent={
582
+ <div className="space-y-2 pt-2">
583
+ {aiStepDone && !agentPairAcknowledged && (
584
+ <>
585
+ <div className="flex items-center gap-2 p-2" style={{ background: 'var(--color-background-alt)', border: '1px solid var(--color-border)' }}>
586
+ <Check size={12} style={{ color: 'var(--color-success)' }} />
587
+ <span className="font-mono text-[10px]" style={{ color: 'var(--color-success)' }}>
588
+ {activeProviderInfo?.label || aiStatus?.activeProvider || 'AI provider'} configured
589
+ </span>
590
+ </div>
591
+ {doneTestResult === 'success' && (
592
+ <div className="flex items-center gap-2 p-1.5" style={{ border: '1px solid var(--color-success)', color: 'var(--color-success)' }}>
593
+ <Check size={10} />
594
+ <span className="font-mono text-[9px]">Provider available</span>
595
+ </div>
596
+ )}
597
+ {doneTestResult === 'fail' && (
598
+ <div className="font-mono text-[9px]" style={{ color: 'var(--color-warning)' }}>Provider not available</div>
599
+ )}
600
+ <div className="flex gap-2">
601
+ <Button variant="ghost" size="sm" onClick={async () => {
602
+ setDoneTestLoading(true);
603
+ setDoneTestResult(null);
604
+ try {
605
+ const res = await api.get<AiStatusResponse>(Api.Wallet, '/ai/status');
606
+ setAiStatus(res);
607
+ const provider = res.providers.find(p => p.mode === res.activeProvider);
608
+ setDoneTestResult(provider?.available ? 'success' : 'fail');
609
+ } catch {
610
+ setDoneTestResult('fail');
611
+ } finally {
612
+ setDoneTestLoading(false);
613
+ }
614
+ }} disabled={doneTestLoading} loading={doneTestLoading} icon={<FlaskConical size={10} />}>
615
+ TEST
616
+ </Button>
617
+ <Button variant="ghost" size="sm" onClick={handleRemoveProvider} icon={<Trash2 size={10} />}>
618
+ CHANGE
619
+ </Button>
620
+ </div>
621
+ </>
622
+ )}
623
+ {agentPairAcknowledged && (
624
+ <>
625
+ <div className="flex items-center gap-2 p-2" style={{ background: 'var(--color-background-alt)', border: '1px solid var(--color-border)' }}>
626
+ <Check size={12} style={{ color: 'var(--color-success)' }} />
627
+ <span className="font-mono text-[10px]" style={{ color: 'var(--color-success)' }}>
628
+ Agent paired externally
629
+ </span>
630
+ </div>
631
+ <Button variant="ghost" size="sm" onClick={() => {
632
+ const addr = status?.address || 'unknown';
633
+ localStorage.removeItem(`agentPaired:${addr}`);
634
+ setAgentPairAcknowledged(false);
635
+ setEditingStep(1);
636
+ }} icon={<Trash2 size={10} />}>
637
+ CHANGE
638
+ </Button>
639
+ </>
640
+ )}
641
+ </div>
642
+ }
643
+ >
644
+ <div className="space-y-3 pt-2">
645
+ {/* Tab selector */}
646
+ <div className="flex gap-2">
647
+ <button
648
+ onClick={() => setStep1Tab('provider')}
649
+ className="flex-1 p-1.5 font-mono text-[9px] text-left"
650
+ style={{
651
+ border: step1Tab === 'provider' ? '1px solid var(--color-accent, #ccff00)' : '1px solid var(--color-border)',
652
+ background: step1Tab === 'provider' ? 'var(--color-background-alt)' : 'transparent',
653
+ color: step1Tab === 'provider' ? 'var(--color-text)' : 'var(--color-text-muted)',
654
+ }}
655
+ >
656
+ <div className="font-bold">AI Provider</div>
657
+ <div className="text-[8px]" style={{ color: 'var(--color-text-muted)' }}>Claude Max, API keys</div>
658
+ </button>
659
+ <button
660
+ onClick={() => setStep1Tab('agent')}
661
+ className="flex-1 p-1.5 font-mono text-[9px] text-left"
662
+ style={{
663
+ border: step1Tab === 'agent' ? '1px solid var(--color-accent, #ccff00)' : '1px solid var(--color-border)',
664
+ background: step1Tab === 'agent' ? 'var(--color-background-alt)' : 'transparent',
665
+ color: step1Tab === 'agent' ? 'var(--color-text)' : 'var(--color-text-muted)',
666
+ }}
667
+ >
668
+ <div className="font-bold">Pair with Agent</div>
669
+ <div className="text-[8px]" style={{ color: 'var(--color-text-muted)' }}>MCP, skill, CLI, API</div>
670
+ </button>
671
+ </div>
672
+
673
+ {/* Provider tab */}
674
+ {step1Tab === 'provider' && (
675
+ <>
676
+ {aiLoading ? (
677
+ <div className="py-3 flex items-center justify-center">
678
+ <Loader2 size={14} className="animate-spin" style={{ color: 'var(--color-text-muted)' }} />
679
+ </div>
680
+ ) : (
681
+ <>
682
+ {/* Provider radio buttons */}
683
+ <div className="space-y-1">
684
+ <div className="font-mono text-[9px] font-bold" style={{ color: 'var(--color-text)' }}>Provider</div>
685
+ {aiStatus?.providers.map((p) => (
686
+ <label
687
+ key={p.mode}
688
+ className="flex items-center gap-2 cursor-pointer py-1 px-1.5"
689
+ style={{
690
+ border: selectedProvider === p.mode ? '1px solid var(--color-accent, #ccff00)' : '1px solid transparent',
691
+ background: selectedProvider === p.mode ? 'var(--color-background-alt, #f4f4f5)' : 'transparent',
692
+ }}
693
+ >
694
+ <input
695
+ type="radio"
696
+ name="setup-ai-provider"
697
+ value={p.mode}
698
+ checked={selectedProvider === p.mode}
699
+ onChange={() => handleProviderChange(p.mode)}
700
+ className="accent-[var(--color-accent,#ccff00)]"
701
+ />
702
+ <span className="font-mono text-[9px] flex-1" style={{ color: 'var(--color-text)' }}>
703
+ {p.label}
704
+ </span>
705
+ {p.available ? (
706
+ <Check size={10} style={{ color: 'var(--color-success, #22c55e)' }} />
707
+ ) : (
708
+ <X size={10} style={{ color: 'var(--color-text-muted)' }} />
709
+ )}
710
+ {!p.available && (
711
+ <span className="font-mono text-[8px]" style={{ color: 'var(--color-text-muted)' }}>
712
+ {p.reason}
713
+ </span>
714
+ )}
715
+ </label>
716
+ ))}
717
+ </div>
718
+
719
+ {/* CLI provider: instructions + test button */}
720
+ {isCliProvider && selectedProviderInfo && (
721
+ <div className="space-y-2">
722
+ <div className="p-1.5 font-mono text-[8px]" style={{ background: 'var(--color-background-alt)', border: '1px solid var(--color-border)', color: 'var(--color-text-muted)' }}>
723
+ {CLI_INSTRUCTIONS[selectedProvider] || `Make sure the CLI is installed and authenticated.`}
724
+ </div>
725
+ {providerTestResult === 'success' && (
726
+ <div className="flex items-center gap-2 p-1.5" style={{ border: '1px solid var(--color-success)', color: 'var(--color-success)' }}>
727
+ <Check size={10} />
728
+ <span className="font-mono text-[9px]">Available! Provider saved.</span>
729
+ </div>
730
+ )}
731
+ <Button size="sm" onClick={handleProviderTest} disabled={providerTesting || providerSaving} loading={providerTesting || providerSaving} icon={<FlaskConical size={10} />}>
732
+ TEST
733
+ </Button>
734
+ </div>
735
+ )}
736
+
737
+ {/* API provider: key input form */}
738
+ {!isCliProvider && keyService && (
739
+ <div className="space-y-2">
740
+ <TextInput
741
+ label={`${KEY_SERVICE_LABEL[keyService] || keyService} API Key`}
742
+ type="password"
743
+ value={providerKeyInput}
744
+ onChange={e => { setProviderKeyInput(e.target.value); setProviderError(''); }}
745
+ placeholder={KEY_PLACEHOLDER[keyService] || 'Paste your API key...'}
746
+ compact
747
+ rightElement={<KeyRound size={10} style={{ color: 'var(--color-text-muted)' }} />}
748
+ />
749
+ <Button size="sm" onClick={handleProviderKeySave} disabled={providerKeyValidating || !providerKeyInput.trim()} loading={providerKeyValidating}>
750
+ VALIDATE & SAVE
751
+ </Button>
752
+ </div>
753
+ )}
754
+
755
+ {/* Error display */}
756
+ {providerError && (
757
+ <div className="font-mono text-[9px]" style={{ color: 'var(--color-warning)' }}>{providerError}</div>
758
+ )}
759
+
760
+ {/* Permission Tier Toggle */}
761
+ <div className="space-y-1 pt-2 mt-2" style={{ borderTop: '1px solid var(--color-border)' }}>
762
+ <div className="font-mono text-[9px] font-bold" style={{ color: 'var(--color-text)' }}>Permission Level</div>
763
+ <div className="flex gap-2">
764
+ <button
765
+ onClick={() => handleTierChange('admin')}
766
+ disabled={agentTierSaving}
767
+ className="flex-1 p-1.5 font-mono text-[9px] text-left"
768
+ style={{
769
+ border: agentTier === 'admin' ? '1px solid var(--color-accent, #ccff00)' : '1px solid var(--color-border)',
770
+ background: agentTier === 'admin' ? 'var(--color-background-alt)' : 'transparent',
771
+ color: agentTier === 'admin' ? 'var(--color-text)' : 'var(--color-text-muted)',
772
+ opacity: agentTierSaving ? 0.5 : 1,
773
+ }}
774
+ >
775
+ <div className="font-bold">Full Admin</div>
776
+ <div className="text-[8px]" style={{ color: 'var(--color-text-muted)' }}>Recommended</div>
777
+ </button>
778
+ <button
779
+ onClick={() => handleTierChange('restricted')}
780
+ disabled={agentTierSaving}
781
+ className="flex-1 p-1.5 font-mono text-[9px] text-left"
782
+ style={{
783
+ border: agentTier === 'restricted' ? '1px solid var(--color-accent, #ccff00)' : '1px solid var(--color-border)',
784
+ background: agentTier === 'restricted' ? 'var(--color-background-alt)' : 'transparent',
785
+ color: agentTier === 'restricted' ? 'var(--color-text)' : 'var(--color-text-muted)',
786
+ opacity: agentTierSaving ? 0.5 : 1,
787
+ }}
788
+ >
789
+ <div className="font-bold">Restricted</div>
790
+ <div className="text-[8px]" style={{ color: 'var(--color-text-muted)' }}>Approval required</div>
791
+ </button>
792
+ </div>
793
+ </div>
794
+ </>
795
+ )}
796
+ </>
797
+ )}
798
+
799
+ {/* Agent tab */}
800
+ {step1Tab === 'agent' && (
801
+ <div className="space-y-2">
802
+ <div className="font-mono text-[9px]" style={{ color: 'var(--color-text-muted)' }}>
803
+ Choose the path that matches your setup:
804
+ </div>
805
+ {[
806
+ { label: 'Agent Skill', desc: 'Claude Code, Cursor, VS Code, Windsurf', cmd: 'npx skills add Aura-Industry/aurawallet', note: 'Then ask: "Set up my wallet"' },
807
+ { label: 'MCP Server', desc: 'Claude Desktop or any MCP client', cmd: 'npx aurawallet mcp --install', note: 'Auto-configures your IDE' },
808
+ { label: 'Headless CLI', desc: 'Local bots, CI/CD, containers', cmd: 'npm run cli', note: 'Approve agent requests in terminal' },
809
+ { label: 'Direct API', desc: 'Any language or platform', cmd: 'curl http://localhost:4242/health', note: 'POST /auth to bootstrap a token' },
810
+ ].map((path) => (
811
+ <div key={path.label} className="p-2" style={{ background: 'var(--color-background-alt)', border: '1px solid var(--color-border)' }}>
812
+ <div className="flex items-center justify-between">
813
+ <span className="font-mono text-[10px] font-bold" style={{ color: 'var(--color-text)' }}>{path.label}</span>
814
+ <span className="font-mono text-[8px]" style={{ color: 'var(--color-text-muted)' }}>{path.desc}</span>
815
+ </div>
816
+ <div className="font-mono text-[9px] mt-1 px-1.5 py-1" style={{ background: 'var(--color-surface)', border: '1px solid var(--color-border)', color: 'var(--color-text-muted)' }}>
817
+ {path.cmd}
818
+ </div>
819
+ <div className="font-mono text-[8px] mt-1" style={{ color: 'var(--color-text-faint, #9ca3af)' }}>{path.note}</div>
820
+ </div>
821
+ ))}
822
+ <Button size="sm" onClick={() => {
823
+ const addr = status?.address || 'unknown';
824
+ localStorage.setItem(`agentPaired:${addr}`, 'true');
825
+ setAgentPairAcknowledged(true);
826
+ setEditingStep(null);
827
+ autoExpand(aiStatus, status);
828
+ }} className="w-full">
829
+ I&apos;VE CONNECTED
830
+ </Button>
831
+ </div>
832
+ )}
833
+ </div>
834
+ </StepItem>
835
+
836
+ {/* Step 2: RPC Provider */}
837
+ <StepItem
838
+ number={2}
839
+ title="RPC Provider"
840
+ subtitle="Add Alchemy for reliable RPC"
841
+ icon={<Globe size={14} />}
842
+ done={!!status?.apiKeys.alchemy && editingStep !== 2}
843
+ expanded={expandedStep === 2}
844
+ onToggle={() => setExpandedStep(expandedStep === 2 ? null : 2)}
845
+ doneContent={
846
+ <div className="space-y-2 pt-2">
847
+ <div className="flex items-center gap-2 p-2" style={{ background: 'var(--color-background-alt)', border: '1px solid var(--color-border)' }}>
848
+ <Check size={12} style={{ color: 'var(--color-success)' }} />
849
+ <span className="font-mono text-[10px]" style={{ color: 'var(--color-success)' }}>Alchemy key configured</span>
850
+ </div>
851
+ <Button variant="ghost" size="sm" onClick={handleRemoveAlchemy} disabled={removingAlchemy} loading={removingAlchemy} icon={<Trash2 size={10} />}>
852
+ REMOVE
853
+ </Button>
854
+ </div>
855
+ }
856
+ >
857
+ <div className="space-y-2 pt-2">
858
+ <TextInput label="ALCHEMY_KEY" type="password" value={alchemyKey} onChange={e => setAlchemyKey(e.target.value)} placeholder="Paste your Alchemy API key..." compact />
859
+ {alchemyError && <div className="font-mono text-[9px]" style={{ color: 'var(--color-warning)' }}>{alchemyError}</div>}
860
+ <Button size="sm" onClick={handleAlchemySave} disabled={alchemyValidating || !alchemyKey} loading={alchemyValidating}>
861
+ VALIDATE & SAVE
862
+ </Button>
863
+ </div>
864
+ </StepItem>
865
+
866
+ {/* Step 3: Mobile Approvals */}
867
+ <StepItem
868
+ number={3}
869
+ title="Mobile Approvals"
870
+ subtitle="Approve agent actions via Telegram"
871
+ icon={<MessageSquare size={14} />}
872
+ done={!!status?.adapters.telegram && editingStep !== 3}
873
+ expanded={expandedStep === 3}
874
+ onToggle={() => setExpandedStep(expandedStep === 3 ? null : 3)}
875
+ doneContent={
876
+ <div className="space-y-2 pt-2">
877
+ <div className="flex items-center gap-2 p-2" style={{ background: 'var(--color-background-alt)', border: '1px solid var(--color-border)' }}>
878
+ <Check size={12} style={{ color: 'var(--color-success)' }} />
879
+ <span className="font-mono text-[10px]" style={{ color: 'var(--color-success)' }}>Telegram connected</span>
880
+ </div>
881
+ <div className="font-mono text-[8px]" style={{ color: 'var(--color-text-muted)' }}>
882
+ Agent chat is enabled by default. You can toggle it in System Defaults &rarr; Adapter Chat.
883
+ </div>
884
+ <Button variant="ghost" size="sm" onClick={handleRemoveTelegram} disabled={removingTelegram} loading={removingTelegram} icon={<Trash2 size={10} />}>
885
+ REMOVE
886
+ </Button>
887
+ </div>
888
+ }
889
+ >
890
+ <div className="space-y-2 pt-2">
891
+ {telegramStep === 'token' && (
892
+ <>
893
+ <div className="font-mono text-[8px]" style={{ color: 'var(--color-text-muted)' }}>
894
+ Approve agent actions from your phone via Telegram. Optionally, chat with your AI agent directly.
895
+ </div>
896
+ <TextInput label="BOT_TOKEN" type="password" value={botToken} onChange={e => setBotToken(e.target.value)} placeholder="123456:ABC-DEF..." compact />
897
+ <div className="font-mono text-[8px]" style={{ color: 'var(--color-text-faint)' }}>
898
+ Create a bot via @BotFather on Telegram
899
+ </div>
900
+ <Button size="sm" onClick={handleTelegramValidate} disabled={telegramValidating || !botToken} loading={telegramValidating}>
901
+ VALIDATE BOT
902
+ </Button>
903
+ </>
904
+ )}
905
+ {telegramStep === 'detecting' && (
906
+ <>
907
+ {botUsername && (
908
+ <div className="flex items-center gap-2 p-2" style={{ background: 'var(--color-background-alt)', border: '1px solid var(--color-border)' }}>
909
+ <Check size={12} style={{ color: 'var(--color-success)' }} />
910
+ <span className="font-mono text-[10px]" style={{ color: 'var(--color-text)' }}>@{botUsername}</span>
911
+ </div>
912
+ )}
913
+ <Button variant="secondary" size="sm" icon={<ExternalLink size={10} />}
914
+ onClick={() => window.open(deepLink, '_blank')}>
915
+ Open @{botUsername} in Telegram
916
+ </Button>
917
+ <div className="font-mono text-[8px]" style={{ color: 'var(--color-text-faint)' }}>
918
+ Click the link above, then press Start in Telegram
919
+ </div>
920
+ <div className="flex items-center gap-2 py-2">
921
+ <Loader2 size={14} className="animate-spin" style={{ color: 'var(--color-info)' }} />
922
+ <span className="font-mono text-[10px]" style={{ color: 'var(--color-text-muted)' }}>Waiting for you to press Start...</span>
923
+ </div>
924
+ <Button variant="ghost" size="sm" onClick={() => { detectAbortRef.current?.abort(); setTelegramStep('chatId'); }}>
925
+ ENTER MANUALLY
926
+ </Button>
927
+ </>
928
+ )}
929
+ {telegramStep === 'chatId' && (
930
+ <>
931
+ {botUsername && (
932
+ <div className="flex items-center gap-2 p-2" style={{ background: 'var(--color-background-alt)', border: '1px solid var(--color-border)' }}>
933
+ <Check size={12} style={{ color: 'var(--color-success)' }} />
934
+ <span className="font-mono text-[10px]" style={{ color: 'var(--color-text)' }}>@{botUsername}</span>
935
+ </div>
936
+ )}
937
+ {detectedName && chatId && (
938
+ <div className="flex items-center gap-2 p-2" style={{ background: 'var(--color-background-alt)', border: '1px solid var(--color-success)' }}>
939
+ <Check size={12} style={{ color: 'var(--color-success)' }} />
940
+ <span className="font-mono text-[10px]" style={{ color: 'var(--color-success)' }}>Detected: {detectedName} ({chatId})</span>
941
+ </div>
942
+ )}
943
+ {!detectedName && (
944
+ <>
945
+ <TextInput label="CHAT_ID" type="text" value={chatId} onChange={e => setChatId(e.target.value)} placeholder="Your Telegram chat ID..." compact />
946
+ <div className="font-mono text-[8px]" style={{ color: 'var(--color-text-faint)' }}>
947
+ Send /start to your bot, then use @userinfobot to get your chat ID
948
+ </div>
949
+ </>
950
+ )}
951
+ <label className="flex items-center gap-2 py-1 cursor-pointer">
952
+ <input
953
+ type="checkbox"
954
+ checked={chatEnabled}
955
+ onChange={e => setChatEnabled(e.target.checked)}
956
+ className="accent-[var(--color-accent)]"
957
+ />
958
+ <span className="font-mono text-[10px]" style={{ color: 'var(--color-text)' }}>Enable agent chat</span>
959
+ </label>
960
+ <div className="font-mono text-[8px]" style={{ color: 'var(--color-text-faint)' }}>
961
+ Enable this to chat with your agent via Telegram. Your AI agent will respond to messages you send in this chat.
962
+ </div>
963
+ <Button size="sm" onClick={handleTelegramActivate} disabled={telegramValidating || !chatId} loading={telegramValidating}>
964
+ ACTIVATE & TEST
965
+ </Button>
966
+ </>
967
+ )}
968
+ {telegramStep === 'testing' && (
969
+ <div className="flex items-center gap-2 py-2">
970
+ <Loader2 size={14} className="animate-spin" style={{ color: 'var(--color-info)' }} />
971
+ <span className="font-mono text-[10px]" style={{ color: 'var(--color-text-muted)' }}>Testing connection...</span>
972
+ </div>
973
+ )}
974
+ {telegramStep === 'done' && (
975
+ <div className="flex items-center gap-2 py-2">
976
+ <Check size={14} style={{ color: 'var(--color-success)' }} />
977
+ <span className="font-mono text-[10px]" style={{ color: 'var(--color-success)' }}>Telegram connected!</span>
978
+ </div>
979
+ )}
980
+ {telegramError && <div className="font-mono text-[9px]" style={{ color: 'var(--color-warning)' }}>{telegramError}</div>}
981
+ </div>
982
+ </StepItem>
983
+
984
+ {!allDone && (
985
+ <div className="pt-3 flex justify-end">
986
+ <Button variant="ghost" size="sm" onClick={handleDismiss}>
987
+ SKIP FOR NOW
988
+ </Button>
989
+ </div>
990
+ )}
991
+
992
+ {allDone && (
993
+ <div className="pt-3 flex justify-end">
994
+ <Button variant="ghost" size="sm" onClick={handleDismiss}>
995
+ DISMISS
996
+ </Button>
997
+ </div>
998
+ )}
999
+ </div>
1000
+ );
1001
+ };
1002
+
1003
+ export { SetupWizardApp };
1004
+ export default SetupWizardApp;