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.
- package/LICENSE +26 -0
- package/README.md +112 -0
- package/bin/aurawallet.js +121 -0
- package/docs/ADAPTERS.md +467 -0
- package/docs/API.md +2679 -0
- package/docs/APPS.md +198 -0
- package/docs/ARCHITECTURE.md +350 -0
- package/docs/AUTH.md +698 -0
- package/docs/BEST-PRACTICES.md +121 -0
- package/docs/CLI.md +61 -0
- package/docs/DEVELOPING-APPS.md +452 -0
- package/docs/EXTENSION.md +97 -0
- package/docs/JOBS.md +33 -0
- package/docs/MCP.md +76 -0
- package/docs/PROTOCOL.md +142 -0
- package/docs/SETUP.md +219 -0
- package/docs/WORKSPACE.md +672 -0
- package/docs/agent-auth.md +63 -0
- package/docs/aura-file.md +48 -0
- package/docs/credentials.md +53 -0
- package/docs/external/getting-started.md +65 -0
- package/docs/external/overview.md +45 -0
- package/docs/external/use-cases.md +48 -0
- package/docs/external/why-aura.md +35 -0
- package/docs/jobs/connect-agent.md +77 -0
- package/docs/jobs/migrate-from-dotenv.md +79 -0
- package/docs/jobs/recover-from-lockout.md +72 -0
- package/docs/jobs/secure-ci.md +63 -0
- package/docs/oauth2.md +42 -0
- package/docs/passkeys.md +60 -0
- package/docs/security.md +540 -0
- package/docs/specs/aura-open-protocol.md +61 -0
- package/docs/specs/aura-provider-plugin.md +24 -0
- package/docs/specs/aura-registry-model.md +31 -0
- package/docs/specs/fixtures/invalid-bad-key.aura +1 -0
- package/docs/specs/fixtures/invalid-bad-unicode-escape.aura +1 -0
- package/docs/specs/fixtures/invalid-duplicate-key.aura +2 -0
- package/docs/specs/fixtures/valid-basic.aura +4 -0
- package/docs/specs/fixtures/valid-provider-ref.aura +1 -0
- package/docs/specs/fixtures/valid-quoted-escapes.aura +2 -0
- package/docs/templates/RELEASE_NOTES_TEMPLATE.md +22 -0
- package/docs/totp.md +40 -0
- package/docs/wallet/AI.md +508 -0
- package/docs/wallet/DEVELOPING-STRATEGIES.md +713 -0
- package/docs/wallet/README.md +47 -0
- package/docs/wallet/STRATEGY.md +89 -0
- package/next.config.ts +21 -0
- package/package.json +151 -0
- package/postcss.config.mjs +8 -0
- package/prisma/migrations/20260214170000_baseline/migration.sql +511 -0
- package/prisma/migrations/20260216214537_add_passkey_model/migration.sql +18 -0
- package/prisma/migrations/20260217150500_add_credential_access_audit/migration.sql +31 -0
- package/prisma/migrations/migration_lock.toml +3 -0
- package/prisma/schema.prisma +447 -0
- package/public/logo-chevron.svg +31 -0
- package/public/logo-concentric.svg +31 -0
- package/public/logo-crosshatch.svg +39 -0
- package/public/logo-dashed.svg +39 -0
- package/public/logo-horizontal.svg +31 -0
- package/public/logo-m56.svg +64 -0
- package/public/logo.webp +0 -0
- package/scripts/add-app.js +245 -0
- package/scripts/init.sh +57 -0
- package/scripts/migrate-apikeys-to-credentials.ts +35 -0
- package/scripts/sandbox-agent-flow.sh +235 -0
- package/scripts/sandbox.sh +175 -0
- package/scripts/validate-job-docs.mjs +125 -0
- package/server/abi/SwapHelper.json +438 -0
- package/server/cli/approval.ts +447 -0
- package/server/cli/commands/app.ts +204 -0
- package/server/cli/commands/cron.ts +24 -0
- package/server/cli/commands/doctor.ts +1007 -0
- package/server/cli/commands/env.ts +456 -0
- package/server/cli/commands/init.ts +752 -0
- package/server/cli/commands/mcp.ts +125 -0
- package/server/cli/commands/restore.ts +314 -0
- package/server/cli/commands/shell-hook.ts +468 -0
- package/server/cli/commands/start.ts +62 -0
- package/server/cli/commands/status.ts +59 -0
- package/server/cli/commands/stop.ts +14 -0
- package/server/cli/commands/token.ts +180 -0
- package/server/cli/commands/unlock.ts +49 -0
- package/server/cli/commands/vault.ts +417 -0
- package/server/cli/index.ts +328 -0
- package/server/cli/lib/aura-parser.ts +64 -0
- package/server/cli/lib/credential-create.ts +74 -0
- package/server/cli/lib/credential-resolve.ts +254 -0
- package/server/cli/lib/dotenv-migrate.ts +116 -0
- package/server/cli/lib/dotenv-parser.ts +146 -0
- package/server/cli/lib/http.ts +91 -0
- package/server/cli/lib/init-steps.ts +76 -0
- package/server/cli/lib/local-agent-trust.ts +45 -0
- package/server/cli/lib/process.ts +136 -0
- package/server/cli/lib/prompt.ts +85 -0
- package/server/cli/lib/theme.ts +240 -0
- package/server/cli/socket.ts +570 -0
- package/server/cli/transport-client.ts +50 -0
- package/server/cron/index.ts +137 -0
- package/server/cron/job.ts +31 -0
- package/server/cron/jobs/balance-sync.ts +436 -0
- package/server/cron/jobs/incoming-scan.ts +506 -0
- package/server/cron/jobs/native-price.ts +70 -0
- package/server/cron/jobs/orphan-cleanup.ts +40 -0
- package/server/cron/jobs/strategy-runner.ts +175 -0
- package/server/cron/scheduler.ts +125 -0
- package/server/index.ts +406 -0
- package/server/lib/adapters/factory.ts +110 -0
- package/server/lib/adapters/index.ts +19 -0
- package/server/lib/adapters/router.ts +297 -0
- package/server/lib/adapters/telegram.ts +645 -0
- package/server/lib/adapters/types.ts +89 -0
- package/server/lib/adapters/webhook.ts +95 -0
- package/server/lib/address.ts +49 -0
- package/server/lib/agent-auth/contracts.ts +1194 -0
- package/server/lib/agent-profiles.ts +328 -0
- package/server/lib/ai.ts +285 -0
- package/server/lib/api-registry/contracts.ts +86 -0
- package/server/lib/api-registry/validation.ts +172 -0
- package/server/lib/apikey-migration.ts +189 -0
- package/server/lib/app-installer.ts +505 -0
- package/server/lib/app-tokens.ts +247 -0
- package/server/lib/auth.ts +314 -0
- package/server/lib/batch.ts +242 -0
- package/server/lib/cold.ts +874 -0
- package/server/lib/config.ts +381 -0
- package/server/lib/credential-access-audit.ts +85 -0
- package/server/lib/credential-access-policy.ts +110 -0
- package/server/lib/credential-health.ts +343 -0
- package/server/lib/credential-import.ts +487 -0
- package/server/lib/credential-scope.ts +87 -0
- package/server/lib/credential-shares.ts +190 -0
- package/server/lib/credential-transport.ts +342 -0
- package/server/lib/credential-vault.ts +77 -0
- package/server/lib/credentials.ts +333 -0
- package/server/lib/crypto.ts +8 -0
- package/server/lib/db.ts +15 -0
- package/server/lib/defaults.ts +366 -0
- package/server/lib/dex/index.ts +80 -0
- package/server/lib/dex/relay.ts +235 -0
- package/server/lib/dex/types.ts +59 -0
- package/server/lib/dex/uniswap.ts +370 -0
- package/server/lib/e2e-agent/artifacts.ts +36 -0
- package/server/lib/e2e-agent/contracts.ts +112 -0
- package/server/lib/e2e-agent/validation.ts +135 -0
- package/server/lib/encrypt.ts +128 -0
- package/server/lib/error.ts +20 -0
- package/server/lib/events.ts +205 -0
- package/server/lib/hot.ts +357 -0
- package/server/lib/key-fingerprint.ts +28 -0
- package/server/lib/logger.ts +331 -0
- package/server/lib/network.ts +137 -0
- package/server/lib/notifications.ts +219 -0
- package/server/lib/oauth2-refresh.ts +241 -0
- package/server/lib/oursecret.ts +54 -0
- package/server/lib/passkey-credential.ts +360 -0
- package/server/lib/passkey.ts +68 -0
- package/server/lib/permissions.ts +248 -0
- package/server/lib/pino.ts +24 -0
- package/server/lib/policy-preview.ts +138 -0
- package/server/lib/price.ts +338 -0
- package/server/lib/prices.ts +34 -0
- package/server/lib/project-scope.ts +239 -0
- package/server/lib/resolve-action.ts +427 -0
- package/server/lib/resolve.ts +36 -0
- package/server/lib/sessions.ts +632 -0
- package/server/lib/solana/connection.ts +26 -0
- package/server/lib/solana/jupiter.ts +128 -0
- package/server/lib/solana/transfer.ts +108 -0
- package/server/lib/solana/wallet.ts +136 -0
- package/server/lib/strategy/emits.ts +21 -0
- package/server/lib/strategy/engine.ts +1305 -0
- package/server/lib/strategy/executor.ts +115 -0
- package/server/lib/strategy/hook-context.ts +158 -0
- package/server/lib/strategy/hooks.ts +990 -0
- package/server/lib/strategy/index.ts +28 -0
- package/server/lib/strategy/installer.ts +305 -0
- package/server/lib/strategy/loader.ts +256 -0
- package/server/lib/strategy/message.ts +235 -0
- package/server/lib/strategy/repository.ts +218 -0
- package/server/lib/strategy/session-logger.ts +693 -0
- package/server/lib/strategy/sources.ts +288 -0
- package/server/lib/strategy/state.ts +189 -0
- package/server/lib/strategy/templates.ts +403 -0
- package/server/lib/strategy/tick.ts +404 -0
- package/server/lib/strategy/types.ts +230 -0
- package/server/lib/swap.ts +3 -0
- package/server/lib/temp.ts +86 -0
- package/server/lib/token-metadata.ts +86 -0
- package/server/lib/token-safety.ts +200 -0
- package/server/lib/token-search.ts +444 -0
- package/server/lib/totp.ts +194 -0
- package/server/lib/transactions.ts +123 -0
- package/server/lib/transport.ts +75 -0
- package/server/lib/txhistory/decoder.ts +262 -0
- package/server/lib/txhistory/enricher.ts +652 -0
- package/server/lib/txhistory/index.ts +391 -0
- package/server/lib/txhistory/signatures.ts +59 -0
- package/server/lib/verified-summary.ts +421 -0
- package/server/mcp/profile-policy.ts +30 -0
- package/server/mcp/server.ts +619 -0
- package/server/mcp/tools.ts +523 -0
- package/server/middleware/auth.ts +119 -0
- package/server/middleware/requestLogger.ts +84 -0
- package/server/routes/actions.ts +459 -0
- package/server/routes/adapters.ts +703 -0
- package/server/routes/addressbook.ts +113 -0
- package/server/routes/ai.ts +34 -0
- package/server/routes/apikeys.ts +295 -0
- package/server/routes/apps.ts +601 -0
- package/server/routes/auth.ts +457 -0
- package/server/routes/backup.ts +340 -0
- package/server/routes/batch.ts +270 -0
- package/server/routes/bookmarks.ts +162 -0
- package/server/routes/credential-shares.ts +198 -0
- package/server/routes/credential-vaults.ts +154 -0
- package/server/routes/credentials.ts +1290 -0
- package/server/routes/dashboard.ts +71 -0
- package/server/routes/defaults.ts +124 -0
- package/server/routes/fund.ts +229 -0
- package/server/routes/import.ts +352 -0
- package/server/routes/launch.ts +665 -0
- package/server/routes/lock.ts +54 -0
- package/server/routes/logs.ts +68 -0
- package/server/routes/nuke.ts +111 -0
- package/server/routes/passkey-credentials.ts +99 -0
- package/server/routes/passkey.ts +346 -0
- package/server/routes/portfolio.ts +217 -0
- package/server/routes/price.ts +63 -0
- package/server/routes/resolve.ts +31 -0
- package/server/routes/security.ts +45 -0
- package/server/routes/send-evm.ts +241 -0
- package/server/routes/send-solana.ts +281 -0
- package/server/routes/send.ts +178 -0
- package/server/routes/setup.ts +210 -0
- package/server/routes/strategy.ts +894 -0
- package/server/routes/swap-evm.ts +353 -0
- package/server/routes/swap-solana.ts +177 -0
- package/server/routes/swap.ts +356 -0
- package/server/routes/token.ts +247 -0
- package/server/routes/unlock.ts +403 -0
- package/server/routes/wallet-assets.ts +361 -0
- package/server/routes/wallet-transactions.ts +515 -0
- package/server/routes/wallet.ts +710 -0
- package/server/types.ts +146 -0
- package/skills/aurawallet/SKILL.md +739 -0
- package/skills/aurawallet-setup/SKILL.md +74 -0
- package/skills/security-review/SKILL.md +148 -0
- package/src/app/api/agent-requests/route.ts +30 -0
- package/src/app/api/apps/install/route.ts +126 -0
- package/src/app/api/apps/manifests/route.ts +16 -0
- package/src/app/api/apps/static/[...path]/route.ts +57 -0
- package/src/app/api/events/route.ts +92 -0
- package/src/app/api/page.tsx +212 -0
- package/src/app/api/workspace/[id]/apps/[wid]/route.ts +119 -0
- package/src/app/api/workspace/[id]/apps/route.ts +81 -0
- package/src/app/api/workspace/[id]/export/route.ts +67 -0
- package/src/app/api/workspace/[id]/route.ts +168 -0
- package/src/app/api/workspace/auth.ts +34 -0
- package/src/app/api/workspace/config/route.ts +106 -0
- package/src/app/api/workspace/import/route.ts +127 -0
- package/src/app/api/workspace/route.ts +116 -0
- package/src/app/app/page.tsx +2122 -0
- package/src/app/apple-icon.png +0 -0
- package/src/app/docs/page.tsx +178 -0
- package/src/app/favicon.ico +0 -0
- package/src/app/globals.css +572 -0
- package/src/app/health/page.tsx +5 -0
- package/src/app/hello/page.tsx +15 -0
- package/src/app/icon.png +0 -0
- package/src/app/layout.tsx +34 -0
- package/src/app/page.tsx +986 -0
- package/src/app/providers.tsx +90 -0
- package/src/app/share/[token]/page.tsx +295 -0
- package/src/components/ChainSelector.tsx +144 -0
- package/src/components/HumanActionBar.tsx +695 -0
- package/src/components/NotificationDrawer.tsx +129 -0
- package/src/components/apps/AgentKeysApp.tsx +490 -0
- package/src/components/apps/App.tsx +153 -0
- package/src/components/apps/AppGrid.tsx +15 -0
- package/src/components/apps/DetailedAddressDrawer.tsx +325 -0
- package/src/components/apps/DraggableApp.tsx +562 -0
- package/src/components/apps/IFrameApp.tsx +73 -0
- package/src/components/apps/LogsApp.tsx +360 -0
- package/src/components/apps/SendApp.tsx +394 -0
- package/src/components/apps/SetupWizardApp.tsx +1004 -0
- package/src/components/apps/SystemDefaultsApp.tsx +845 -0
- package/src/components/apps/ThirdPartyApp.tsx +428 -0
- package/src/components/apps/TokenApp.tsx +319 -0
- package/src/components/apps/TransactionsApp.tsx +438 -0
- package/src/components/apps/WalletDetailApp.tsx +1505 -0
- package/src/components/apps/index.ts +13 -0
- package/src/components/design-system/Button.tsx +53 -0
- package/src/components/design-system/ChainIndicator.tsx +65 -0
- package/src/components/design-system/ChainSelector.tsx +137 -0
- package/src/components/design-system/ConfirmationModal.tsx +106 -0
- package/src/components/design-system/ConfirmationPopover.tsx +81 -0
- package/src/components/design-system/Drawer.tsx +123 -0
- package/src/components/design-system/FilterDropdown.tsx +72 -0
- package/src/components/design-system/Modal.tsx +206 -0
- package/src/components/design-system/Popover.tsx +142 -0
- package/src/components/design-system/TextInput.tsx +85 -0
- package/src/components/design-system/Toggle.tsx +58 -0
- package/src/components/design-system/index.ts +11 -0
- package/src/components/docs/DocsThemeToggle.tsx +49 -0
- package/src/components/health/CredentialHealthDashboard.tsx +214 -0
- package/src/components/icons/ChainIcons.tsx +72 -0
- package/src/components/layout/AppStoreDrawer.tsx +369 -0
- package/src/components/layout/ContentArea.tsx +21 -0
- package/src/components/layout/TabBar.tsx +278 -0
- package/src/components/layout/WalletSidebar.tsx +1033 -0
- package/src/components/layout/index.ts +4 -0
- package/src/components/marketing/AuraWalletSpecOverlay.tsx +635 -0
- package/src/components/marketing/DeviceMorphExperience.tsx +216 -0
- package/src/components/vault/ApiKeysConsole.tsx +1080 -0
- package/src/components/vault/AuditConsole.tsx +584 -0
- package/src/components/vault/CredentialDetail.tsx +455 -0
- package/src/components/vault/CredentialEmpty.tsx +55 -0
- package/src/components/vault/CredentialField.tsx +361 -0
- package/src/components/vault/CredentialForm.tsx +1212 -0
- package/src/components/vault/CredentialList.tsx +165 -0
- package/src/components/vault/CredentialRow.tsx +97 -0
- package/src/components/vault/CredentialShareModal.tsx +178 -0
- package/src/components/vault/CredentialVault.tsx +754 -0
- package/src/components/vault/CredentialWalletWidget.tsx +103 -0
- package/src/components/vault/ImportCredentialsModal.tsx +515 -0
- package/src/components/vault/LargeTypeModal.tsx +64 -0
- package/src/components/vault/PasswordGenerator.tsx +224 -0
- package/src/components/vault/TOTPDisplay.tsx +123 -0
- package/src/components/vault/VaultSidebar.tsx +413 -0
- package/src/components/vault/types.ts +54 -0
- package/src/context/AuthContext.tsx +337 -0
- package/src/context/PriceContext.tsx +113 -0
- package/src/context/ThemeContext.tsx +164 -0
- package/src/context/WebSocketContext.tsx +269 -0
- package/src/context/WorkspaceContext.tsx +668 -0
- package/src/hooks/index.ts +3 -0
- package/src/hooks/useAgentActions.ts +368 -0
- package/src/hooks/useBalance.ts +103 -0
- package/src/hooks/useBalances.ts +129 -0
- package/src/instrumentation.ts +12 -0
- package/src/lib/api.ts +449 -0
- package/src/lib/app-loader.ts +148 -0
- package/src/lib/app-registry.ts +178 -0
- package/src/lib/app-sdk.ts +157 -0
- package/src/lib/audit-console-adapter.ts +151 -0
- package/src/lib/auth-client.ts +75 -0
- package/src/lib/config.ts +74 -0
- package/src/lib/crypto.ts +112 -0
- package/src/lib/db.ts +21 -0
- package/src/lib/docs.ts +390 -0
- package/src/lib/events.ts +361 -0
- package/src/lib/pino.ts +24 -0
- package/src/lib/theme-handlers.ts +168 -0
- package/src/lib/theme.ts +351 -0
- package/src/lib/tokenData.ts +378 -0
- package/src/lib/vault-crypto.ts +129 -0
- package/src/lib/websocket-server.ts +302 -0
- package/src/lib/websocket-setup.ts +79 -0
- package/src/lib/wordlist.ts +2050 -0
- package/src/lib/workspace-handlers.ts +285 -0
- package/start.sh +80 -0
- package/tailwind.config.ts +99 -0
- 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
|
+
}
|