@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,23 @@
|
|
|
1
|
+
import { NextResponse, type NextRequest } from 'next/server';
|
|
2
|
+
import { openDb } from '@/lib/db';
|
|
3
|
+
import { requireAuthJWT } from '@/lib/rev4a-auth';
|
|
4
|
+
|
|
5
|
+
export async function GET(request: NextRequest): Promise<NextResponse> {
|
|
6
|
+
const denied = await requireAuthJWT(request); if (denied) return denied as any;
|
|
7
|
+
try {
|
|
8
|
+
const db = openDb();
|
|
9
|
+
const sessions = db.prepare(`
|
|
10
|
+
SELECT s.*,
|
|
11
|
+
l.label AS lineage_label,
|
|
12
|
+
l.agent_name AS lineage_agent_name
|
|
13
|
+
FROM sessions s
|
|
14
|
+
LEFT JOIN lineage l ON s.session_id = l.child_id
|
|
15
|
+
ORDER BY s.started_at DESC
|
|
16
|
+
LIMIT 2000
|
|
17
|
+
`).all();
|
|
18
|
+
db.close();
|
|
19
|
+
return NextResponse.json(sessions);
|
|
20
|
+
} catch (e: unknown) {
|
|
21
|
+
return NextResponse.json({ error: (e as Error).message }, { status: 500 });
|
|
22
|
+
}
|
|
23
|
+
}
|
|
@@ -0,0 +1,102 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* GET /api/setup/agent-image
|
|
3
|
+
*
|
|
4
|
+
* Checks if openclaw-agent-base:latest exists locally or needs downloading.
|
|
5
|
+
* Returns status and a hint for the UI.
|
|
6
|
+
*
|
|
7
|
+
* POST /api/setup/agent-image
|
|
8
|
+
*
|
|
9
|
+
* Downloads openclaw-agent-base:latest from ghcr.io (public registry).
|
|
10
|
+
* Streams progress via SSE so the setup wizard can show a live progress bar.
|
|
11
|
+
*
|
|
12
|
+
* Only one download at a time — returns 409 if already running.
|
|
13
|
+
* Cleans up the docker process if the client disconnects.
|
|
14
|
+
*/
|
|
15
|
+
import { NextResponse } from 'next/server';
|
|
16
|
+
import { downloadAgentImage, getIsDownloading } from '@/lib/buildAgentImage';
|
|
17
|
+
import { execSync } from 'child_process';
|
|
18
|
+
|
|
19
|
+
export const dynamic = 'force-dynamic';
|
|
20
|
+
|
|
21
|
+
function dockerOk(): boolean {
|
|
22
|
+
try {
|
|
23
|
+
execSync('docker version --format "{{.Client.Version}}" 2>/dev/null', {
|
|
24
|
+
encoding: 'utf-8',
|
|
25
|
+
timeout: 2000,
|
|
26
|
+
});
|
|
27
|
+
return true;
|
|
28
|
+
} catch {
|
|
29
|
+
return false;
|
|
30
|
+
}
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
export async function GET(): Promise<NextResponse> {
|
|
34
|
+
if (!dockerOk()) {
|
|
35
|
+
return NextResponse.json({ exists: false });
|
|
36
|
+
}
|
|
37
|
+
try {
|
|
38
|
+
const { exec } = await import('child_process');
|
|
39
|
+
const result = await new Promise<string>((resolve, reject) => {
|
|
40
|
+
exec(
|
|
41
|
+
'docker image inspect openclaw-agent-base:latest --format "{{.ID}}" 2>/dev/null',
|
|
42
|
+
{ timeout: 5000 },
|
|
43
|
+
(err, stdout) => {
|
|
44
|
+
if (err) return reject(err);
|
|
45
|
+
resolve(stdout.trim());
|
|
46
|
+
},
|
|
47
|
+
);
|
|
48
|
+
});
|
|
49
|
+
if (result) {
|
|
50
|
+
return NextResponse.json({ exists: true, imageId: result });
|
|
51
|
+
}
|
|
52
|
+
return NextResponse.json({ exists: false });
|
|
53
|
+
} catch {
|
|
54
|
+
return NextResponse.json({ exists: false });
|
|
55
|
+
}
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
export async function POST(): Promise<Response> {
|
|
59
|
+
if (getIsDownloading()) {
|
|
60
|
+
return new Response(
|
|
61
|
+
JSON.stringify({ error: 'A download is already in progress' }),
|
|
62
|
+
{ status: 409, headers: { 'Content-Type': 'application/json' } },
|
|
63
|
+
);
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
const encoder = new TextEncoder();
|
|
67
|
+
|
|
68
|
+
const stream = new ReadableStream({
|
|
69
|
+
async start(controller) {
|
|
70
|
+
const send = (data: string) => {
|
|
71
|
+
try {
|
|
72
|
+
controller.enqueue(encoder.encode(`data: ${data}\n\n`));
|
|
73
|
+
} catch { /* stream closed */ }
|
|
74
|
+
};
|
|
75
|
+
|
|
76
|
+
const ctrl = controller as ReadableStreamDefaultController & { signal?: AbortSignal };
|
|
77
|
+
|
|
78
|
+
try {
|
|
79
|
+
await downloadAgentImage({
|
|
80
|
+
force: false, // setup: skip if already exists
|
|
81
|
+
onEvent: send,
|
|
82
|
+
signal: ctrl.signal,
|
|
83
|
+
});
|
|
84
|
+
} catch (e: unknown) {
|
|
85
|
+
send(JSON.stringify({
|
|
86
|
+
type: 'error',
|
|
87
|
+
message: (e as Error).message || 'Failed to download agent image',
|
|
88
|
+
}));
|
|
89
|
+
}
|
|
90
|
+
controller.close();
|
|
91
|
+
},
|
|
92
|
+
cancel() { /* downloadAgentImage handles cleanup via signal */ },
|
|
93
|
+
});
|
|
94
|
+
|
|
95
|
+
return new Response(stream, {
|
|
96
|
+
headers: {
|
|
97
|
+
'Content-Type': 'text/event-stream',
|
|
98
|
+
'Cache-Control': 'no-cache',
|
|
99
|
+
Connection: 'keep-alive',
|
|
100
|
+
},
|
|
101
|
+
});
|
|
102
|
+
}
|
|
@@ -0,0 +1,111 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* POST /api/setup/password
|
|
3
|
+
*
|
|
4
|
+
* No auth required. Sets the admin password on first run.
|
|
5
|
+
* Refuses if a password is already configured.
|
|
6
|
+
* Also generates a random JWT secret and API token if missing.
|
|
7
|
+
*
|
|
8
|
+
* Body: { password: string }
|
|
9
|
+
*
|
|
10
|
+
* Does NOT restart the server — the password takes effect
|
|
11
|
+
* after the full setup wizard completes and the server restarts.
|
|
12
|
+
*/
|
|
13
|
+
import { NextResponse } from 'next/server';
|
|
14
|
+
import * as fs from 'fs';
|
|
15
|
+
import * as path from 'path';
|
|
16
|
+
import * as crypto from 'crypto';
|
|
17
|
+
import { ENV_FILE } from '@/lib/rev4a-paths';
|
|
18
|
+
|
|
19
|
+
export const dynamic = 'force-dynamic';
|
|
20
|
+
export const runtime = 'nodejs';
|
|
21
|
+
|
|
22
|
+
function generateSecret(len: number): string {
|
|
23
|
+
return crypto.randomBytes(len).toString('base64url').slice(0, len);
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
export async function POST(request: Request): Promise<NextResponse> {
|
|
27
|
+
// Check both env and .env file (same fallback pattern as login/status routes)
|
|
28
|
+
let currentPassword = process.env.REV4A_PASSWORD;
|
|
29
|
+
if (typeof currentPassword !== 'string' || currentPassword.length === 0) {
|
|
30
|
+
try {
|
|
31
|
+
if (fs.existsSync(ENV_FILE)) {
|
|
32
|
+
const content = fs.readFileSync(ENV_FILE, 'utf-8');
|
|
33
|
+
const match = content.match(/^REV4A_PASSWORD=(.+)$/m);
|
|
34
|
+
if (match && match[1].trim()) {
|
|
35
|
+
currentPassword = match[1].trim();
|
|
36
|
+
}
|
|
37
|
+
}
|
|
38
|
+
} catch { /* ignore */ }
|
|
39
|
+
}
|
|
40
|
+
if (typeof currentPassword === 'string' && currentPassword.length > 0) {
|
|
41
|
+
return NextResponse.json(
|
|
42
|
+
{ error: 'Password is already configured. Use the Config page to change it.' },
|
|
43
|
+
{ status: 409 },
|
|
44
|
+
);
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
let body: { password?: string };
|
|
48
|
+
try {
|
|
49
|
+
body = await request.json();
|
|
50
|
+
} catch {
|
|
51
|
+
return NextResponse.json({ error: 'Invalid JSON body' }, { status: 400 });
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
if (!body.password || typeof body.password !== 'string') {
|
|
55
|
+
return NextResponse.json({ error: 'password is required' }, { status: 400 });
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
if (body.password.length < 8) {
|
|
59
|
+
return NextResponse.json(
|
|
60
|
+
{ error: 'Password must be at least 8 characters' },
|
|
61
|
+
{ status: 400 },
|
|
62
|
+
);
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
try {
|
|
66
|
+
// Ensure parent directory exists
|
|
67
|
+
const dir = path.dirname(ENV_FILE);
|
|
68
|
+
if (!fs.existsSync(dir)) fs.mkdirSync(dir, { recursive: true });
|
|
69
|
+
|
|
70
|
+
let content = '';
|
|
71
|
+
if (fs.existsSync(ENV_FILE)) {
|
|
72
|
+
content = fs.readFileSync(ENV_FILE, 'utf-8');
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
// Ensure password line
|
|
76
|
+
if (content.includes('REV4A_PASSWORD=')) {
|
|
77
|
+
content = content
|
|
78
|
+
.split('\n')
|
|
79
|
+
.map((line) =>
|
|
80
|
+
line.startsWith('REV4A_PASSWORD=')
|
|
81
|
+
? 'REV4A_PASSWORD=' + body.password
|
|
82
|
+
: line,
|
|
83
|
+
)
|
|
84
|
+
.join('\n');
|
|
85
|
+
} else {
|
|
86
|
+
content += '\nREV4A_PASSWORD=' + body.password + '\n';
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
// Generate JWT secret if missing
|
|
90
|
+
if (!content.includes('REV4A_JWT_SECRET=')) {
|
|
91
|
+
const secret = generateSecret(64);
|
|
92
|
+
content += '\nREV4A_JWT_SECRET=' + secret + '\n';
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
// Generate API token if missing
|
|
96
|
+
if (!content.includes('REV4A_TOKEN=')) {
|
|
97
|
+
const token = generateSecret(32);
|
|
98
|
+
content += '\nREV4A_TOKEN=' + token + '\n';
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
fs.writeFileSync(ENV_FILE, content.trim() + '\n', { encoding: 'utf-8', mode: 0o600 });
|
|
102
|
+
|
|
103
|
+
// Update in-memory so proxy redirects work before restart
|
|
104
|
+
process.env.REV4A_PASSWORD = body.password;
|
|
105
|
+
|
|
106
|
+
return NextResponse.json({ status: 'ok' });
|
|
107
|
+
} catch (e: unknown) {
|
|
108
|
+
console.error('[setup/password]', e);
|
|
109
|
+
return NextResponse.json({ error: 'Failed to write password' }, { status: 500 });
|
|
110
|
+
}
|
|
111
|
+
}
|
|
@@ -0,0 +1,76 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* POST /api/setup/restart
|
|
3
|
+
*
|
|
4
|
+
* No auth required. Restarts the Rev4a server so that changes to
|
|
5
|
+
* .env (especially REV4A_PASSWORD) take effect.
|
|
6
|
+
*
|
|
7
|
+
* Next.js inlines env vars at build time, so a runtime process.env
|
|
8
|
+
* mutation is not enough — the process must restart to reload .env.
|
|
9
|
+
*
|
|
10
|
+
* Strategy:
|
|
11
|
+
* 1. If running under systemd (rev4a.service), restart via systemctl.
|
|
12
|
+
* 2. Otherwise send SIGTERM to the current process. The parent process
|
|
13
|
+
* manager (npx rev4a all or a shell wrapper) should restart it.
|
|
14
|
+
*/
|
|
15
|
+
import { NextResponse } from 'next/server';
|
|
16
|
+
import * as fs from 'fs';
|
|
17
|
+
import { execSync } from 'child_process';
|
|
18
|
+
import { ENV_FILE } from '@/lib/rev4a-paths';
|
|
19
|
+
|
|
20
|
+
export const dynamic = 'force-dynamic';
|
|
21
|
+
export const runtime = 'nodejs';
|
|
22
|
+
|
|
23
|
+
export async function POST(): Promise<NextResponse> {
|
|
24
|
+
try {
|
|
25
|
+
if (!fs.existsSync(ENV_FILE)) {
|
|
26
|
+
return NextResponse.json({ error: '.env file not found' }, { status: 500 });
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
const envContent = fs.readFileSync(ENV_FILE, 'utf-8');
|
|
30
|
+
const match = envContent.match(/^REV4A_PASSWORD=(.*)$/m);
|
|
31
|
+
if (!match || !match[1].trim()) {
|
|
32
|
+
return NextResponse.json(
|
|
33
|
+
{ error: 'REV4A_PASSWORD is empty or missing in .env' },
|
|
34
|
+
{ status: 400 },
|
|
35
|
+
);
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
// Try systemd first (production).
|
|
39
|
+
// If it fails, fall back to SIGTERM (dev mode / container).
|
|
40
|
+
let usedSystemd = false;
|
|
41
|
+
try {
|
|
42
|
+
execSync('sudo systemctl daemon-reload', { timeout: 5000 });
|
|
43
|
+
usedSystemd = true;
|
|
44
|
+
} catch {
|
|
45
|
+
// systemctl not available — fine, we'll SIGTERM
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
const response = NextResponse.json({
|
|
49
|
+
status: 'ok',
|
|
50
|
+
restarting: true,
|
|
51
|
+
mode: usedSystemd ? 'systemd' : 'process',
|
|
52
|
+
});
|
|
53
|
+
|
|
54
|
+
if (usedSystemd) {
|
|
55
|
+
setTimeout(() => {
|
|
56
|
+
try {
|
|
57
|
+
execSync('sudo systemctl restart rev4a.service', { timeout: 15000 });
|
|
58
|
+
} catch (err) {
|
|
59
|
+
console.error('[setup/restart] systemd restart failed:', err);
|
|
60
|
+
}
|
|
61
|
+
}, 2000);
|
|
62
|
+
} else {
|
|
63
|
+
// Dev mode / container: send SIGTERM after flushing response.
|
|
64
|
+
// The parent (npx rev4a all or shell) will restart the process,
|
|
65
|
+
// which will re-read .env at startup.
|
|
66
|
+
setTimeout(() => {
|
|
67
|
+
process.kill(process.pid, 'SIGTERM');
|
|
68
|
+
}, 2000);
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
return response;
|
|
72
|
+
} catch (e: unknown) {
|
|
73
|
+
console.error('[setup/restart]', e);
|
|
74
|
+
return NextResponse.json({ error: 'Failed to restart' }, { status: 500 });
|
|
75
|
+
}
|
|
76
|
+
}
|
|
@@ -0,0 +1,105 @@
|
|
|
1
|
+
import { NextResponse } from 'next/server';
|
|
2
|
+
import fs from 'fs';
|
|
3
|
+
import path from 'path';
|
|
4
|
+
import { execFileSync } from 'child_process';
|
|
5
|
+
import { requireAuthJWT } from '@/lib/rev4a-auth';
|
|
6
|
+
|
|
7
|
+
const SHARED_SKILLS_DIR = '/docker/shared-skills';
|
|
8
|
+
|
|
9
|
+
/**
|
|
10
|
+
* DELETE /api/skills/delete
|
|
11
|
+
*
|
|
12
|
+
* Deletes a skill. Supports two modes:
|
|
13
|
+
* - shared: removes from /docker/shared-skills/
|
|
14
|
+
* - workspace/container: removes from container via docker exec rm -rf
|
|
15
|
+
*
|
|
16
|
+
* Body: { skillName, containerName? }
|
|
17
|
+
*/
|
|
18
|
+
export async function DELETE(request) {
|
|
19
|
+
const denied = await requireAuthJWT(request);
|
|
20
|
+
if (denied) return denied;
|
|
21
|
+
|
|
22
|
+
try {
|
|
23
|
+
const { skillName, containerName } = await request.json();
|
|
24
|
+
|
|
25
|
+
if (!skillName) {
|
|
26
|
+
return NextResponse.json(
|
|
27
|
+
{ error: 'skillName is required' },
|
|
28
|
+
{ status: 400 }
|
|
29
|
+
);
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
// Sanitize: prevent path traversal
|
|
33
|
+
if (!/^[a-z0-9]([a-z0-9-]*[a-z0-9])?(\/[a-z0-9]([a-z0-9-]*[a-z0-9])?)*$/.test(skillName)) {
|
|
34
|
+
return NextResponse.json(
|
|
35
|
+
{ error: 'Invalid skill name format' },
|
|
36
|
+
{ status: 400 }
|
|
37
|
+
);
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
if (containerName) {
|
|
41
|
+
// Delete from agent container workspace
|
|
42
|
+
const skillDir = `/root/.openclaw/workspace/skills/${skillName}`;
|
|
43
|
+
|
|
44
|
+
// Verify container exists
|
|
45
|
+
try {
|
|
46
|
+
execFileSync('docker', ['inspect', containerName], { encoding: 'utf8', timeout: 5000 });
|
|
47
|
+
} catch {
|
|
48
|
+
return NextResponse.json(
|
|
49
|
+
{ error: `Container ${containerName} not found` },
|
|
50
|
+
{ status: 404 }
|
|
51
|
+
);
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
// Check if skill exists in container
|
|
55
|
+
try {
|
|
56
|
+
execFileSync('docker', ['exec', containerName, 'test', '-d', skillDir], { encoding: 'utf8', timeout: 5000 });
|
|
57
|
+
} catch {
|
|
58
|
+
return NextResponse.json(
|
|
59
|
+
{ error: `Skill "${skillName}" not found in container ${containerName}` },
|
|
60
|
+
{ status: 404 }
|
|
61
|
+
);
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
execFileSync('docker', ['exec', containerName, 'rm', '-rf', skillDir], { encoding: 'utf8', timeout: 10000 });
|
|
65
|
+
|
|
66
|
+
return NextResponse.json({
|
|
67
|
+
ok: true,
|
|
68
|
+
skillName,
|
|
69
|
+
containerName,
|
|
70
|
+
deleted: true,
|
|
71
|
+
from: 'workspace',
|
|
72
|
+
});
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
// Delete from shared skills directory
|
|
76
|
+
const skillDir = path.join(SHARED_SKILLS_DIR, skillName);
|
|
77
|
+
|
|
78
|
+
if (!fs.existsSync(skillDir)) {
|
|
79
|
+
return NextResponse.json(
|
|
80
|
+
{ error: `Skill "${skillName}" not found in shared skills` },
|
|
81
|
+
{ status: 404 }
|
|
82
|
+
);
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
// Prevent deletion of non-shared paths (safety check)
|
|
86
|
+
if (!skillDir.startsWith(SHARED_SKILLS_DIR)) {
|
|
87
|
+
return NextResponse.json(
|
|
88
|
+
{ error: 'Invalid path' },
|
|
89
|
+
{ status: 400 }
|
|
90
|
+
);
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
fs.rmSync(skillDir, { recursive: true, force: true });
|
|
94
|
+
|
|
95
|
+
return NextResponse.json({
|
|
96
|
+
ok: true,
|
|
97
|
+
skillName,
|
|
98
|
+
deleted: true,
|
|
99
|
+
from: 'shared',
|
|
100
|
+
});
|
|
101
|
+
} catch (err) {
|
|
102
|
+
const msg = err instanceof Error ? err.message : 'Internal error';
|
|
103
|
+
return NextResponse.json({ error: msg }, { status: 500 });
|
|
104
|
+
}
|
|
105
|
+
}
|
|
@@ -0,0 +1,129 @@
|
|
|
1
|
+
import { NextResponse } from 'next/server';
|
|
2
|
+
import fs from 'fs';
|
|
3
|
+
import path from 'path';
|
|
4
|
+
import { execSync, execFileSync } from 'child_process';
|
|
5
|
+
import { requireAuthJWT } from '@/lib/rev4a-auth';
|
|
6
|
+
|
|
7
|
+
const SHARED_SKILLS_DIR = '/docker/shared-skills';
|
|
8
|
+
|
|
9
|
+
/**
|
|
10
|
+
* POST /api/skills/promote
|
|
11
|
+
*
|
|
12
|
+
* Copies a skill from an agent container's workspace to the shared skills
|
|
13
|
+
* directory, making it available to all agents via extraDirs.
|
|
14
|
+
*
|
|
15
|
+
* Body: {
|
|
16
|
+
* containerName: "openclaw-atlas",
|
|
17
|
+
* skillName: "my-custom-skill"
|
|
18
|
+
* }
|
|
19
|
+
*/
|
|
20
|
+
export async function POST(request) {
|
|
21
|
+
const denied = await requireAuthJWT(request);
|
|
22
|
+
if (denied) return denied;
|
|
23
|
+
|
|
24
|
+
try {
|
|
25
|
+
const { containerName, skillName } = await request.json();
|
|
26
|
+
|
|
27
|
+
if (!containerName || !skillName) {
|
|
28
|
+
return NextResponse.json(
|
|
29
|
+
{ error: 'containerName and skillName are required' },
|
|
30
|
+
{ status: 400 }
|
|
31
|
+
);
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
// Sanitize: skill name must be a valid directory name
|
|
35
|
+
if (!/^[a-z0-9]([a-z0-9-]*[a-z0-9])?(\/[a-z0-9]([a-z0-9-]*[a-z0-9])?)*$/.test(skillName)) {
|
|
36
|
+
return NextResponse.json(
|
|
37
|
+
{ error: 'Invalid skill name format' },
|
|
38
|
+
{ status: 400 }
|
|
39
|
+
);
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
// Verify the container exists and has an AGENT_ID label
|
|
43
|
+
let agentId;
|
|
44
|
+
try {
|
|
45
|
+
agentId = execFileSync('docker', [
|
|
46
|
+
'inspect', containerName,
|
|
47
|
+
'--format', '{{index .Config.Labels "AGENT_ID"}}'
|
|
48
|
+
], { encoding: 'utf8', timeout: 5000 }).trim();
|
|
49
|
+
if (!agentId) {
|
|
50
|
+
return NextResponse.json(
|
|
51
|
+
{ error: `Container ${containerName} has no AGENT_ID label` },
|
|
52
|
+
{ status: 400 }
|
|
53
|
+
);
|
|
54
|
+
}
|
|
55
|
+
} catch {
|
|
56
|
+
return NextResponse.json(
|
|
57
|
+
{ error: `Container ${containerName} not found` },
|
|
58
|
+
{ status: 404 }
|
|
59
|
+
);
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
// Read SKILL.md content from the container
|
|
63
|
+
const srcPath = `/root/.openclaw/workspace/skills/${skillName}/SKILL.md`;
|
|
64
|
+
let content;
|
|
65
|
+
try {
|
|
66
|
+
content = execFileSync('docker', ['exec', containerName, 'cat', srcPath], {
|
|
67
|
+
encoding: 'utf8', timeout: 5000
|
|
68
|
+
});
|
|
69
|
+
} catch {
|
|
70
|
+
return NextResponse.json(
|
|
71
|
+
{ error: `Skill "${skillName}" not found in container ${containerName}` },
|
|
72
|
+
{ status: 404 }
|
|
73
|
+
);
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
// Check for additional files in the skill directory (assets, scripts, etc.)
|
|
77
|
+
const extraFiles = [];
|
|
78
|
+
try {
|
|
79
|
+
const listRaw = execFileSync('docker', ['exec', containerName, 'ls', '-1p', `/root/.openclaw/workspace/skills/${skillName}/`], {
|
|
80
|
+
encoding: 'utf8', timeout: 5000
|
|
81
|
+
}).trim();
|
|
82
|
+
// Exclude SKILL.md and directories from the extras list
|
|
83
|
+
extraFiles.push(
|
|
84
|
+
...listRaw.split('\n')
|
|
85
|
+
.map(f => f.trim())
|
|
86
|
+
.filter(f => f && !f.endsWith('/') && f !== 'SKILL.md')
|
|
87
|
+
);
|
|
88
|
+
} catch {
|
|
89
|
+
// No extra files — that's fine
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
// Create shared skills directory if it doesn't exist
|
|
93
|
+
const destDir = path.join(SHARED_SKILLS_DIR, skillName);
|
|
94
|
+
fs.mkdirSync(destDir, { recursive: true });
|
|
95
|
+
|
|
96
|
+
// Write SKILL.md
|
|
97
|
+
const destPath = path.join(destDir, 'SKILL.md');
|
|
98
|
+
const overwritten = fs.existsSync(destPath);
|
|
99
|
+
fs.writeFileSync(destPath, content, 'utf8');
|
|
100
|
+
|
|
101
|
+
// Copy extra files from container
|
|
102
|
+
let extraCopied = 0;
|
|
103
|
+
for (const file of extraFiles) {
|
|
104
|
+
try {
|
|
105
|
+
const fileContent = execFileSync('docker', ['exec', containerName, 'cat', `/root/.openclaw/workspace/skills/${skillName}/${file}`], {
|
|
106
|
+
encoding: 'utf8', timeout: 5000
|
|
107
|
+
});
|
|
108
|
+
fs.writeFileSync(path.join(destDir, file), fileContent, 'utf8');
|
|
109
|
+
extraCopied++;
|
|
110
|
+
} catch {
|
|
111
|
+
// Skip files that can't be read
|
|
112
|
+
}
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
return NextResponse.json({
|
|
116
|
+
ok: true,
|
|
117
|
+
skillName,
|
|
118
|
+
agentId,
|
|
119
|
+
containerName,
|
|
120
|
+
destPath,
|
|
121
|
+
overwritten,
|
|
122
|
+
extraCopied,
|
|
123
|
+
});
|
|
124
|
+
} catch (err) {
|
|
125
|
+
// Only return message for known Error objects, never raw unknown throws
|
|
126
|
+
const msg = err instanceof Error ? err.message : 'Internal error';
|
|
127
|
+
return NextResponse.json({ error: msg }, { status: 500 });
|
|
128
|
+
}
|
|
129
|
+
}
|