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,845 @@
1
+ 'use client';
2
+
3
+ import React, { useCallback, useEffect, useMemo, useRef, useState } from 'react';
4
+ import { Check, Loader2, MessageSquare, RotateCcw, Save, X, KeyRound, ExternalLink } from 'lucide-react';
5
+ import { api, Api } from '@/lib/api';
6
+ import { Button, TextInput } from '@/components/design-system';
7
+
8
+ type DefaultType = 'permissions' | 'financial' | 'swap' | 'ttl' | 'rate_limit' | 'ai_safety' | 'launch' | 'app';
9
+
10
+ interface DefaultItem {
11
+ key: string;
12
+ value: unknown;
13
+ type: DefaultType | string;
14
+ label: string;
15
+ description: string | null;
16
+ updatedAt: string;
17
+ }
18
+
19
+ interface DefaultsResponse {
20
+ success: boolean;
21
+ defaults: Record<string, DefaultItem[]>;
22
+ }
23
+
24
+ interface ProviderInfo {
25
+ mode: string;
26
+ label: string;
27
+ available: boolean;
28
+ reason: string;
29
+ models: string[];
30
+ }
31
+
32
+ interface AiTiers {
33
+ fast: string;
34
+ standard: string;
35
+ powerful: string;
36
+ }
37
+
38
+ interface AiStatusResponse {
39
+ activeProvider: string;
40
+ tiers: AiTiers;
41
+ providers: ProviderInfo[];
42
+ }
43
+
44
+ const KNOWN_PERMISSIONS = [
45
+ 'wallet:create:hot',
46
+ 'send:hot',
47
+ 'swap',
48
+ 'fund',
49
+ 'action:create',
50
+ ];
51
+
52
+ const EDITABLE_KEYS = [
53
+ 'limits.fund',
54
+ 'limits.send',
55
+ 'limits.swap',
56
+ 'swap.max_slippage',
57
+ 'swap.min_slippage_admin',
58
+ 'swap.min_slippage_agent',
59
+ 'permissions.default',
60
+ ] as const;
61
+
62
+ const SUFFIX_BY_KEY: Partial<Record<(typeof EDITABLE_KEYS)[number], string>> = {
63
+ 'limits.fund': 'ETH',
64
+ 'limits.send': 'ETH',
65
+ 'limits.swap': 'ETH',
66
+ 'swap.max_slippage': '%',
67
+ 'swap.min_slippage_admin': '%',
68
+ 'swap.min_slippage_agent': '%',
69
+ };
70
+
71
+ /** Tier labels for display */
72
+ const TIER_LABELS: Record<string, string> = {
73
+ fast: 'Fast',
74
+ standard: 'Standard',
75
+ powerful: 'Powerful',
76
+ };
77
+
78
+ /** Map provider mode to the API key service name it requires (if any) */
79
+ const PROVIDER_KEY_SERVICE: Record<string, string | null> = {
80
+ 'claude-cli': null,
81
+ 'claude-api': 'anthropic',
82
+ 'codex-cli': null,
83
+ 'openai-api': 'openai',
84
+ };
85
+
86
+ /** Human-readable label for API key services */
87
+ const KEY_SERVICE_LABEL: Record<string, string> = {
88
+ anthropic: 'Anthropic',
89
+ openai: 'OpenAI',
90
+ };
91
+
92
+ function formatInputValue(value: unknown): string {
93
+ if (typeof value === 'number') return String(value);
94
+ if (typeof value === 'string') return value;
95
+ return '';
96
+ }
97
+
98
+ // ─── AI Engine Section ─────────────────────────────────────────────
99
+
100
+ export function AiEngineSection() {
101
+ const [aiStatus, setAiStatus] = useState<AiStatusResponse | null>(null);
102
+ const [aiLoading, setAiLoading] = useState(true);
103
+ const [aiError, setAiError] = useState<string | null>(null);
104
+ const [aiSaving, setAiSaving] = useState(false);
105
+ const [aiMessage, setAiMessage] = useState<string | null>(null);
106
+
107
+ // Draft state (local until SAVE)
108
+ const [draftProvider, setDraftProvider] = useState<string>('claude-cli');
109
+
110
+ // API key inline form
111
+ const [showKeyForm, setShowKeyForm] = useState(false);
112
+ const [keyInput, setKeyInput] = useState('');
113
+ const [keyValidating, setKeyValidating] = useState(false);
114
+ const [keyError, setKeyError] = useState<string | null>(null);
115
+ const [keySaved, setKeySaved] = useState(false);
116
+
117
+ const loadAiStatus = useCallback(async () => {
118
+ setAiLoading(true);
119
+ setAiError(null);
120
+ try {
121
+ const res = await api.get<AiStatusResponse>(Api.Wallet, '/ai/status');
122
+ setAiStatus(res);
123
+ setDraftProvider(res.activeProvider);
124
+ } catch (err) {
125
+ setAiError(err instanceof Error ? err.message : 'Failed to load AI status');
126
+ } finally {
127
+ setAiLoading(false);
128
+ }
129
+ }, []);
130
+
131
+ useEffect(() => { void loadAiStatus(); }, [loadAiStatus]);
132
+
133
+ // Get current provider info
134
+ const selectedProvider = aiStatus?.providers.find(p => p.mode === draftProvider);
135
+
136
+ const handleProviderChange = (mode: string) => {
137
+ setDraftProvider(mode);
138
+ setShowKeyForm(false);
139
+ setKeyInput('');
140
+ setKeyError(null);
141
+ setKeySaved(false);
142
+ };
143
+
144
+ // Check if selected provider needs a key that isn't configured
145
+ const keyService = PROVIDER_KEY_SERVICE[draftProvider];
146
+ const needsKey = keyService && !selectedProvider?.available;
147
+
148
+ const onSaveAi = async () => {
149
+ setAiSaving(true);
150
+ setAiError(null);
151
+ setAiMessage(null);
152
+ try {
153
+ await api.patch(Api.Wallet, `/defaults/${encodeURIComponent('ai.provider')}`, { value: draftProvider });
154
+ setAiMessage('AI settings saved');
155
+ await loadAiStatus();
156
+ } catch (err) {
157
+ setAiError(err instanceof Error ? err.message : 'Failed to save AI settings');
158
+ } finally {
159
+ setAiSaving(false);
160
+ }
161
+ };
162
+
163
+ const onResetAi = async () => {
164
+ setAiSaving(true);
165
+ setAiError(null);
166
+ setAiMessage(null);
167
+ try {
168
+ await api.post(Api.Wallet, '/defaults/reset', { key: 'ai.provider' });
169
+ setAiMessage('AI settings reset to defaults');
170
+ await loadAiStatus();
171
+ } catch (err) {
172
+ setAiError(err instanceof Error ? err.message : 'Failed to reset AI settings');
173
+ } finally {
174
+ setAiSaving(false);
175
+ }
176
+ };
177
+
178
+ const onValidateKey = async () => {
179
+ if (!keyService || !keyInput.trim()) return;
180
+ setKeyValidating(true);
181
+ setKeyError(null);
182
+ setKeySaved(false);
183
+ try {
184
+ const res = await api.post<{ valid: boolean; error?: string }>(Api.Wallet, '/apikeys/validate', {
185
+ service: keyService,
186
+ key: keyInput.trim(),
187
+ });
188
+ if (res.valid) {
189
+ // Save the key
190
+ await api.post(Api.Wallet, '/apikeys', {
191
+ service: keyService,
192
+ name: 'default',
193
+ key: keyInput.trim(),
194
+ });
195
+ setKeySaved(true);
196
+ setKeyInput('');
197
+ setShowKeyForm(false);
198
+ // Refresh status to show updated availability
199
+ await loadAiStatus();
200
+ } else {
201
+ setKeyError(res.error || 'Invalid API key');
202
+ }
203
+ } catch (err) {
204
+ setKeyError(err instanceof Error ? err.message : 'Validation failed');
205
+ } finally {
206
+ setKeyValidating(false);
207
+ }
208
+ };
209
+
210
+ if (aiLoading) {
211
+ return (
212
+ <div className="p-2 space-y-2" style={{ border: '1px solid var(--color-border)', background: 'var(--color-surface)' }}>
213
+ <div className="font-mono text-[10px] tracking-widest" style={{ color: 'var(--color-text-muted)' }}>
214
+ AI ENGINE
215
+ </div>
216
+ <div className="py-4 flex items-center justify-center">
217
+ <Loader2 size={14} className="animate-spin" style={{ color: 'var(--color-text-muted)' }} />
218
+ </div>
219
+ </div>
220
+ );
221
+ }
222
+
223
+ return (
224
+ <div className="p-2 space-y-2" style={{ border: '1px solid var(--color-border)', background: 'var(--color-surface)' }}>
225
+ <div>
226
+ <div className="font-mono text-[10px] tracking-widest" style={{ color: 'var(--color-text-muted)' }}>
227
+ AI ENGINE
228
+ </div>
229
+ <div className="font-mono text-[8px]" style={{ color: 'var(--color-text-muted)' }}>
230
+ Select AI provider and default model for hooks and strategies.
231
+ </div>
232
+ </div>
233
+
234
+ {aiError && (
235
+ <div className="p-1.5 font-mono text-[9px]" style={{ border: '1px solid var(--color-warning)', color: 'var(--color-warning)' }}>
236
+ {aiError}
237
+ </div>
238
+ )}
239
+ {aiMessage && (
240
+ <div className="p-1.5 font-mono text-[9px]" style={{ border: '1px solid var(--color-success)', color: 'var(--color-success)' }}>
241
+ {aiMessage}
242
+ </div>
243
+ )}
244
+
245
+ {/* Provider radio buttons */}
246
+ <div className="space-y-1">
247
+ <div className="font-mono text-[9px] font-bold" style={{ color: 'var(--color-text)' }}>Provider</div>
248
+ {aiStatus?.providers.map((p) => (
249
+ <label
250
+ key={p.mode}
251
+ className="flex items-center gap-2 cursor-pointer py-1 px-1.5"
252
+ style={{
253
+ border: draftProvider === p.mode ? '1px solid var(--color-accent, #ccff00)' : '1px solid transparent',
254
+ background: draftProvider === p.mode ? 'var(--color-background-alt, #f4f4f5)' : 'transparent',
255
+ }}
256
+ >
257
+ <input
258
+ type="radio"
259
+ name="ai-provider"
260
+ value={p.mode}
261
+ checked={draftProvider === p.mode}
262
+ onChange={() => handleProviderChange(p.mode)}
263
+ className="accent-[var(--color-accent,#ccff00)]"
264
+ />
265
+ <span className="font-mono text-[9px] flex-1" style={{ color: 'var(--color-text)' }}>
266
+ {p.label}
267
+ </span>
268
+ {p.available ? (
269
+ <Check size={10} style={{ color: 'var(--color-success, #22c55e)' }} />
270
+ ) : (
271
+ <X size={10} style={{ color: 'var(--color-text-muted)' }} />
272
+ )}
273
+ <span className="font-mono text-[8px]" style={{ color: 'var(--color-text-muted)' }}>
274
+ {p.available ? '' : p.reason}
275
+ </span>
276
+ </label>
277
+ ))}
278
+ </div>
279
+
280
+ {/* API key warning + inline form */}
281
+ {needsKey && keyService && (
282
+ <div className="space-y-1.5">
283
+ <div className="p-1.5 font-mono text-[9px] flex items-center gap-1.5" style={{ border: '1px solid var(--color-warning)', color: 'var(--color-warning)' }}>
284
+ No {KEY_SERVICE_LABEL[keyService] || keyService} API key configured.
285
+ {!showKeyForm && (
286
+ <button
287
+ onClick={() => setShowKeyForm(true)}
288
+ className="font-bold underline ml-1"
289
+ style={{ color: 'var(--color-warning)' }}
290
+ >
291
+ Add API Key
292
+ </button>
293
+ )}
294
+ </div>
295
+ {showKeyForm && (
296
+ <div className="flex items-end gap-1.5">
297
+ <div className="flex-1">
298
+ <TextInput
299
+ label={`${KEY_SERVICE_LABEL[keyService] || keyService} API Key`}
300
+ compact
301
+ value={keyInput}
302
+ onChange={(e) => { setKeyInput(e.target.value); setKeyError(null); }}
303
+ rightElement={<KeyRound size={10} style={{ color: 'var(--color-text-muted)' }} />}
304
+ />
305
+ </div>
306
+ <Button size="sm" onClick={() => void onValidateKey()} loading={keyValidating} disabled={!keyInput.trim()}>
307
+ VALIDATE
308
+ </Button>
309
+ </div>
310
+ )}
311
+ {keyError && (
312
+ <div className="font-mono text-[8px]" style={{ color: 'var(--color-warning)' }}>{keyError}</div>
313
+ )}
314
+ {keySaved && (
315
+ <div className="font-mono text-[8px]" style={{ color: 'var(--color-success)' }}>API key saved successfully</div>
316
+ )}
317
+ </div>
318
+ )}
319
+
320
+ {/* Tier-based model display */}
321
+ {aiStatus?.tiers && (
322
+ <div className="space-y-0.5">
323
+ <div className="font-mono text-[9px]" style={{ color: 'var(--color-text-muted)' }}>Model Tiers (auto-selected by permissions):</div>
324
+ {(['fast', 'standard', 'powerful'] as const).map((tier) => (
325
+ <div key={tier} className="flex items-center gap-2 pl-1">
326
+ <span className="font-mono text-[8px] w-14" style={{ color: 'var(--color-text-muted)' }}>{TIER_LABELS[tier]}:</span>
327
+ <span className="font-mono text-[9px] font-bold" style={{ color: 'var(--color-text)' }}>
328
+ {aiStatus.tiers[tier]}
329
+ </span>
330
+ </div>
331
+ ))}
332
+ </div>
333
+ )}
334
+
335
+ {/* Save / Reset */}
336
+ <div className="flex gap-2">
337
+ <Button size="sm" onClick={() => void onSaveAi()} loading={aiSaving} icon={<Save size={11} />}>
338
+ SAVE
339
+ </Button>
340
+ <Button size="sm" variant="secondary" onClick={() => void onResetAi()} disabled={aiSaving} icon={<RotateCcw size={11} />}>
341
+ RESET
342
+ </Button>
343
+ </div>
344
+ </div>
345
+ );
346
+ }
347
+
348
+ // ─── Adapter Chat Section ──────────────────────────────────────────
349
+
350
+ interface AdapterInfo {
351
+ type: string;
352
+ enabled: boolean;
353
+ config: Record<string, unknown>;
354
+ chat?: { enabled?: boolean };
355
+ hasSecrets: boolean;
356
+ secretKeys: string[];
357
+ }
358
+
359
+ interface AdaptersResponse {
360
+ success: boolean;
361
+ enabled: boolean;
362
+ chat?: { defaultApp?: string };
363
+ adapters: AdapterInfo[];
364
+ running: boolean;
365
+ }
366
+
367
+ /** Which secret keys each adapter type requires for chat to work */
368
+ const REQUIRED_SECRETS: Record<string, string[]> = {
369
+ telegram: ['botToken'],
370
+ };
371
+
372
+ function AdapterChatSection() {
373
+ const [adapters, setAdapters] = useState<AdapterInfo[]>([]);
374
+ const [routerRunning, setRouterRunning] = useState(false);
375
+ const [loading, setLoading] = useState(true);
376
+ const [error, setError] = useState<string | null>(null);
377
+ const [togglingType, setTogglingType] = useState<string | null>(null);
378
+
379
+ // Chat ID auto-detection state
380
+ const [detectingType, setDetectingType] = useState<string | null>(null);
381
+ const [deepLink, setDeepLink] = useState('');
382
+ const [, setSetupToken] = useState('');
383
+ const [botUsername, setBotUsername] = useState('');
384
+ const detectAbortRef = useRef<AbortController | null>(null);
385
+
386
+ useEffect(() => {
387
+ return () => { detectAbortRef.current?.abort(); };
388
+ }, []);
389
+
390
+ const loadAdapters = useCallback(async () => {
391
+ setLoading(true);
392
+ setError(null);
393
+ try {
394
+ const res = await api.get<AdaptersResponse>(Api.Wallet, '/adapters');
395
+ setAdapters(res.adapters || []);
396
+ setRouterRunning(res.running);
397
+ } catch (err) {
398
+ setError(err instanceof Error ? err.message : 'Failed to load adapters');
399
+ } finally {
400
+ setLoading(false);
401
+ }
402
+ }, []);
403
+
404
+ useEffect(() => { void loadAdapters(); }, [loadAdapters]);
405
+
406
+ /** Check if an adapter has all required secrets configured */
407
+ const hasMissingSecrets = (adapter: AdapterInfo): boolean => {
408
+ const required = REQUIRED_SECRETS[adapter.type];
409
+ if (!required) return false;
410
+ return required.some(key => !adapter.secretKeys.includes(key));
411
+ };
412
+
413
+ /** Start auto-detection flow for missing chatId */
414
+ const startChatIdDetection = async (adapter: AdapterInfo) => {
415
+ setDetectingType(adapter.type);
416
+ setError(null);
417
+ try {
418
+ const linkResult = await api.post<{ success: boolean; link: string; setupToken: string; botUsername: string; error?: string }>(Api.Wallet, '/adapters/telegram/setup-link', {});
419
+ if (!linkResult.success) {
420
+ setError(linkResult.error || 'Failed to generate setup link');
421
+ setDetectingType(null);
422
+ return;
423
+ }
424
+ setDeepLink(linkResult.link);
425
+ setSetupToken(linkResult.setupToken);
426
+ setBotUsername(linkResult.botUsername);
427
+
428
+ // Poll for detection
429
+ const abort = new AbortController();
430
+ detectAbortRef.current = abort;
431
+
432
+ for (let attempt = 0; attempt < 2; attempt++) {
433
+ if (abort.signal.aborted) return;
434
+ try {
435
+ const result = await api.post<{ chatId: string | null; firstName?: string; username?: string; timeout?: boolean }>(Api.Wallet, '/adapters/telegram/detect-chat', { setupToken: linkResult.setupToken });
436
+ if (abort.signal.aborted) return;
437
+
438
+ if (result.chatId) {
439
+ // Save config with detected chatId and enable chat
440
+ await api.post(Api.Wallet, '/adapters', {
441
+ type: adapter.type,
442
+ enabled: adapter.enabled,
443
+ config: { ...adapter.config, chatId: result.chatId },
444
+ chat: { enabled: true },
445
+ });
446
+ await api.post(Api.Wallet, '/adapters/restart');
447
+ try {
448
+ await api.post(Api.Wallet, '/adapters/test', { type: adapter.type });
449
+ } catch { /* non-critical */ }
450
+ setDetectingType(null);
451
+ setDeepLink('');
452
+ await loadAdapters();
453
+ return;
454
+ }
455
+ } catch {
456
+ if (abort.signal.aborted) return;
457
+ break;
458
+ }
459
+ }
460
+
461
+ // Timed out
462
+ if (!abort.signal.aborted) {
463
+ setError('Auto-detection timed out. Re-configure Telegram in the Setup Wizard to set your chat ID.');
464
+ setDetectingType(null);
465
+ setDeepLink('');
466
+ }
467
+ } catch (err) {
468
+ setError(err instanceof Error ? err.message : 'Failed to start detection');
469
+ setDetectingType(null);
470
+ }
471
+ };
472
+
473
+ const cancelDetection = () => {
474
+ detectAbortRef.current?.abort();
475
+ setDetectingType(null);
476
+ setDeepLink('');
477
+ };
478
+
479
+ const toggleChat = async (adapter: AdapterInfo) => {
480
+ const newChatEnabled = !adapter.chat?.enabled;
481
+
482
+ // When enabling chat for telegram, check if chatId is missing
483
+ if (newChatEnabled && adapter.type === 'telegram' && !adapter.config?.chatId) {
484
+ await startChatIdDetection(adapter);
485
+ return;
486
+ }
487
+
488
+ setTogglingType(adapter.type);
489
+ setError(null);
490
+ try {
491
+ // Preserve existing config — POST /adapters replaces the full entry
492
+ await api.post(Api.Wallet, '/adapters', {
493
+ type: adapter.type,
494
+ enabled: adapter.enabled,
495
+ config: adapter.config,
496
+ chat: { enabled: newChatEnabled },
497
+ });
498
+ // Restart the router so the running adapter picks up the change
499
+ await api.post(Api.Wallet, '/adapters/restart');
500
+ // Send a notification via the bot so the human knows chat is active
501
+ if (newChatEnabled) {
502
+ try {
503
+ await api.post(Api.Wallet, '/adapters/test', { type: adapter.type });
504
+ } catch {
505
+ // Non-critical — toggle still succeeded
506
+ }
507
+ }
508
+ await loadAdapters();
509
+ } catch (err) {
510
+ setError(err instanceof Error ? err.message : 'Failed to toggle chat');
511
+ } finally {
512
+ setTogglingType(null);
513
+ }
514
+ };
515
+
516
+
517
+ if (loading) {
518
+ return (
519
+ <div className="p-2 space-y-2" style={{ border: '1px solid var(--color-border)', background: 'var(--color-surface)' }}>
520
+ <div className="font-mono text-[10px] tracking-widest flex items-center gap-1" style={{ color: 'var(--color-text-muted)' }}>
521
+ <MessageSquare size={10} />
522
+ ADAPTER CHAT
523
+ </div>
524
+ <div className="py-3 flex items-center justify-center">
525
+ <Loader2 size={14} className="animate-spin" style={{ color: 'var(--color-text-muted)' }} />
526
+ </div>
527
+ </div>
528
+ );
529
+ }
530
+
531
+ const enabledAdapters = adapters.filter(a => a.enabled);
532
+ const anyChatEnabled = enabledAdapters.some(a => a.chat?.enabled === true);
533
+
534
+ return (
535
+ <div className="p-2 space-y-2" style={{ border: '1px solid var(--color-border)', background: 'var(--color-surface)' }}>
536
+ <div>
537
+ <div className="font-mono text-[10px] tracking-widest flex items-center gap-1" style={{ color: 'var(--color-text-muted)' }}>
538
+ <MessageSquare size={10} />
539
+ ADAPTER CHAT
540
+ </div>
541
+ <div className="font-mono text-[8px]" style={{ color: 'var(--color-text-muted)' }}>
542
+ Allow AI to respond to messages via external adapters.
543
+ </div>
544
+ </div>
545
+
546
+ {error && (
547
+ <div className="p-1.5 font-mono text-[9px]" style={{ border: '1px solid var(--color-warning)', color: 'var(--color-warning)' }}>
548
+ {error}
549
+ </div>
550
+ )}
551
+
552
+ {/* Chat ID auto-detection inline UI */}
553
+ {detectingType && deepLink && (
554
+ <div className="p-2 space-y-2" style={{ border: '1px solid var(--color-accent)', background: 'var(--color-background-alt)' }}>
555
+ <Button variant="secondary" size="sm" icon={<ExternalLink size={10} />}
556
+ onClick={() => window.open(deepLink, '_blank')}>
557
+ Open @{botUsername} in Telegram
558
+ </Button>
559
+ <div className="font-mono text-[8px]" style={{ color: 'var(--color-text-faint)' }}>
560
+ Click the link above, then press Start in Telegram to detect your chat ID.
561
+ </div>
562
+ <div className="flex items-center gap-2">
563
+ <Loader2 size={12} className="animate-spin" style={{ color: 'var(--color-info)' }} />
564
+ <span className="font-mono text-[9px]" style={{ color: 'var(--color-text-muted)' }}>Waiting for you to press Start...</span>
565
+ </div>
566
+ <button
567
+ onClick={cancelDetection}
568
+ className="font-mono text-[8px] underline"
569
+ style={{ color: 'var(--color-text-muted)' }}
570
+ >
571
+ Cancel
572
+ </button>
573
+ </div>
574
+ )}
575
+
576
+ {!routerRunning && anyChatEnabled && (
577
+ <div className="p-1.5 font-mono text-[8px]" style={{ border: '1px solid var(--color-warning)', color: 'var(--color-warning)' }}>
578
+ Router not running. Restart the server or toggle an adapter to start it.
579
+ </div>
580
+ )}
581
+
582
+ {enabledAdapters.length === 0 ? (
583
+ <div className="py-3 text-center font-mono text-[9px]" style={{ color: 'var(--color-text-muted)' }}>
584
+ No adapters configured. Add one via the API or CLI.
585
+ </div>
586
+ ) : (
587
+ <div className="space-y-1">
588
+ {enabledAdapters.map((adapter) => {
589
+ const chatOn = adapter.chat?.enabled === true;
590
+ const toggling = togglingType === adapter.type;
591
+ const missingSecrets = hasMissingSecrets(adapter);
592
+ return (
593
+ <div key={adapter.type}>
594
+ <div
595
+ className="flex items-center justify-between py-1.5 px-2"
596
+ style={{ border: '1px solid var(--color-border)', background: 'var(--color-background-alt)' }}
597
+ >
598
+ <div className="flex items-center gap-2">
599
+ <span className="font-mono text-[10px] font-bold uppercase" style={{ color: 'var(--color-text)' }}>
600
+ {adapter.type}
601
+ </span>
602
+ <span
603
+ className="font-mono text-[8px] px-1 py-0.5 rounded"
604
+ style={{
605
+ background: chatOn ? 'color-mix(in srgb, var(--color-success) 20%, transparent)' : 'color-mix(in srgb, var(--color-text-muted) 20%, transparent)',
606
+ color: chatOn ? 'var(--color-success)' : 'var(--color-text-muted)',
607
+ }}
608
+ >
609
+ {chatOn ? 'CHAT ON' : 'CHAT OFF'}
610
+ </span>
611
+ </div>
612
+ <button
613
+ onClick={() => void toggleChat(adapter)}
614
+ disabled={toggling || missingSecrets}
615
+ className="relative w-8 h-4 rounded-full transition-colors cursor-pointer disabled:opacity-50 disabled:cursor-not-allowed"
616
+ style={{
617
+ background: chatOn ? 'var(--color-success, #22c55e)' : 'var(--color-border, #d4d4d8)',
618
+ }}
619
+ >
620
+ <div
621
+ className="absolute top-0.5 w-3 h-3 rounded-full transition-all"
622
+ style={{
623
+ left: chatOn ? '17px' : '2px',
624
+ background: 'var(--color-surface, #ffffff)',
625
+ }}
626
+ />
627
+ </button>
628
+ </div>
629
+ {missingSecrets && (
630
+ <div className="px-2 py-1 font-mono text-[8px]" style={{ color: 'var(--color-warning)' }}>
631
+ Missing bot token. Add it in API Keys (service: adapter:{adapter.type}, name: botToken).
632
+ </div>
633
+ )}
634
+ </div>
635
+ );
636
+ })}
637
+ </div>
638
+ )}
639
+ </div>
640
+ );
641
+ }
642
+
643
+ // ─── Main Component ─────────────────────────────────────────────────
644
+
645
+ export default function SystemDefaults() {
646
+ const [loading, setLoading] = useState(true);
647
+ const [savingKey, setSavingKey] = useState<string | null>(null);
648
+ const [error, setError] = useState<string | null>(null);
649
+ const [saveMessage, setSaveMessage] = useState<string | null>(null);
650
+
651
+ const [defaultsByKey, setDefaultsByKey] = useState<Record<string, DefaultItem>>({});
652
+ const [draftValues, setDraftValues] = useState<Record<string, string>>({});
653
+ const [permissionDraft, setPermissionDraft] = useState<string[]>([]);
654
+
655
+ const loadDefaults = useCallback(async () => {
656
+ setLoading(true);
657
+ setError(null);
658
+ try {
659
+ const res = await api.get<DefaultsResponse>(Api.Wallet, '/defaults');
660
+ const flat = Object.values(res.defaults || {}).flat();
661
+ const keyed: Record<string, DefaultItem> = {};
662
+ for (const item of flat) keyed[item.key] = item;
663
+
664
+ setDefaultsByKey(keyed);
665
+
666
+ const nextDraftValues: Record<string, string> = {};
667
+ for (const key of EDITABLE_KEYS) {
668
+ if (key === 'permissions.default') continue;
669
+ nextDraftValues[key] = formatInputValue(keyed[key]?.value);
670
+ }
671
+ setDraftValues(nextDraftValues);
672
+
673
+ const permissionsValue = keyed['permissions.default']?.value;
674
+ setPermissionDraft(Array.isArray(permissionsValue) ? permissionsValue.filter((p): p is string => typeof p === 'string') : []);
675
+ } catch (err) {
676
+ setError(err instanceof Error ? err.message : 'Failed to load defaults');
677
+ } finally {
678
+ setLoading(false);
679
+ }
680
+ }, []);
681
+
682
+ useEffect(() => { void loadDefaults(); }, [loadDefaults]);
683
+
684
+ const rows = useMemo(() => EDITABLE_KEYS.map((key) => defaultsByKey[key]).filter(Boolean), [defaultsByKey]);
685
+
686
+ const onSaveNumber = async (key: string) => {
687
+ const raw = draftValues[key];
688
+ const parsed = Number(raw);
689
+ if (Number.isNaN(parsed)) {
690
+ setError(`${key} must be a number`);
691
+ return;
692
+ }
693
+
694
+ setSavingKey(key);
695
+ setError(null);
696
+ setSaveMessage(null);
697
+ try {
698
+ await api.patch(Api.Wallet, `/defaults/${encodeURIComponent(key)}`, { value: parsed });
699
+ setSaveMessage(`Saved ${key}`);
700
+ await loadDefaults();
701
+ } catch (err) {
702
+ setError(err instanceof Error ? err.message : `Failed to save ${key}`);
703
+ } finally {
704
+ setSavingKey(null);
705
+ }
706
+ };
707
+
708
+ const onSavePermissions = async () => {
709
+ const key = 'permissions.default';
710
+ setSavingKey(key);
711
+ setError(null);
712
+ setSaveMessage(null);
713
+ try {
714
+ await api.patch(Api.Wallet, `/defaults/${encodeURIComponent(key)}`, { value: permissionDraft });
715
+ setSaveMessage('Saved permissions.default');
716
+ await loadDefaults();
717
+ } catch (err) {
718
+ setError(err instanceof Error ? err.message : 'Failed to save permissions.default');
719
+ } finally {
720
+ setSavingKey(null);
721
+ }
722
+ };
723
+
724
+ const onReset = async (key: string) => {
725
+ setSavingKey(key);
726
+ setError(null);
727
+ setSaveMessage(null);
728
+ try {
729
+ await api.post(Api.Wallet, '/defaults/reset', { key });
730
+ setSaveMessage(`Reset ${key}`);
731
+ await loadDefaults();
732
+ } catch (err) {
733
+ setError(err instanceof Error ? err.message : `Failed to reset ${key}`);
734
+ } finally {
735
+ setSavingKey(null);
736
+ }
737
+ };
738
+
739
+ if (loading) {
740
+ return (
741
+ <div className="py-8 flex items-center justify-center">
742
+ <Loader2 size={20} className="animate-spin" style={{ color: 'var(--color-text-muted)' }} />
743
+ </div>
744
+ );
745
+ }
746
+
747
+ return (
748
+ <div className="space-y-3 p-2">
749
+ <div>
750
+ <div className="font-mono text-[10px] tracking-widest" style={{ color: 'var(--color-text-muted)' }}>
751
+ SYSTEM DEFAULTS
752
+ </div>
753
+ <div className="font-mono text-[9px]" style={{ color: 'var(--color-text-muted)' }}>
754
+ Edit baseline limits and permissions for new agent actions.
755
+ </div>
756
+ </div>
757
+
758
+ {error && (
759
+ <div className="p-2 font-mono text-[9px]" style={{ border: '1px solid var(--color-warning)', color: 'var(--color-warning)', background: 'var(--color-surface)' }}>
760
+ {error}
761
+ </div>
762
+ )}
763
+ {saveMessage && (
764
+ <div className="p-2 font-mono text-[9px]" style={{ border: '1px solid var(--color-success)', color: 'var(--color-success)', background: 'var(--color-surface)' }}>
765
+ {saveMessage}
766
+ </div>
767
+ )}
768
+
769
+ {/* AI Engine section — above financial limits */}
770
+ <AiEngineSection />
771
+
772
+ {rows.filter((row) => row.key !== 'permissions.default').map((row) => (
773
+ <div key={row.key} className="p-2 space-y-2" style={{ border: '1px solid var(--color-border)', background: 'var(--color-surface)' }}>
774
+ <div>
775
+ <div className="font-mono text-[10px]" style={{ color: 'var(--color-text)' }}>{row.label}</div>
776
+ <div className="font-mono text-[8px]" style={{ color: 'var(--color-text-muted)' }}>{row.description || row.key}</div>
777
+ </div>
778
+ <div className="flex items-end gap-2">
779
+ <TextInput
780
+ label={row.key}
781
+ compact
782
+ value={draftValues[row.key] ?? ''}
783
+ onChange={(e) => setDraftValues((prev) => ({ ...prev, [row.key]: e.target.value }))}
784
+ rightElement={
785
+ SUFFIX_BY_KEY[row.key as keyof typeof SUFFIX_BY_KEY]
786
+ ? <span className="font-mono text-[8px] text-[var(--color-text-muted)]">{SUFFIX_BY_KEY[row.key as keyof typeof SUFFIX_BY_KEY]}</span>
787
+ : undefined
788
+ }
789
+ />
790
+ <Button size="sm" onClick={() => void onSaveNumber(row.key)} loading={savingKey === row.key} icon={<Save size={11} />}>
791
+ SAVE
792
+ </Button>
793
+ <Button size="sm" variant="secondary" onClick={() => void onReset(row.key)} disabled={savingKey === row.key} icon={<RotateCcw size={11} />}>
794
+ RESET
795
+ </Button>
796
+ </div>
797
+ </div>
798
+ ))}
799
+
800
+ {defaultsByKey['permissions.default'] && (
801
+ <div className="p-2 space-y-2" style={{ border: '1px solid var(--color-border)', background: 'var(--color-surface)' }}>
802
+ <div>
803
+ <div className="font-mono text-[10px]" style={{ color: 'var(--color-text)' }}>
804
+ {defaultsByKey['permissions.default'].label}
805
+ </div>
806
+ <div className="font-mono text-[8px]" style={{ color: 'var(--color-text-muted)' }}>
807
+ {defaultsByKey['permissions.default'].description || 'permissions.default'}
808
+ </div>
809
+ </div>
810
+ <div className="grid grid-cols-2 gap-2">
811
+ {KNOWN_PERMISSIONS.map((perm) => {
812
+ const checked = permissionDraft.includes(perm);
813
+ return (
814
+ <label key={perm} className="flex items-center gap-2 cursor-pointer font-mono text-[9px]" style={{ color: 'var(--color-text)' }}>
815
+ <input
816
+ type="checkbox"
817
+ checked={checked}
818
+ onChange={(e) => {
819
+ setPermissionDraft((prev) => {
820
+ if (e.target.checked) return Array.from(new Set([...prev, perm]));
821
+ return prev.filter((p) => p !== perm);
822
+ });
823
+ }}
824
+ />
825
+ {perm}
826
+ </label>
827
+ );
828
+ })}
829
+ </div>
830
+ <div className="flex gap-2">
831
+ <Button size="sm" onClick={() => void onSavePermissions()} loading={savingKey === 'permissions.default'} icon={<Save size={11} />}>
832
+ SAVE
833
+ </Button>
834
+ <Button size="sm" variant="secondary" onClick={() => void onReset('permissions.default')} disabled={savingKey === 'permissions.default'} icon={<RotateCcw size={11} />}>
835
+ RESET
836
+ </Button>
837
+ </div>
838
+ </div>
839
+ )}
840
+
841
+ {/* Adapter Chat toggles */}
842
+ <AdapterChatSection />
843
+ </div>
844
+ );
845
+ }