@flame0510/project-aether 1.1.9
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/README.md +150 -0
- package/agent-templates/README.md +37 -0
- package/agent-templates/argus/AGENTS.md +52 -0
- package/agent-templates/argus/IDENTITY.md +7 -0
- package/agent-templates/argus/MEMORY.md +9 -0
- package/agent-templates/argus/SOUL.md +24 -0
- package/agent-templates/argus/TOOLS.md +8 -0
- package/agent-templates/atlas/AGENTS.md +47 -0
- package/agent-templates/atlas/HEARTBEAT.md +10 -0
- package/agent-templates/atlas/IDENTITY.md +6 -0
- package/agent-templates/atlas/MEMORY.md +9 -0
- package/agent-templates/atlas/SOUL.md +21 -0
- package/agent-templates/atlas/TOOLS.md +4 -0
- package/agent-templates/base-image/.trigger +0 -0
- package/agent-templates/base-image/Dockerfile +90 -0
- package/agent-templates/base-image/entrypoint.sh +37 -0
- package/agent-templates/custom/AGENTS.md +29 -0
- package/agent-templates/custom/BOOTSTRAP.md +56 -0
- package/agent-templates/custom/MEMORY.md +8 -0
- package/agent-templates/prometheus/AGENTS.md +37 -0
- package/agent-templates/prometheus/IDENTITY.md +6 -0
- package/agent-templates/prometheus/MEMORY.md +9 -0
- package/agent-templates/prometheus/SOUL.md +21 -0
- package/agent-templates/prometheus/TOOLS.md +4 -0
- package/app/agents/ChannelManager.tsx +465 -0
- package/app/agents/ImageDownloadBanner.tsx +159 -0
- package/app/agents/PageClient.tsx +1285 -0
- package/app/agents/create/PageClient.tsx +437 -0
- package/app/agents/create/page.tsx +126 -0
- package/app/agents/loading.tsx +14 -0
- package/app/agents/page.tsx +5 -0
- package/app/api/agents/[id]/backup/route.ts +169 -0
- package/app/api/agents/[id]/channels/pairing/route.ts +69 -0
- package/app/api/agents/[id]/channels/route.ts +14 -0
- package/app/api/agents/[id]/channels/telegram/route.ts +50 -0
- package/app/api/agents/[id]/lifecycle/route.ts +94 -0
- package/app/api/agents/[id]/recreate/route.ts +199 -0
- package/app/api/agents/[id]/restart/route.ts +33 -0
- package/app/api/agents/[id]/restore/route.ts +81 -0
- package/app/api/agents/[id]/route.ts +280 -0
- package/app/api/agents/channels-summary/route.ts +39 -0
- package/app/api/agents/config-json/route.ts +58 -0
- package/app/api/agents/create/route.ts +488 -0
- package/app/api/agents/download-image/route.ts +32 -0
- package/app/api/agents/image-status/route.ts +126 -0
- package/app/api/agents/route.ts +213 -0
- package/app/api/agents/token/route.ts +94 -0
- package/app/api/agents-active/route.ts +190 -0
- package/app/api/agents-config/route.ts +629 -0
- package/app/api/alerts/smoke/route.ts +26 -0
- package/app/api/aliases/add/route.ts +28 -0
- package/app/api/aliases/remove/route.ts +27 -0
- package/app/api/assistant/route.ts +204 -0
- package/app/api/auth/check/route.ts +38 -0
- package/app/api/auth/login/route.ts +85 -0
- package/app/api/auth/logout/route.ts +7 -0
- package/app/api/auth/status/route.ts +33 -0
- package/app/api/config/env/route.ts +132 -0
- package/app/api/config/restart/route.ts +26 -0
- package/app/api/containers/route.ts +75 -0
- package/app/api/cost-override/route.ts +46 -0
- package/app/api/costs/route.ts +84 -0
- package/app/api/credentials/[id]/reveal/route.ts +21 -0
- package/app/api/credentials/[id]/route.ts +52 -0
- package/app/api/credentials/[id]/sync/route.ts +58 -0
- package/app/api/credentials/detect/route.ts +246 -0
- package/app/api/credentials/route.ts +33 -0
- package/app/api/crons/[id]/route.ts +69 -0
- package/app/api/crons/route.ts +12 -0
- package/app/api/crons/runs/route.ts +31 -0
- package/app/api/debug/route.ts +11 -0
- package/app/api/envcheck/route.ts +18 -0
- package/app/api/events/route.ts +24 -0
- package/app/api/gateway/agent/route.ts +154 -0
- package/app/api/gateway/provider/balance/route.ts +38 -0
- package/app/api/gateway/provider/keys.ts +38 -0
- package/app/api/gateway/provider/oauth/route.ts +304 -0
- package/app/api/gateway/provider/route.ts +209 -0
- package/app/api/gateway/route.ts +223 -0
- package/app/api/gateway/sync.ts +189 -0
- package/app/api/lineage/route.ts +54 -0
- package/app/api/memory-context/route.ts +15 -0
- package/app/api/metrics/route.ts +35 -0
- package/app/api/models/route.ts +77 -0
- package/app/api/plugins/route.js +37 -0
- package/app/api/provider/upstream.ts +142 -0
- package/app/api/provider/v1/chat/completions/route.ts +146 -0
- package/app/api/provider/v1/models/route.ts +167 -0
- package/app/api/provider-usage/route.ts +629 -0
- package/app/api/session/route.ts +39 -0
- package/app/api/sessions/[id]/route.ts +38 -0
- package/app/api/sessions/route.ts +23 -0
- package/app/api/setup/agent-image/route.ts +102 -0
- package/app/api/setup/password/route.ts +111 -0
- package/app/api/setup/restart/route.ts +76 -0
- package/app/api/skills/delete/route.js +105 -0
- package/app/api/skills/promote/route.js +129 -0
- package/app/api/skills/route.js +203 -0
- package/app/api/skills/save/route.js +142 -0
- package/app/api/stats/route.ts +34 -0
- package/app/api/stats-since/route.ts +20 -0
- package/app/api/stream/route.ts +70 -0
- package/app/api/system-health/route.ts +233 -0
- package/app/api/tool-calls/route.ts +24 -0
- package/app/api/tools-config/route.ts +163 -0
- package/app/api/update-check/route.ts +127 -0
- package/app/api/version/route.ts +25 -0
- package/app/api/wizard/complete/route.ts +32 -0
- package/app/api/wizard/reset/route.ts +28 -0
- package/app/api/wizard/status/route.ts +28 -0
- package/app/api/workspace/route.ts +392 -0
- package/app/api/workspace/stream/route.ts +136 -0
- package/app/components/AppShell.tsx +33 -0
- package/app/components/AuthGuard.tsx +91 -0
- package/app/components/CostBreakdown.tsx +37 -0
- package/app/components/DashboardHeader.tsx +73 -0
- package/app/components/DashboardLayout.tsx +76 -0
- package/app/components/DashboardToolbar.tsx +61 -0
- package/app/components/FloatingActions.tsx +33 -0
- package/app/components/Icons.tsx +78 -0
- package/app/components/LineageGraphPage.tsx +142 -0
- package/app/components/LiveFeed.tsx +48 -0
- package/app/components/MobileBottomNav.tsx +84 -0
- package/app/components/ModelPickerModal.tsx +94 -0
- package/app/components/PasswordInput.tsx +86 -0
- package/app/components/PulseChat.tsx +362 -0
- package/app/components/Rev4aLoader.tsx +48 -0
- package/app/components/SessionDrawer.tsx +263 -0
- package/app/components/SessionTopology.tsx +289 -0
- package/app/components/Sidebar.tsx +294 -0
- package/app/components/Skeleton.tsx +133 -0
- package/app/components/SystemCockpit.tsx +169 -0
- package/app/components/VersionBanner.tsx +105 -0
- package/app/components/WizardButton.tsx +30 -0
- package/app/components/ui/Badge.tsx +30 -0
- package/app/components/ui/Button.tsx +59 -0
- package/app/components/ui/ConfirmModal.tsx +46 -0
- package/app/components/ui/FilterBar.tsx +33 -0
- package/app/components/ui/Input.tsx +25 -0
- package/app/components/ui/ItemList.tsx +83 -0
- package/app/components/ui/LoadingSpinner.tsx +30 -0
- package/app/components/ui/Metric.tsx +22 -0
- package/app/components/ui/Modal.tsx +42 -0
- package/app/components/ui/Page.tsx +18 -0
- package/app/components/ui/Pill.tsx +6 -0
- package/app/components/ui/PropertyList.tsx +24 -0
- package/app/components/ui/Select.tsx +26 -0
- package/app/components/ui/StatusCard.tsx +32 -0
- package/app/components/ui/Surface.tsx +14 -0
- package/app/components/ui/Tabs.tsx +55 -0
- package/app/components/ui/TemplateOption.tsx +59 -0
- package/app/components/ui/Toast.tsx +44 -0
- package/app/components/ui/index.ts +19 -0
- package/app/components/ui/tokens.ts +18 -0
- package/app/config/PageClient.tsx +293 -0
- package/app/config/loading.tsx +10 -0
- package/app/config/page.tsx +7 -0
- package/app/containers/ContainersClient.tsx +129 -0
- package/app/containers/loading.tsx +21 -0
- package/app/containers/page.tsx +5 -0
- package/app/containers/terminal/[id]/TerminalClient.tsx +313 -0
- package/app/containers/terminal/[id]/page.tsx +6 -0
- package/app/credentials/PageClient.tsx +598 -0
- package/app/credentials/loading.tsx +10 -0
- package/app/credentials/page.tsx +7 -0
- package/app/crons/PageClient.tsx +538 -0
- package/app/crons/loading.tsx +10 -0
- package/app/crons/page.tsx +5 -0
- package/app/design-system.ts +57 -0
- package/app/gateway/PageClient.tsx +1048 -0
- package/app/gateway/loading.tsx +24 -0
- package/app/gateway/page.tsx +5 -0
- package/app/globals.css +1474 -0
- package/app/layout.tsx +45 -0
- package/app/lib/models-context.tsx +41 -0
- package/app/lineage/loading.tsx +10 -0
- package/app/lineage/page.tsx +13 -0
- package/app/loading.tsx +16 -0
- package/app/login/page.tsx +160 -0
- package/app/memory/MemoryContextPageClient.tsx +196 -0
- package/app/memory/loading.tsx +10 -0
- package/app/memory/page.tsx +8 -0
- package/app/page.tsx +31 -0
- package/app/plugins/PageClient.tsx +296 -0
- package/app/plugins/loading.tsx +10 -0
- package/app/plugins/page.tsx +5 -0
- package/app/setup/PageClient.tsx +227 -0
- package/app/setup/loading.tsx +21 -0
- package/app/setup/page.tsx +7 -0
- package/app/skills/PageClient.tsx +499 -0
- package/app/skills/loading.tsx +10 -0
- package/app/skills/page.tsx +5 -0
- package/app/tools/ToolsPageClient.tsx +308 -0
- package/app/tools/loading.tsx +10 -0
- package/app/tools/page.tsx +62 -0
- package/app/tools/tools-catalog.ts +141 -0
- package/app/wizard/PageClient.tsx +485 -0
- package/app/wizard/icons.tsx +89 -0
- package/app/wizard/loading.tsx +24 -0
- package/app/wizard/page.tsx +7 -0
- package/app/wizard/storage.ts +33 -0
- package/app/wizard/useWizard.ts +101 -0
- package/app/workspace/WorkspaceClient.tsx +633 -0
- package/app/workspace/loading.tsx +36 -0
- package/app/workspace/page.tsx +8 -0
- package/bin/docker-entrypoint.sh +21 -0
- package/bin/postinstall.js +20 -0
- package/bin/rev4a.js +696 -0
- package/daemon.js +783 -0
- package/docs/ARCHITECTURE.md +557 -0
- package/docs/CONTAINER-TERMINAL.md +280 -0
- package/docs/DESIGN-SYSTEM.md +99 -0
- package/docs/FRONTEND-ARCHITECTURE.md +119 -0
- package/docs/REV4A.md +504 -0
- package/docs/dev/API-REFERENCE.md +1835 -0
- package/docs/dev/DATABASE.md +224 -0
- package/docs/dev/GATEWAY.md +300 -0
- package/docs/dev/PROVIDERS.md +229 -0
- package/docs/dev/SESSION-MAINTENANCE-PLAN.md +607 -0
- package/docs/dev/WORKSPACE.md +199 -0
- package/docs/rag/DATA-FRESHNESS.md +109 -0
- package/docs/rag/GLOSSARY.md +125 -0
- package/docs/rag/REV4A-OVERVIEW.md +135 -0
- package/docs/rag/WHAT-I-CAN-ANSWER.md +124 -0
- package/lib/agent-setup.ts +130 -0
- package/lib/alerts.ts +115 -0
- package/lib/apiFetch.ts +7 -0
- package/lib/auth.ts +28 -0
- package/lib/billing.ts +100 -0
- package/lib/buildAgentImage.ts +346 -0
- package/lib/channelManager.ts +386 -0
- package/lib/container.ts +60 -0
- package/lib/credentials/db.ts +86 -0
- package/lib/credentials/delivery.ts +448 -0
- package/lib/credentials/detect.ts +149 -0
- package/lib/credentials/hash.ts +9 -0
- package/lib/credentials/providers.ts +110 -0
- package/lib/credentials/vault.ts +195 -0
- package/lib/db-bootstrap.d.ts +3 -0
- package/lib/db-bootstrap.mjs +144 -0
- package/lib/db.ts +25 -0
- package/lib/docker-utils.ts +60 -0
- package/lib/hooks/useDashboard.ts +264 -0
- package/lib/hooks/useRev4aTimezone.ts +28 -0
- package/lib/hooks/useScreenshot.ts +30 -0
- package/lib/memory-context.ts +385 -0
- package/lib/model-catalogue.ts +131 -0
- package/lib/model-pricing.ts +63 -0
- package/lib/openclaw-cron.ts +350 -0
- package/lib/patterns/ApiAdapter.ts +73 -0
- package/lib/patterns/ApiClient.ts +65 -0
- package/lib/patterns/EventBus.ts +113 -0
- package/lib/patterns/FilterStrategy.ts +79 -0
- package/lib/patterns/SessionFactory.ts +117 -0
- package/lib/patterns/sessionPresentation.ts +117 -0
- package/lib/provider-balance.ts +163 -0
- package/lib/requireAuth.tsx +38 -0
- package/lib/rev4a-auth.d.ts +3 -0
- package/lib/rev4a-auth.js +36 -0
- package/lib/rev4a-paths.ts +145 -0
- package/lib/timezone.ts +46 -0
- package/lib/types/index.ts +96 -0
- package/lib/utils/format.ts +82 -0
- package/lineage.js +56 -0
- package/model-pricing.json +482 -0
- package/models.config.json +844 -0
- package/next-env.d.ts +6 -0
- package/next.config.mjs +7 -0
- package/package.json +82 -0
- package/public/apple-touch-icon.png +0 -0
- package/public/favicon-64.png +0 -0
- package/public/favicon.svg +58 -0
- package/public/icon-192-maskable.png +0 -0
- package/public/icon-192.png +0 -0
- package/public/icon-512-maskable.png +0 -0
- package/public/icon-512.png +0 -0
- package/public/manifest.json +38 -0
- package/public/rev4a-logo.png +0 -0
- package/public/rev4a-logo.svg +58 -0
- package/rev4a-rules/AGENTS.md +37 -0
- package/scripts/add-auth.cjs +185 -0
- package/scripts/backup.sh +66 -0
- package/scripts/check-providers.mjs +143 -0
- package/scripts/docker/traefik/docker-compose.yml +32 -0
- package/scripts/docker/traefik/dynamic/.gitkeep +18 -0
- package/scripts/docker/traefik/traefik.yml +37 -0
- package/scripts/remove-auth-profile.js +56 -0
- package/scripts/restore.sh +82 -0
- package/terminal-ws-server.js +139 -0
- package/tsconfig.json +42 -0
|
@@ -0,0 +1,437 @@
|
|
|
1
|
+
'use client';
|
|
2
|
+
|
|
3
|
+
import { useState, useMemo, useEffect } from 'react';
|
|
4
|
+
import { useRouter } from 'next/navigation';
|
|
5
|
+
import { Button, Input, TemplateOption } from '../../components/ui';
|
|
6
|
+
|
|
7
|
+
interface TemplateInfo {
|
|
8
|
+
id: string;
|
|
9
|
+
files: string[];
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
interface ModelInfo {
|
|
13
|
+
id: string;
|
|
14
|
+
name: string;
|
|
15
|
+
provider: string;
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
interface Props {
|
|
19
|
+
templates: TemplateInfo[];
|
|
20
|
+
models: ModelInfo[];
|
|
21
|
+
usedNames: string[];
|
|
22
|
+
usedPorts: number[];
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
type Step = 'template' | 'config' | 'creating' | 'done' | 'error';
|
|
26
|
+
|
|
27
|
+
const REV4A_PORT = 3740;
|
|
28
|
+
|
|
29
|
+
function findFirstFreeBlock(usedPorts: number[]): number | null {
|
|
30
|
+
const used = new Set(usedPorts);
|
|
31
|
+
for (let base = 3700; base + 9 <= 3799; base += 10) {
|
|
32
|
+
if (base <= REV4A_PORT && REV4A_PORT <= base + 9) continue;
|
|
33
|
+
const block = Array.from({ length: 10 }, (_, i) => base + i);
|
|
34
|
+
if (block.every((p) => !used.has(p))) return base;
|
|
35
|
+
}
|
|
36
|
+
// Fallback beyond range
|
|
37
|
+
for (let base = 3800; ; base += 10) {
|
|
38
|
+
const block = Array.from({ length: 10 }, (_, i) => base + i);
|
|
39
|
+
if (block.every((p) => !used.has(p))) return base;
|
|
40
|
+
}
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
// ─── Toast ───
|
|
44
|
+
function Toast({ message, type, onClose }: { message: string; type: 'success' | 'error'; onClose: () => void }) {
|
|
45
|
+
useEffect(() => {
|
|
46
|
+
const timer = setTimeout(onClose, 4000);
|
|
47
|
+
return () => clearTimeout(timer);
|
|
48
|
+
}, [onClose]);
|
|
49
|
+
|
|
50
|
+
return (
|
|
51
|
+
<div style={{
|
|
52
|
+
position: 'fixed', bottom: 24, right: 24, zIndex: 9999,
|
|
53
|
+
padding: '12px 20px', borderRadius: 0, fontSize: 13,
|
|
54
|
+
background: type === 'success' ? '#1a3a1a' : '#3a1a1a',
|
|
55
|
+
border: `1px solid ${type === 'success' ? '#22c55e' : '#ef4444'}`,
|
|
56
|
+
color: type === 'success' ? '#bbf7d0' : '#fecaca',
|
|
57
|
+
boxShadow: '0 4px 12px rgba(0,0,0,0.4)',
|
|
58
|
+
animation: 'fadeIn 0.3s ease',
|
|
59
|
+
maxWidth: 400,
|
|
60
|
+
}}>
|
|
61
|
+
<style>{`@keyframes fadeIn { from { opacity: 0; transform: translateY(8px); } to { opacity: 1; transform: translateY(0); } }`}</style>
|
|
62
|
+
{type === 'success' ? '✓ ' : '✗ '}{message}
|
|
63
|
+
</div>
|
|
64
|
+
);
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
export default function CreatePageClient({ templates, models, usedNames, usedPorts: initialUsedPorts }: Props) {
|
|
68
|
+
const router = useRouter();
|
|
69
|
+
|
|
70
|
+
const [step, setStep] = useState<Step>('template');
|
|
71
|
+
const [selectedTemplate, setSelectedTemplate] = useState<string>('');
|
|
72
|
+
const [agentName, setAgentName] = useState('');
|
|
73
|
+
const [portRange, setPortRange] = useState<string>('');
|
|
74
|
+
const [selectedModel, setSelectedModel] = useState<string>(
|
|
75
|
+
models.length > 0 ? models[0].id : ''
|
|
76
|
+
);
|
|
77
|
+
const [fallbacks] = useState<string[]>([]);
|
|
78
|
+
const [submitting, setSubmitting] = useState(false);
|
|
79
|
+
const [errorMsg, setErrorMsg] = useState('');
|
|
80
|
+
const [result, setResult] = useState<{ name: string; containerId: string; traefikUrl: string } | null>(null);
|
|
81
|
+
const [toast, setToast] = useState<{ message: string; type: 'success' | 'error' } | null>(null);
|
|
82
|
+
|
|
83
|
+
const handleSelectTemplate = (id: string) => {
|
|
84
|
+
setSelectedTemplate(id);
|
|
85
|
+
setAgentName('');
|
|
86
|
+
// Auto-assign port block: find first free 10-port block
|
|
87
|
+
if (initialUsedPorts.length === 0) {
|
|
88
|
+
setPortRange('3700-3709');
|
|
89
|
+
} else {
|
|
90
|
+
// Simple client-side block finder
|
|
91
|
+
let block = findFirstFreeBlock(initialUsedPorts);
|
|
92
|
+
setPortRange(block ? `${block}-${block + 9}` : '');
|
|
93
|
+
}
|
|
94
|
+
};
|
|
95
|
+
|
|
96
|
+
const nameError = useMemo(() => {
|
|
97
|
+
if (!agentName) return '';
|
|
98
|
+
if (agentName.length > 128) return 'Name too long (max 128 chars)';
|
|
99
|
+
if (usedNames.includes(agentName)) return 'Name is already in use by another agent';
|
|
100
|
+
return '';
|
|
101
|
+
}, [agentName, usedNames]);
|
|
102
|
+
|
|
103
|
+
const portError = useMemo(() => {
|
|
104
|
+
const v = portRange.trim();
|
|
105
|
+
if (!v) return '';
|
|
106
|
+
if (v.includes('-')) {
|
|
107
|
+
const m = v.match(/^(\d{1,5})-(\d{1,5})$/);
|
|
108
|
+
if (!m) return 'Use format: 3700-3709';
|
|
109
|
+
const start = parseInt(m[1], 10);
|
|
110
|
+
const end = parseInt(m[2], 10);
|
|
111
|
+
if (start < 1 || end > 65535) return 'Ports 1-65535';
|
|
112
|
+
if (end <= start) return 'End port must be greater than start';
|
|
113
|
+
for (let p = start; p <= end; p++) {
|
|
114
|
+
if (initialUsedPorts.includes(p)) return `Port ${p} is already in use`;
|
|
115
|
+
if (p === 3740) return 'Port 3740 is reserved';
|
|
116
|
+
}
|
|
117
|
+
return '';
|
|
118
|
+
}
|
|
119
|
+
const p = parseInt(v, 10);
|
|
120
|
+
if (isNaN(p) || p < 1 || p > 65535) return 'Port must be between 1 and 65535';
|
|
121
|
+
if (p === 3740) return 'Port 3740 is reserved';
|
|
122
|
+
if (initialUsedPorts.includes(p)) return 'Port is already in use';
|
|
123
|
+
return '';
|
|
124
|
+
}, [portRange, initialUsedPorts]);
|
|
125
|
+
|
|
126
|
+
const canProceed = (): boolean => {
|
|
127
|
+
if (step === 'template') return selectedTemplate !== '';
|
|
128
|
+
if (step === 'config') return agentName.length > 0 && !nameError && !portError;
|
|
129
|
+
return false;
|
|
130
|
+
};
|
|
131
|
+
|
|
132
|
+
const handleSubmit = async () => {
|
|
133
|
+
setSubmitting(true);
|
|
134
|
+
setStep('creating');
|
|
135
|
+
setErrorMsg('');
|
|
136
|
+
|
|
137
|
+
try {
|
|
138
|
+
const body: Record<string, unknown> = {
|
|
139
|
+
name: agentName,
|
|
140
|
+
template: selectedTemplate,
|
|
141
|
+
};
|
|
142
|
+
if (portRange.trim()) body.portRange = portRange.trim();
|
|
143
|
+
if (selectedModel) body.model = selectedModel;
|
|
144
|
+
if (fallbacks.length > 0) body.fallbacks = fallbacks;
|
|
145
|
+
|
|
146
|
+
const res = await fetch('/api/agents/create', {
|
|
147
|
+
method: 'POST',
|
|
148
|
+
headers: { 'Content-Type': 'application/json' },
|
|
149
|
+
body: JSON.stringify(body),
|
|
150
|
+
});
|
|
151
|
+
|
|
152
|
+
const data = await res.json();
|
|
153
|
+
|
|
154
|
+
if (!res.ok || !data.success) {
|
|
155
|
+
setErrorMsg(data.error || 'Unknown error');
|
|
156
|
+
setStep('error');
|
|
157
|
+
setSubmitting(false);
|
|
158
|
+
return;
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
setSubmitting(false);
|
|
162
|
+
setResult(data);
|
|
163
|
+
setStep('done');
|
|
164
|
+
setToast({ message: `Agent "${agentName}" deployed successfully`, type: 'success' });
|
|
165
|
+
} catch (e: unknown) {
|
|
166
|
+
setErrorMsg((e as Error).message || 'Network error');
|
|
167
|
+
setStep('error');
|
|
168
|
+
}
|
|
169
|
+
};
|
|
170
|
+
|
|
171
|
+
// Template description helper
|
|
172
|
+
const templateDescription = (id: string): string => {
|
|
173
|
+
const descs: Record<string, string> = {
|
|
174
|
+
atlas: 'General-purpose agent. Trello discipline, code review, git workflow.',
|
|
175
|
+
argus: 'Security & audit agent. Strict protocol, delegation, lineage tracking.',
|
|
176
|
+
prometheus: 'Client-facing agent. Project delivery, repo discipline, documentation.',
|
|
177
|
+
custom: 'Blank slate. No preset identity — the agent will introduce itself and you define everything together.',
|
|
178
|
+
};
|
|
179
|
+
return descs[id] || 'Custom agent template';
|
|
180
|
+
};
|
|
181
|
+
|
|
182
|
+
return (
|
|
183
|
+
<div style={{ minHeight: '100dvh', overflowY: 'auto', WebkitOverflowScrolling: 'touch', paddingBottom: 72, boxSizing: 'border-box' }}>
|
|
184
|
+
<div style={{ maxWidth: 760, margin: '0 auto', padding: '24px 16px' }}>
|
|
185
|
+
{toast && <Toast message={toast.message} type={toast.type} onClose={() => setToast(null)} />}
|
|
186
|
+
|
|
187
|
+
<h1 style={{ fontSize: 20, fontWeight: 500, marginBottom: 24, color: 'var(--violet)' }}>
|
|
188
|
+
Create Agent
|
|
189
|
+
</h1>
|
|
190
|
+
|
|
191
|
+
{/* ─── Step: Template ─── */}
|
|
192
|
+
{step === 'template' && (
|
|
193
|
+
<div>
|
|
194
|
+
<div style={{ fontSize: 12, color: '#888', marginBottom: 16, letterSpacing: '0.05em' }}>
|
|
195
|
+
STEP 1 OF 2 · SELECT TEMPLATE
|
|
196
|
+
</div>
|
|
197
|
+
|
|
198
|
+
{/* ── Custom: blank slate ── */}
|
|
199
|
+
<div style={{ marginBottom: 24 }}>
|
|
200
|
+
<div style={{
|
|
201
|
+
fontSize: 10, color: 'var(--violet)', textTransform: 'uppercase',
|
|
202
|
+
letterSpacing: '0.08em', marginBottom: 8,
|
|
203
|
+
}}>
|
|
204
|
+
Start from scratch
|
|
205
|
+
</div>
|
|
206
|
+
<button
|
|
207
|
+
type="button"
|
|
208
|
+
onClick={() => handleSelectTemplate('custom')}
|
|
209
|
+
style={{
|
|
210
|
+
display: 'flex',
|
|
211
|
+
alignItems: 'flex-start',
|
|
212
|
+
gap: 12,
|
|
213
|
+
width: '100%',
|
|
214
|
+
padding: '16px',
|
|
215
|
+
textAlign: 'left',
|
|
216
|
+
fontSize: 14,
|
|
217
|
+
fontFamily: 'var(--font-mono-stack)',
|
|
218
|
+
color: 'var(--text)',
|
|
219
|
+
cursor: 'pointer',
|
|
220
|
+
outline: 'none',
|
|
221
|
+
transition: 'background 0.1s, border-color 0.1s',
|
|
222
|
+
background: selectedTemplate === 'custom' ? 'var(--violet-bg)' : 'transparent',
|
|
223
|
+
border: `1px solid ${selectedTemplate === 'custom' ? 'var(--violet)' : 'var(--border)'}`,
|
|
224
|
+
}}
|
|
225
|
+
>
|
|
226
|
+
<span style={{
|
|
227
|
+
width: 32,
|
|
228
|
+
height: 32,
|
|
229
|
+
display: 'flex',
|
|
230
|
+
alignItems: 'center',
|
|
231
|
+
justifyContent: 'center',
|
|
232
|
+
background: selectedTemplate === 'custom' ? 'var(--violet)' : 'var(--border)',
|
|
233
|
+
color: selectedTemplate === 'custom' ? '#111' : '#666',
|
|
234
|
+
fontSize: 14,
|
|
235
|
+
fontWeight: 600,
|
|
236
|
+
flexShrink: 0,
|
|
237
|
+
}}>
|
|
238
|
+
+
|
|
239
|
+
</span>
|
|
240
|
+
<div style={{ flex: 1, minWidth: 0 }}>
|
|
241
|
+
<div style={{ fontWeight: 500, fontSize: 14 }}>Custom Agent</div>
|
|
242
|
+
<div style={{ fontSize: 11, color: '#888', marginTop: 2 }}>
|
|
243
|
+
Blank slate. No preset identity — the agent will introduce itself and you define everything together.
|
|
244
|
+
</div>
|
|
245
|
+
</div>
|
|
246
|
+
</button>
|
|
247
|
+
</div>
|
|
248
|
+
|
|
249
|
+
{/* ── Pre-built templates ── */}
|
|
250
|
+
<div style={{
|
|
251
|
+
fontSize: 10, color: '#888', textTransform: 'uppercase',
|
|
252
|
+
letterSpacing: '0.08em', marginBottom: 8,
|
|
253
|
+
}}>
|
|
254
|
+
Pre-built templates
|
|
255
|
+
</div>
|
|
256
|
+
<div style={{ display: 'grid', gap: 8 }}>
|
|
257
|
+
{templates.filter(t => t.id !== 'custom').map((t) => (
|
|
258
|
+
<TemplateOption
|
|
259
|
+
key={t.id}
|
|
260
|
+
id={t.id}
|
|
261
|
+
selected={selectedTemplate === t.id}
|
|
262
|
+
description={templateDescription(t.id)}
|
|
263
|
+
onClick={() => handleSelectTemplate(t.id)}
|
|
264
|
+
badge={undefined}
|
|
265
|
+
/>
|
|
266
|
+
))}
|
|
267
|
+
</div>
|
|
268
|
+
|
|
269
|
+
<div style={{ marginTop: 24, display: 'flex', justifyContent: 'space-between' }}>
|
|
270
|
+
<Button variant="ghost" size="md" onClick={() => router.push('/agents')}>
|
|
271
|
+
Cancel
|
|
272
|
+
</Button>
|
|
273
|
+
<Button variant="primary" size="md" onClick={() => setStep('config')} disabled={!canProceed()}>
|
|
274
|
+
Next
|
|
275
|
+
</Button>
|
|
276
|
+
</div>
|
|
277
|
+
</div>
|
|
278
|
+
)}
|
|
279
|
+
|
|
280
|
+
{/* ─── Step: Config ─── */}
|
|
281
|
+
{step === 'config' && (
|
|
282
|
+
<div>
|
|
283
|
+
<div style={{ fontSize: 12, color: '#888', marginBottom: 16, letterSpacing: '0.05em' }}>
|
|
284
|
+
STEP 2 OF 2 · CONFIGURE AGENT
|
|
285
|
+
</div>
|
|
286
|
+
|
|
287
|
+
{/* Name */}
|
|
288
|
+
<div style={{ marginBottom: 16 }}>
|
|
289
|
+
<Input
|
|
290
|
+
label="Agent Name"
|
|
291
|
+
value={agentName}
|
|
292
|
+
onChange={(e) => setAgentName(e.target.value)}
|
|
293
|
+
placeholder="My Agent 🤖"
|
|
294
|
+
error={nameError}
|
|
295
|
+
/>
|
|
296
|
+
</div>
|
|
297
|
+
|
|
298
|
+
{/* Template info */}
|
|
299
|
+
<div style={{ marginBottom: 16, border: '1px solid var(--border)', borderRadius: 0, padding: '10px 14px', background: 'var(--bg)' }}>
|
|
300
|
+
<div style={{ display: 'flex', justifyContent: 'space-between', alignItems: 'center' }}>
|
|
301
|
+
<div>
|
|
302
|
+
<div style={{ fontSize: 11, color: '#888', marginBottom: 4 }}>TEMPLATE</div>
|
|
303
|
+
<div style={{ fontSize: 14, fontWeight: 500 }}>{selectedTemplate}</div>
|
|
304
|
+
</div>
|
|
305
|
+
<Button variant="ghost" size="sm" onClick={() => setStep('template')}>
|
|
306
|
+
Change
|
|
307
|
+
</Button>
|
|
308
|
+
</div>
|
|
309
|
+
<div style={{ fontSize: 11, color: '#666', marginTop: 6 }}>{templateDescription(selectedTemplate)}</div>
|
|
310
|
+
</div>
|
|
311
|
+
|
|
312
|
+
{/* Port */}
|
|
313
|
+
<div style={{ marginBottom: 16 }}>
|
|
314
|
+
<Input
|
|
315
|
+
label="Host Port"
|
|
316
|
+
value={portRange}
|
|
317
|
+
onChange={(e) => setPortRange(e.target.value.replace(/[^0-9-]/g, '').slice(0, 11))}
|
|
318
|
+
placeholder="3700-3709"
|
|
319
|
+
error={portError}
|
|
320
|
+
/>
|
|
321
|
+
<div style={{ fontSize: 10, color: 'var(--text-dim)', marginTop: 4 }}>
|
|
322
|
+
Port range (e.g. 3700-3709) or single port (e.g. 3700). Default: 10-port block. Port 3740 is reserved.
|
|
323
|
+
</div>
|
|
324
|
+
</div>
|
|
325
|
+
|
|
326
|
+
{/* Model */}
|
|
327
|
+
<div style={{ marginBottom: 16 }}>
|
|
328
|
+
<label style={{ display: 'block', fontSize: 12, color: '#888', marginBottom: 6 }}>Model (optional)</label>
|
|
329
|
+
{models.length === 0 ? (
|
|
330
|
+
<div style={{
|
|
331
|
+
padding: '10px 12px',
|
|
332
|
+
background: 'rgba(251,191,36,0.06)',
|
|
333
|
+
border: '1px solid rgba(251,191,36,0.2)',
|
|
334
|
+
fontSize: 12, color: '#fbbf24',
|
|
335
|
+
lineHeight: 1.5,
|
|
336
|
+
}}>
|
|
337
|
+
No provider keys configured. Add API keys in the Providers section to unlock model selection.
|
|
338
|
+
</div>
|
|
339
|
+
) : (
|
|
340
|
+
<select
|
|
341
|
+
value={selectedModel}
|
|
342
|
+
onChange={(e) => setSelectedModel(e.target.value)}
|
|
343
|
+
style={{
|
|
344
|
+
width: '100%', padding: '10px 12px', borderRadius: 0, border: '1px solid var(--border)',
|
|
345
|
+
background: 'var(--bg)', color: 'var(--text)', fontSize: 14, outline: 'none',
|
|
346
|
+
}}
|
|
347
|
+
>
|
|
348
|
+
{models.map((m) => (
|
|
349
|
+
<option key={m.id} value={m.id}>{m.name} ({m.provider})</option>
|
|
350
|
+
))}
|
|
351
|
+
</select>
|
|
352
|
+
)}
|
|
353
|
+
</div>
|
|
354
|
+
|
|
355
|
+
{/* Buttons */}
|
|
356
|
+
<div style={{ marginTop: 24, display: 'flex', gap: 12, justifyContent: 'space-between' }}>
|
|
357
|
+
<Button variant="ghost" size="md" onClick={() => setStep('template')}>
|
|
358
|
+
Back
|
|
359
|
+
</Button>
|
|
360
|
+
<Button variant="primary" size="md" onClick={handleSubmit} disabled={!canProceed() || submitting} loading={submitting}>
|
|
361
|
+
Deploy Agent
|
|
362
|
+
</Button>
|
|
363
|
+
</div>
|
|
364
|
+
</div>
|
|
365
|
+
)}
|
|
366
|
+
|
|
367
|
+
{/* ─── Step: Creating ─── */}
|
|
368
|
+
{step === 'creating' && (
|
|
369
|
+
<div style={{ textAlign: 'center', padding: 40 }}>
|
|
370
|
+
<div style={{ fontSize: 14, color: '#888', marginBottom: 16 }}>Creating agent container...</div>
|
|
371
|
+
<div style={{
|
|
372
|
+
width: 32, height: 32, border: '2px solid var(--border)', borderTopColor: 'var(--violet)',
|
|
373
|
+
borderRadius: '50%', animation: 'spin 0.8s linear infinite', margin: '0 auto',
|
|
374
|
+
}} />
|
|
375
|
+
<style>{`@keyframes spin { to { transform: rotate(360deg); } }`}</style>
|
|
376
|
+
</div>
|
|
377
|
+
)}
|
|
378
|
+
|
|
379
|
+
{/* ─── Step: Done ─── */}
|
|
380
|
+
{step === 'done' && result && (
|
|
381
|
+
<div>
|
|
382
|
+
<div style={{
|
|
383
|
+
border: '1px solid #22c55e', borderRadius: 0, padding: 20, background: 'rgba(34,197,94,0.06)', marginBottom: 20,
|
|
384
|
+
}}>
|
|
385
|
+
<div style={{ color: '#22c55e', fontSize: 16, fontWeight: 500, marginBottom: 8 }}>✓ Agent deployed</div>
|
|
386
|
+
<div style={{ fontSize: 13, color: '#888', marginBottom: 4 }}>
|
|
387
|
+
Container: <span style={{ color: 'var(--text)', fontFamily: 'var(--font-mono-stack)' }}>{result.containerId}</span>
|
|
388
|
+
</div>
|
|
389
|
+
<div style={{ fontSize: 13, color: '#888', marginBottom: 4 }}>
|
|
390
|
+
Name: <span style={{ color: 'var(--text)' }}>{result.name}</span>
|
|
391
|
+
</div>
|
|
392
|
+
{result.traefikUrl && (
|
|
393
|
+
<div style={{ fontSize: 13, color: '#888' }}>
|
|
394
|
+
Access: <a href={result.traefikUrl} target="_blank" rel="noopener noreferrer"
|
|
395
|
+
style={{ color: '#60a5fa', textDecoration: 'underline' }}>{result.traefikUrl}</a>
|
|
396
|
+
</div>
|
|
397
|
+
)}
|
|
398
|
+
</div>
|
|
399
|
+
<div style={{ display: 'flex', gap: 12 }}>
|
|
400
|
+
<Button variant="ghost" size="md" onClick={() => { setToast(null); router.push('/agents'); }} style={{ flex: 1 }}>
|
|
401
|
+
View All Agents
|
|
402
|
+
</Button>
|
|
403
|
+
<Button variant="primary" size="md" onClick={() => {
|
|
404
|
+
setToast(null);
|
|
405
|
+
setStep('template');
|
|
406
|
+
setSelectedTemplate('');
|
|
407
|
+
setAgentName('');
|
|
408
|
+
setPortRange('');
|
|
409
|
+
setSelectedModel('');
|
|
410
|
+
setResult(null);
|
|
411
|
+
}} style={{ flex: 1 }}>
|
|
412
|
+
Create Another
|
|
413
|
+
</Button>
|
|
414
|
+
</div>
|
|
415
|
+
</div>
|
|
416
|
+
)}
|
|
417
|
+
|
|
418
|
+
{/* ─── Step: Error ─── */}
|
|
419
|
+
{step === 'error' && (
|
|
420
|
+
<div>
|
|
421
|
+
<div style={{
|
|
422
|
+
border: '1px solid #ef4444', borderRadius: 0, padding: 20, background: 'rgba(239,68,68,0.06)', marginBottom: 20,
|
|
423
|
+
}}>
|
|
424
|
+
<div style={{ color: '#ef4444', fontSize: 16, fontWeight: 500, marginBottom: 8 }}>Failed to create agent</div>
|
|
425
|
+
<div style={{ fontSize: 13, color: '#f87171', fontFamily: 'var(--font-mono-stack)', wordBreak: 'break-word' }}>
|
|
426
|
+
{errorMsg}
|
|
427
|
+
</div>
|
|
428
|
+
</div>
|
|
429
|
+
<Button variant="ghost" size="md" onClick={() => setStep('config')}>
|
|
430
|
+
Back to Config
|
|
431
|
+
</Button>
|
|
432
|
+
</div>
|
|
433
|
+
)}
|
|
434
|
+
</div>
|
|
435
|
+
</div>
|
|
436
|
+
);
|
|
437
|
+
}
|
|
@@ -0,0 +1,126 @@
|
|
|
1
|
+
import { execSync } from 'child_process';
|
|
2
|
+
import * as fs from 'fs';
|
|
3
|
+
import * as path from 'path';
|
|
4
|
+
import { DB_FILE, AGENTS_TOKEN_FILE, MODELS_CONFIG_FILE, PROVIDER_KEYS_FILE, templatesDir } from '@/lib/rev4a-paths';
|
|
5
|
+
import CreatePageClient from './PageClient';
|
|
6
|
+
|
|
7
|
+
export const dynamic = 'force-dynamic';
|
|
8
|
+
|
|
9
|
+
interface TemplateInfo {
|
|
10
|
+
id: string;
|
|
11
|
+
files: string[];
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
interface ModelInfo {
|
|
15
|
+
id: string;
|
|
16
|
+
name: string;
|
|
17
|
+
provider: string;
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
async function getTemplates(): Promise<TemplateInfo[]> {
|
|
21
|
+
const dir = templatesDir(process.cwd());
|
|
22
|
+
try {
|
|
23
|
+
const entries = fs.readdirSync(dir, { withFileTypes: true });
|
|
24
|
+
return entries
|
|
25
|
+
.filter((e) => e.isDirectory() && e.name !== 'base-image')
|
|
26
|
+
.map((e) => ({
|
|
27
|
+
id: e.name,
|
|
28
|
+
files: fs.readdirSync(path.join(dir, e.name)),
|
|
29
|
+
}));
|
|
30
|
+
} catch {
|
|
31
|
+
return [];
|
|
32
|
+
}
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
async function getModels(): Promise<ModelInfo[]> {
|
|
36
|
+
try {
|
|
37
|
+
const raw = fs.readFileSync(MODELS_CONFIG_FILE, 'utf-8');
|
|
38
|
+
const config = JSON.parse(raw);
|
|
39
|
+
const allModels: ModelInfo[] = (config.models || []).filter((m: { enabled: boolean }) => m.enabled !== false);
|
|
40
|
+
|
|
41
|
+
let configuredProviders: string[] = [];
|
|
42
|
+
try {
|
|
43
|
+
const keysRaw = fs.readFileSync(PROVIDER_KEYS_FILE, 'utf-8');
|
|
44
|
+
configuredProviders = Object.keys(JSON.parse(keysRaw));
|
|
45
|
+
} catch { /* no keys configured yet */ }
|
|
46
|
+
|
|
47
|
+
if (configuredProviders.length > 0) {
|
|
48
|
+
return allModels.filter((m) => configuredProviders.includes(m.provider));
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
// No providers configured — show no models
|
|
52
|
+
return [];
|
|
53
|
+
} catch {
|
|
54
|
+
return [];
|
|
55
|
+
}
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
async function getUsedNames(): Promise<string[]> {
|
|
59
|
+
try {
|
|
60
|
+
const raw = execSync(
|
|
61
|
+
`docker ps --filter "label=AGENT_ID" --format '{{.Names}}'`,
|
|
62
|
+
{ encoding: 'utf-8', timeout: 5000 },
|
|
63
|
+
).trim();
|
|
64
|
+
if (!raw) return [];
|
|
65
|
+
|
|
66
|
+
// Read AGENT_NAME env from each container (display name)
|
|
67
|
+
const names: string[] = [];
|
|
68
|
+
for (const name of raw.split('\n').filter(Boolean)) {
|
|
69
|
+
try {
|
|
70
|
+
const displayName = execSync(
|
|
71
|
+
`docker exec ${name} sh -c 'echo $AGENT_NAME'`,
|
|
72
|
+
{ encoding: 'utf-8', timeout: 3000 },
|
|
73
|
+
).trim();
|
|
74
|
+
if (displayName) names.push(displayName);
|
|
75
|
+
} catch { /* skip */ }
|
|
76
|
+
}
|
|
77
|
+
return names;
|
|
78
|
+
} catch {
|
|
79
|
+
return [];
|
|
80
|
+
}
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
async function getUsedPorts(): Promise<number[]> {
|
|
84
|
+
try {
|
|
85
|
+
const raw = execSync(
|
|
86
|
+
`docker ps --format '{{.Ports}}'`,
|
|
87
|
+
{ encoding: 'utf-8', timeout: 5000 },
|
|
88
|
+
).trim();
|
|
89
|
+
const ports: number[] = [];
|
|
90
|
+
if (raw) {
|
|
91
|
+
for (const line of raw.split('\n')) {
|
|
92
|
+
const matches = line.match(/(\d+)->/g);
|
|
93
|
+
if (matches) {
|
|
94
|
+
matches.forEach((m) => {
|
|
95
|
+
const p = parseInt(m.replace('->', ''), 10);
|
|
96
|
+
if (!isNaN(p)) ports.push(p);
|
|
97
|
+
});
|
|
98
|
+
}
|
|
99
|
+
}
|
|
100
|
+
}
|
|
101
|
+
// Add Rev4a port as reserved so the client won't suggest it
|
|
102
|
+
ports.push(3740);
|
|
103
|
+
return [...new Set(ports)];
|
|
104
|
+
} catch {
|
|
105
|
+
return [3740];
|
|
106
|
+
}
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
export default async function CreateAgentPage() {
|
|
110
|
+
|
|
111
|
+
const [templates, models, usedNames, usedPorts] = await Promise.all([
|
|
112
|
+
getTemplates(),
|
|
113
|
+
getModels(),
|
|
114
|
+
getUsedNames(),
|
|
115
|
+
getUsedPorts(),
|
|
116
|
+
]);
|
|
117
|
+
|
|
118
|
+
return (
|
|
119
|
+
<CreatePageClient
|
|
120
|
+
templates={templates}
|
|
121
|
+
models={models}
|
|
122
|
+
usedNames={usedNames}
|
|
123
|
+
usedPorts={usedPorts}
|
|
124
|
+
/>
|
|
125
|
+
);
|
|
126
|
+
}
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
import { Skeleton, CardRowSkeleton } from '../components/Skeleton';
|
|
2
|
+
|
|
3
|
+
export default function AgentsLoading() {
|
|
4
|
+
return (
|
|
5
|
+
<div style={{
|
|
6
|
+
padding: '24px 20px',
|
|
7
|
+
maxWidth: 1080,
|
|
8
|
+
margin: '0 auto',
|
|
9
|
+
}}>
|
|
10
|
+
<Skeleton width={120} height={24} accent style={{ marginBottom: 20 }} />
|
|
11
|
+
<CardRowSkeleton count={5} height={56} />
|
|
12
|
+
</div>
|
|
13
|
+
);
|
|
14
|
+
}
|