@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,203 @@
|
|
|
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
|
+
const CONTAINER_BUNDLED_SKILLS_DIR = '/usr/local/lib/node_modules/openclaw/skills';
|
|
9
|
+
|
|
10
|
+
function readSkillMeta(skillDir, name, type, agentId) {
|
|
11
|
+
const skillMdPath = path.join(skillDir, name, 'SKILL.md');
|
|
12
|
+
let description = '';
|
|
13
|
+
let version = '';
|
|
14
|
+
let hasSkillMd = false;
|
|
15
|
+
|
|
16
|
+
try {
|
|
17
|
+
const content = fs.readFileSync(skillMdPath, 'utf8');
|
|
18
|
+
hasSkillMd = true;
|
|
19
|
+
const descMatch = content.match(/^description:\s*(.+)$/m);
|
|
20
|
+
const verMatch = content.match(/^\*\*Version:\*\*\s*(.+)$/m) || content.match(/^version:\s*(.+)$/m);
|
|
21
|
+
if (descMatch) description = descMatch[1].trim();
|
|
22
|
+
if (verMatch) version = verMatch[1].trim();
|
|
23
|
+
} catch {}
|
|
24
|
+
|
|
25
|
+
const result = {
|
|
26
|
+
name,
|
|
27
|
+
type,
|
|
28
|
+
path: path.join(skillDir, name),
|
|
29
|
+
skillMdPath: hasSkillMd ? skillMdPath : null,
|
|
30
|
+
hasSkillMd,
|
|
31
|
+
description,
|
|
32
|
+
version,
|
|
33
|
+
};
|
|
34
|
+
|
|
35
|
+
if (agentId) result.agentId = agentId;
|
|
36
|
+
|
|
37
|
+
return result;
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
/**
|
|
41
|
+
* Discover agent containers with AGENT_ID label.
|
|
42
|
+
* Returns [{ containerName, agentId }]
|
|
43
|
+
*/
|
|
44
|
+
function discoverAgentContainers() {
|
|
45
|
+
try {
|
|
46
|
+
// Get container names with AGENT_ID label
|
|
47
|
+
const namesRaw = execFileSync('docker', [
|
|
48
|
+
'ps', '--filter', 'label=AGENT_ID', '--format', '{{.Names}}'
|
|
49
|
+
], { encoding: 'utf8', timeout: 5000 }).trim();
|
|
50
|
+
if (!namesRaw) return [];
|
|
51
|
+
|
|
52
|
+
return namesRaw.split('\n').map(name => {
|
|
53
|
+
const containerName = name.trim();
|
|
54
|
+
if (!containerName) return null;
|
|
55
|
+
// Get agent ID from label using docker inspect
|
|
56
|
+
const agentId = execFileSync('docker', [
|
|
57
|
+
'inspect', containerName, '--format', '{{index .Config.Labels "AGENT_ID"}}'
|
|
58
|
+
], { encoding: 'utf8', timeout: 5000 }).trim();
|
|
59
|
+
return { containerName, agentId: agentId || containerName };
|
|
60
|
+
}).filter(Boolean);
|
|
61
|
+
} catch {
|
|
62
|
+
return [];
|
|
63
|
+
}
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
/**
|
|
67
|
+
* Read skill directories from a container agent's workspace via docker exec.
|
|
68
|
+
* Returns an array of skill names found.
|
|
69
|
+
*/
|
|
70
|
+
function readContainerSkillDirs(containerName) {
|
|
71
|
+
const workspaceSkillsPath = '/root/.openclaw/workspace/skills';
|
|
72
|
+
try {
|
|
73
|
+
// Check if the skills directory exists
|
|
74
|
+
execFileSync('docker', ['exec', containerName, 'test', '-d', workspaceSkillsPath], {
|
|
75
|
+
encoding: 'utf8', timeout: 5000
|
|
76
|
+
});
|
|
77
|
+
|
|
78
|
+
// List subdirectories (each is a potential skill)
|
|
79
|
+
const raw = execFileSync('docker', ['exec', containerName, 'ls', '-1', workspaceSkillsPath], {
|
|
80
|
+
encoding: 'utf8', timeout: 5000
|
|
81
|
+
}).trim();
|
|
82
|
+
|
|
83
|
+
if (!raw) return [];
|
|
84
|
+
|
|
85
|
+
return raw.split('\n').map(line => line.trim()).filter(Boolean);
|
|
86
|
+
} catch {
|
|
87
|
+
return [];
|
|
88
|
+
}
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
/**
|
|
92
|
+
* Read SKILL.md content from inside a container.
|
|
93
|
+
*/
|
|
94
|
+
function readContainerSkillMd(containerName, skillName) {
|
|
95
|
+
const filePath = `/root/.openclaw/workspace/skills/${skillName}/SKILL.md`;
|
|
96
|
+
try {
|
|
97
|
+
return execFileSync('docker', ['exec', containerName, 'cat', filePath], {
|
|
98
|
+
encoding: 'utf8', timeout: 5000
|
|
99
|
+
});
|
|
100
|
+
} catch {
|
|
101
|
+
return null;
|
|
102
|
+
}
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
/**
|
|
106
|
+
* Parse description and version from SKILL.md content string.
|
|
107
|
+
*/
|
|
108
|
+
function parseSkillContent(content) {
|
|
109
|
+
let description = '';
|
|
110
|
+
let version = '';
|
|
111
|
+
if (!content) return { description, version };
|
|
112
|
+
|
|
113
|
+
const descMatch = content.match(/^description:\s*(.+)$/m);
|
|
114
|
+
const verMatch = content.match(/^\*\*Version:\*\*\s*(.+)$/m) || content.match(/^version:\s*(.+)$/m);
|
|
115
|
+
if (descMatch) description = descMatch[1].trim();
|
|
116
|
+
if (verMatch) version = verMatch[1].trim();
|
|
117
|
+
|
|
118
|
+
return { description, version };
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
export async function GET(request) {
|
|
122
|
+
const denied = await requireAuthJWT(request); if (denied) return denied;
|
|
123
|
+
const skills = [];
|
|
124
|
+
|
|
125
|
+
// 1. Shared skills
|
|
126
|
+
try {
|
|
127
|
+
if (fs.existsSync(SHARED_SKILLS_DIR)) {
|
|
128
|
+
const entries = fs.readdirSync(SHARED_SKILLS_DIR, { withFileTypes: true });
|
|
129
|
+
for (const entry of entries) {
|
|
130
|
+
if (!entry.isDirectory()) continue;
|
|
131
|
+
const skillMd = path.join(SHARED_SKILLS_DIR, entry.name, 'SKILL.md');
|
|
132
|
+
if (fs.existsSync(skillMd)) {
|
|
133
|
+
skills.push(readSkillMeta(SHARED_SKILLS_DIR, entry.name, 'shared', null));
|
|
134
|
+
}
|
|
135
|
+
}
|
|
136
|
+
}
|
|
137
|
+
} catch {}
|
|
138
|
+
|
|
139
|
+
// 2. Agent container skills (workspace-local)
|
|
140
|
+
const agents = discoverAgentContainers();
|
|
141
|
+
for (const agent of agents) {
|
|
142
|
+
const dirs = readContainerSkillDirs(agent.containerName);
|
|
143
|
+
for (const dir of dirs) {
|
|
144
|
+
const content = readContainerSkillMd(agent.containerName, dir);
|
|
145
|
+
if (content) {
|
|
146
|
+
const { description, version } = parseSkillContent(content);
|
|
147
|
+
skills.push({
|
|
148
|
+
name: dir,
|
|
149
|
+
type: 'workspace',
|
|
150
|
+
agentId: agent.agentId,
|
|
151
|
+
containerName: agent.containerName,
|
|
152
|
+
path: `/root/.openclaw/workspace/skills/${dir}`,
|
|
153
|
+
skillMdPath: `/root/.openclaw/workspace/skills/${dir}/SKILL.md`,
|
|
154
|
+
hasSkillMd: true,
|
|
155
|
+
description,
|
|
156
|
+
version,
|
|
157
|
+
});
|
|
158
|
+
}
|
|
159
|
+
}
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
// 3. Bundled skills (read-only) — discover from any agent container
|
|
163
|
+
try {
|
|
164
|
+
let bundledDirNames = [];
|
|
165
|
+
let bundledSourceContainer = null;
|
|
166
|
+
|
|
167
|
+
if (agents.length > 0) {
|
|
168
|
+
for (const agent of agents) {
|
|
169
|
+
try {
|
|
170
|
+
const raw = execFileSync('docker', [
|
|
171
|
+
'exec', agent.containerName, 'ls', '-1', CONTAINER_BUNDLED_SKILLS_DIR
|
|
172
|
+
], { encoding: 'utf8', timeout: 5000 }).trim();
|
|
173
|
+
if (raw) {
|
|
174
|
+
bundledDirNames = raw.split('\n').map(l => l.trim()).filter(Boolean);
|
|
175
|
+
bundledSourceContainer = agent.containerName;
|
|
176
|
+
break;
|
|
177
|
+
}
|
|
178
|
+
} catch { /* try next container */ }
|
|
179
|
+
}
|
|
180
|
+
}
|
|
181
|
+
|
|
182
|
+
for (const name of bundledDirNames) {
|
|
183
|
+
try {
|
|
184
|
+
const skillMdPath = `${CONTAINER_BUNDLED_SKILLS_DIR}/${name}/SKILL.md`;
|
|
185
|
+
const content = execFileSync('docker', [
|
|
186
|
+
'exec', bundledSourceContainer, 'cat', skillMdPath
|
|
187
|
+
], { encoding: 'utf8', timeout: 5000 });
|
|
188
|
+
const { description, version } = parseSkillContent(content);
|
|
189
|
+
skills.push({
|
|
190
|
+
name,
|
|
191
|
+
type: 'bundled',
|
|
192
|
+
path: `${CONTAINER_BUNDLED_SKILLS_DIR}/${name}`,
|
|
193
|
+
skillMdPath,
|
|
194
|
+
hasSkillMd: true,
|
|
195
|
+
description,
|
|
196
|
+
version,
|
|
197
|
+
});
|
|
198
|
+
} catch { /* skip this skill */ }
|
|
199
|
+
}
|
|
200
|
+
} catch {}
|
|
201
|
+
|
|
202
|
+
return NextResponse.json({ skills });
|
|
203
|
+
}
|
|
@@ -0,0 +1,142 @@
|
|
|
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
|
+
const BUNDLED_SKILLS_DIR = '/usr/lib/node_modules/openclaw/skills';
|
|
9
|
+
|
|
10
|
+
/**
|
|
11
|
+
* Read SKILL.md content from various sources:
|
|
12
|
+
* - Host path (shared / bundled)
|
|
13
|
+
* - Container path (via docker exec)
|
|
14
|
+
*/
|
|
15
|
+
function readSkillContent(skillPath, containerName) {
|
|
16
|
+
if (containerName) {
|
|
17
|
+
// Container skill — read via docker exec
|
|
18
|
+
try {
|
|
19
|
+
return execFileSync('docker', ['exec', containerName, 'cat', skillPath], {
|
|
20
|
+
encoding: 'utf8', timeout: 5000
|
|
21
|
+
});
|
|
22
|
+
} catch {
|
|
23
|
+
throw Object.assign(new Error(`Skill not found in container ${containerName}`), { status: 404 });
|
|
24
|
+
}
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
// Host skill
|
|
28
|
+
if (!fs.existsSync(skillPath)) {
|
|
29
|
+
throw Object.assign(new Error('Skill not found'), { status: 404 });
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
return fs.readFileSync(skillPath, 'utf8');
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
/**
|
|
36
|
+
* Write SKILL.md content.
|
|
37
|
+
* Shared skills — write to host filesystem.
|
|
38
|
+
* Container workspace skills — write via docker exec.
|
|
39
|
+
*/
|
|
40
|
+
function writeSkillContent(skillPath, containerName, content) {
|
|
41
|
+
if (containerName) {
|
|
42
|
+
// Container skill — write via docker exec heredoc
|
|
43
|
+
const escapedContent = content
|
|
44
|
+
.replace(/\\/g, '\\\\')
|
|
45
|
+
.replace(/'/g, "'\\''");
|
|
46
|
+
execSync(
|
|
47
|
+
`docker exec ${containerName} sh -c "cat > '${skillPath}' << 'NEXUS_EOF'\n${escapedContent}\nNEXUS_EOF"`,
|
|
48
|
+
{ encoding: 'utf8', timeout: 10000 }
|
|
49
|
+
);
|
|
50
|
+
return;
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
// Host skill
|
|
54
|
+
fs.mkdirSync(path.dirname(skillPath), { recursive: true });
|
|
55
|
+
if (fs.existsSync(skillPath)) {
|
|
56
|
+
fs.copyFileSync(skillPath, skillPath + '.bak');
|
|
57
|
+
}
|
|
58
|
+
fs.writeFileSync(skillPath, content, 'utf8');
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
export async function POST(request) {
|
|
62
|
+
const denied = await requireAuthJWT(request);
|
|
63
|
+
if (denied) return denied;
|
|
64
|
+
|
|
65
|
+
try {
|
|
66
|
+
const { skillPath, content, containerName } = await request.json();
|
|
67
|
+
|
|
68
|
+
if (!skillPath || !content) {
|
|
69
|
+
return NextResponse.json({ error: 'Missing skillPath or content' }, { status: 400 });
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
// Validate path ends with SKILL.md
|
|
73
|
+
if (!skillPath.endsWith('SKILL.md')) {
|
|
74
|
+
return NextResponse.json({ error: 'Only SKILL.md files can be saved' }, { status: 403 });
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
// Determine if this is a shared, bundled, or container skill
|
|
78
|
+
const absPath = path.resolve(skillPath);
|
|
79
|
+
const absShared = path.resolve(SHARED_SKILLS_DIR);
|
|
80
|
+
const absBundled = path.resolve(BUNDLED_SKILLS_DIR);
|
|
81
|
+
|
|
82
|
+
const isShared = absPath.startsWith(absShared + path.sep) || absPath === absShared;
|
|
83
|
+
const isBundled = absPath.startsWith(absBundled + path.sep) || absPath === absBundled;
|
|
84
|
+
const isContainer = !!containerName;
|
|
85
|
+
|
|
86
|
+
if (isBundled) {
|
|
87
|
+
return NextResponse.json({ error: 'Bundled skills are read-only' }, { status: 403 });
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
if (!isShared && !isContainer) {
|
|
91
|
+
return NextResponse.json({ error: 'Path not in allowed roots and no containerName provided' }, { status: 403 });
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
writeSkillContent(isContainer ? skillPath : absPath, containerName || null, content);
|
|
95
|
+
|
|
96
|
+
return NextResponse.json({ ok: true, saved: isContainer ? skillPath : absPath });
|
|
97
|
+
} catch (err) {
|
|
98
|
+
const msg = err instanceof Error ? err.message : 'Internal error';
|
|
99
|
+
const status = err.status || 500;
|
|
100
|
+
return NextResponse.json({ error: msg }, { status });
|
|
101
|
+
}
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
export async function GET(request) {
|
|
105
|
+
const denied = await requireAuthJWT(request);
|
|
106
|
+
if (denied) return denied;
|
|
107
|
+
|
|
108
|
+
const { searchParams } = new URL(request.url);
|
|
109
|
+
const skillPath = searchParams.get('path');
|
|
110
|
+
const containerName = searchParams.get('containerName');
|
|
111
|
+
|
|
112
|
+
if (!skillPath) {
|
|
113
|
+
return NextResponse.json({ error: 'Missing path' }, { status: 400 });
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
try {
|
|
117
|
+
if (containerName) {
|
|
118
|
+
// Read from container via docker exec
|
|
119
|
+
const content = readSkillContent(skillPath, containerName);
|
|
120
|
+
return NextResponse.json({ content });
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
// Read from host path
|
|
124
|
+
const absPath = path.resolve(skillPath);
|
|
125
|
+
const absShared = path.resolve(SHARED_SKILLS_DIR);
|
|
126
|
+
const absBundled = path.resolve(BUNDLED_SKILLS_DIR);
|
|
127
|
+
|
|
128
|
+
const isShared = absPath.startsWith(absShared + path.sep) || absPath === absShared;
|
|
129
|
+
const isBundled = absPath.startsWith(absBundled + path.sep) || absPath === absBundled;
|
|
130
|
+
|
|
131
|
+
if (!isShared && !isBundled) {
|
|
132
|
+
return NextResponse.json({ error: 'Host path not in allowed skill roots' }, { status: 403 });
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
const content = readSkillContent(absPath, null);
|
|
136
|
+
return NextResponse.json({ content });
|
|
137
|
+
} catch (err) {
|
|
138
|
+
const msg = err instanceof Error ? err.message : 'Internal error';
|
|
139
|
+
const status = err.status || 404;
|
|
140
|
+
return NextResponse.json({ error: msg }, { status });
|
|
141
|
+
}
|
|
142
|
+
}
|
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
import { NextResponse, type NextRequest } from 'next/server';
|
|
2
|
+
import { openDb } from '@/lib/db';
|
|
3
|
+
import { requireAuthJWT } from '@/lib/rev4a-auth';
|
|
4
|
+
|
|
5
|
+
export const dynamic = 'force-dynamic';
|
|
6
|
+
|
|
7
|
+
export async function GET(request: NextRequest): Promise<NextResponse> {
|
|
8
|
+
const denied = await requireAuthJWT(request); if (denied) return denied as any;
|
|
9
|
+
try {
|
|
10
|
+
const db = openDb();
|
|
11
|
+
const now = new Date();
|
|
12
|
+
const startOfMonth = Math.floor(
|
|
13
|
+
new Date(now.getFullYear(), now.getMonth(), 1).getTime() / 1000,
|
|
14
|
+
);
|
|
15
|
+
const total = db
|
|
16
|
+
.prepare(
|
|
17
|
+
`SELECT COALESCE(SUM(cost_usd),0) as total, COALESCE(SUM(tokens_in),0) as total_in,
|
|
18
|
+
COALESCE(SUM(tokens_out),0) as total_out, COUNT(*) as sessions
|
|
19
|
+
FROM sessions WHERE started_at >= ?`,
|
|
20
|
+
)
|
|
21
|
+
.get(startOfMonth);
|
|
22
|
+
const byModel = db
|
|
23
|
+
.prepare(
|
|
24
|
+
`SELECT model, COALESCE(SUM(cost_usd),0) as cost,
|
|
25
|
+
COALESCE(SUM(tokens_in),0) as tokens_in, COALESCE(SUM(tokens_out),0) as tokens_out,
|
|
26
|
+
COUNT(*) as sessions FROM sessions WHERE started_at >= ? GROUP BY model ORDER BY cost DESC`,
|
|
27
|
+
)
|
|
28
|
+
.all(startOfMonth);
|
|
29
|
+
db.close();
|
|
30
|
+
return NextResponse.json({ total, byModel });
|
|
31
|
+
} catch (e: unknown) {
|
|
32
|
+
return NextResponse.json({ error: (e as Error).message }, { status: 500 });
|
|
33
|
+
}
|
|
34
|
+
}
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
import { NextResponse, type NextRequest } from 'next/server';
|
|
2
|
+
import { openDb } from '@/lib/db';
|
|
3
|
+
import { requireAuthJWT } from '@/lib/rev4a-auth';
|
|
4
|
+
|
|
5
|
+
export const dynamic = 'force-dynamic';
|
|
6
|
+
|
|
7
|
+
export async function GET(request: NextRequest): Promise<NextResponse> {
|
|
8
|
+
const denied = await requireAuthJWT(request); if (denied) return denied as any;
|
|
9
|
+
const ts = Number.parseInt(new URL(request.url).searchParams.get('ts') ?? '0', 10);
|
|
10
|
+
try {
|
|
11
|
+
const db = openDb();
|
|
12
|
+
const row = db
|
|
13
|
+
.prepare('SELECT COALESCE(SUM(cost_usd),0) as total FROM sessions WHERE started_at >= ?')
|
|
14
|
+
.get(Math.floor(ts / 1000)) as { total: number } | undefined;
|
|
15
|
+
db.close();
|
|
16
|
+
return NextResponse.json({ total: row?.total ?? 0 });
|
|
17
|
+
} catch (e: unknown) {
|
|
18
|
+
return NextResponse.json({ error: (e as Error).message }, { status: 500 });
|
|
19
|
+
}
|
|
20
|
+
}
|
|
@@ -0,0 +1,70 @@
|
|
|
1
|
+
import { type NextRequest } from 'next/server';
|
|
2
|
+
import { DB_PATH } from '@/lib/db';
|
|
3
|
+
import Database from 'better-sqlite3';
|
|
4
|
+
import { requireAuthJWT } from '@/lib/rev4a-auth';
|
|
5
|
+
|
|
6
|
+
export const dynamic = 'force-dynamic';
|
|
7
|
+
|
|
8
|
+
export async function GET(request: NextRequest): Promise<Response> {
|
|
9
|
+
const denied = await requireAuthJWT(request); if (denied) return denied as any;
|
|
10
|
+
let lastEventId = 0;
|
|
11
|
+
const encoder = new TextEncoder();
|
|
12
|
+
|
|
13
|
+
const stream = new ReadableStream({
|
|
14
|
+
start(controller) {
|
|
15
|
+
const send = (data: unknown) => {
|
|
16
|
+
controller.enqueue(encoder.encode(`data: ${JSON.stringify(data)}\n\n`));
|
|
17
|
+
};
|
|
18
|
+
|
|
19
|
+
const poll = () => {
|
|
20
|
+
try {
|
|
21
|
+
const db = new Database(DB_PATH, { readonly: true });
|
|
22
|
+
const newEvents = db
|
|
23
|
+
.prepare(
|
|
24
|
+
`SELECT e.*, s.label AS session_label FROM events e
|
|
25
|
+
LEFT JOIN sessions s ON e.session_id = s.session_id
|
|
26
|
+
WHERE e.id > ? ORDER BY e.ts ASC LIMIT 20`,
|
|
27
|
+
)
|
|
28
|
+
.all(lastEventId) as { id: number }[];
|
|
29
|
+
const sessions = db
|
|
30
|
+
.prepare(`SELECT s.*, l.label AS lineage_label, l.agent_name AS lineage_agent_name
|
|
31
|
+
FROM sessions s
|
|
32
|
+
LEFT JOIN lineage l ON s.session_id = l.child_id
|
|
33
|
+
ORDER BY s.started_at DESC LIMIT 100`)
|
|
34
|
+
.all();
|
|
35
|
+
const costs = db
|
|
36
|
+
.prepare('SELECT SUM(cost_usd) AS today FROM sessions WHERE started_at >= ?')
|
|
37
|
+
.get(Math.floor(new Date().setHours(0, 0, 0, 0) / 1000)) as
|
|
38
|
+
| { today: number | null }
|
|
39
|
+
| undefined;
|
|
40
|
+
db.close();
|
|
41
|
+
|
|
42
|
+
if (newEvents.length > 0) {
|
|
43
|
+
lastEventId = newEvents[newEvents.length - 1].id;
|
|
44
|
+
send({ type: 'events', data: newEvents });
|
|
45
|
+
}
|
|
46
|
+
send({ type: 'sessions', data: sessions });
|
|
47
|
+
send({ type: 'costs', data: { today: costs?.today ?? 0 } });
|
|
48
|
+
} catch {
|
|
49
|
+
// Ignore DB read errors to keep stream alive.
|
|
50
|
+
}
|
|
51
|
+
};
|
|
52
|
+
|
|
53
|
+
poll();
|
|
54
|
+
const interval = setInterval(poll, 5000);
|
|
55
|
+
|
|
56
|
+
request.signal.addEventListener('abort', () => {
|
|
57
|
+
clearInterval(interval);
|
|
58
|
+
controller.close();
|
|
59
|
+
});
|
|
60
|
+
},
|
|
61
|
+
});
|
|
62
|
+
|
|
63
|
+
return new Response(stream, {
|
|
64
|
+
headers: {
|
|
65
|
+
'Content-Type': 'text/event-stream',
|
|
66
|
+
'Cache-Control': 'no-cache',
|
|
67
|
+
Connection: 'keep-alive',
|
|
68
|
+
},
|
|
69
|
+
});
|
|
70
|
+
}
|