@flame0510/project-aether 1.1.11 → 1.1.13
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 +4 -1
- package/app/agents/PageClient.tsx +84 -22
- package/app/api/agents/create/route.ts +15 -3
- package/app/api/agents/download-image/route.ts +2 -0
- package/app/api/agents/image-status/route.ts +2 -0
- package/app/api/agents/route.ts +1 -36
- package/app/api/agents/token/route.ts +4 -1
- package/app/api/auth/check/route.ts +2 -23
- package/app/api/auth/login/route.ts +3 -35
- package/app/api/auth/logout/route.ts +1 -1
- package/app/api/auth/status/route.ts +2 -21
- package/app/api/config/env/route.ts +7 -3
- package/app/api/config/restart/route.ts +2 -0
- package/app/api/containers/route.ts +3 -34
- package/app/api/crons/route.ts +2 -0
- package/app/api/debug/route.ts +2 -0
- package/app/api/envcheck/route.ts +3 -1
- package/app/api/gateway/provider/route.ts +43 -5
- package/app/api/gateway/route.ts +2 -0
- package/app/api/models/route.ts +2 -0
- package/app/api/setup/agent-image/route.ts +8 -3
- package/app/api/setup/password/route.ts +32 -37
- package/app/api/setup/restart/route.ts +8 -2
- package/app/api/skills/delete/route.js +1 -1
- package/app/api/skills/promote/route.js +1 -1
- package/app/api/skills/route.js +1 -1
- package/app/api/skills/save/route.js +18 -5
- package/app/api/update-check/route.ts +4 -23
- package/app/api/wizard/complete/route.ts +7 -3
- package/app/api/wizard/reset/route.ts +8 -3
- package/app/api/wizard/status/route.ts +8 -5
- package/app/components/AuthGuard.tsx +29 -8
- package/app/components/ui/ModalityIcons.tsx +157 -0
- package/app/components/ui/index.ts +1 -0
- package/app/gateway/PageClient.tsx +104 -16
- package/bin/rev4a.js +35 -24
- package/docs/ARCHITECTURE.md +31 -13
- package/docs/FRONTEND-ARCHITECTURE.md +1 -0
- package/docs/REV4A.md +36 -6
- package/docs/dev/API-REFERENCE.md +9 -4
- package/docs/dev/GATEWAY.md +42 -1
- package/docs/dev/PROVIDERS.md +4 -3
- package/docs/rag/REV4A-OVERVIEW.md +1 -1
- package/instrumentation.ts +18 -0
- package/lib/agent-setup.ts +8 -9
- package/lib/buildAgentImage.ts +5 -1
- package/lib/db.ts +0 -10
- package/lib/docker-socket.ts +121 -0
- package/lib/openrouter-pricing.ts +184 -0
- package/lib/rev4a-auth.d.ts +4 -1
- package/lib/rev4a-auth.js +68 -20
- package/lib/rev4a-paths.ts +19 -0
- package/model-pricing.json +50 -50
- package/models.config.json +2 -2
- package/package.json +4 -1
- package/proxy.ts +68 -0
- package/scripts/refresh-model-pricing.mjs +171 -0
- package/lib/auth.ts +0 -28
- package/lib/requireAuth.tsx +0 -38
|
@@ -0,0 +1,157 @@
|
|
|
1
|
+
'use client';
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Modality badges for a model.
|
|
5
|
+
*
|
|
6
|
+
* Reads the `modality` string that ships in models.config.json (and matches
|
|
7
|
+
* OpenRouter's `architecture.modality` format verbatim), e.g.
|
|
8
|
+
* "text+image+file->text" → accepts text, images, files
|
|
9
|
+
* "text+image+file+audio+video->text+image" → also *produces* images
|
|
10
|
+
*
|
|
11
|
+
* Inputs are shown as icons; a non-text output is called out separately,
|
|
12
|
+
* since "can read an image" and "can generate an image" are very different
|
|
13
|
+
* capabilities and collapsing them would be misleading.
|
|
14
|
+
*/
|
|
15
|
+
|
|
16
|
+
interface ModalitySpec {
|
|
17
|
+
label: string;
|
|
18
|
+
icon: React.ReactNode;
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
const ICONS: Record<string, ModalitySpec> = {
|
|
22
|
+
text: {
|
|
23
|
+
label: 'Text',
|
|
24
|
+
icon: (
|
|
25
|
+
<svg viewBox="0 0 16 16" width="12" height="12" fill="none" stroke="currentColor" strokeWidth="1.4" strokeLinecap="round">
|
|
26
|
+
<path d="M3 4h10M3 8h10M3 12h6" />
|
|
27
|
+
</svg>
|
|
28
|
+
),
|
|
29
|
+
},
|
|
30
|
+
image: {
|
|
31
|
+
label: 'Images',
|
|
32
|
+
icon: (
|
|
33
|
+
<svg viewBox="0 0 16 16" width="12" height="12" fill="none" stroke="currentColor" strokeWidth="1.4">
|
|
34
|
+
<rect x="2" y="3" width="12" height="10" rx="1.5" />
|
|
35
|
+
<circle cx="6" cy="6.5" r="1.2" />
|
|
36
|
+
<path d="M3 11.5l3.2-3 2.3 2.2 2-1.8 2.5 2.6" strokeLinecap="round" strokeLinejoin="round" />
|
|
37
|
+
</svg>
|
|
38
|
+
),
|
|
39
|
+
},
|
|
40
|
+
file: {
|
|
41
|
+
label: 'Files / PDF',
|
|
42
|
+
icon: (
|
|
43
|
+
<svg viewBox="0 0 16 16" width="12" height="12" fill="none" stroke="currentColor" strokeWidth="1.4" strokeLinejoin="round">
|
|
44
|
+
<path d="M9 2H4.5A1.5 1.5 0 0 0 3 3.5v9A1.5 1.5 0 0 0 4.5 14h7a1.5 1.5 0 0 0 1.5-1.5V6z" />
|
|
45
|
+
<path d="M9 2v4h4" />
|
|
46
|
+
</svg>
|
|
47
|
+
),
|
|
48
|
+
},
|
|
49
|
+
audio: {
|
|
50
|
+
label: 'Audio',
|
|
51
|
+
icon: (
|
|
52
|
+
<svg viewBox="0 0 16 16" width="12" height="12" fill="none" stroke="currentColor" strokeWidth="1.4" strokeLinecap="round">
|
|
53
|
+
<path d="M8 2.5v11" />
|
|
54
|
+
<path d="M5 5.5v5M11 5.5v5" />
|
|
55
|
+
<path d="M2.5 7.5v1M13.5 7.5v1" />
|
|
56
|
+
</svg>
|
|
57
|
+
),
|
|
58
|
+
},
|
|
59
|
+
video: {
|
|
60
|
+
label: 'Video',
|
|
61
|
+
icon: (
|
|
62
|
+
<svg viewBox="0 0 16 16" width="12" height="12" fill="none" stroke="currentColor" strokeWidth="1.4" strokeLinejoin="round">
|
|
63
|
+
<rect x="2" y="4" width="8.5" height="8" rx="1.5" />
|
|
64
|
+
<path d="M10.5 8l3.5-2v6l-3.5-2z" />
|
|
65
|
+
</svg>
|
|
66
|
+
),
|
|
67
|
+
},
|
|
68
|
+
};
|
|
69
|
+
|
|
70
|
+
export interface ParsedModality {
|
|
71
|
+
inputs: string[];
|
|
72
|
+
outputs: string[];
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
/** Split "text+image->text+image" into its input and output lists. */
|
|
76
|
+
export function parseModality(modality: string | undefined | null): ParsedModality | null {
|
|
77
|
+
if (!modality || typeof modality !== 'string') return null;
|
|
78
|
+
const [rawIn, rawOut] = modality.split('->');
|
|
79
|
+
if (!rawIn) return null;
|
|
80
|
+
const split = (s: string) => s.split('+').map((p) => p.trim()).filter(Boolean);
|
|
81
|
+
return { inputs: split(rawIn), outputs: rawOut ? split(rawOut) : [] };
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
/** Own-property lookup: a token like "constructor" must not resolve to Object.prototype. */
|
|
85
|
+
function iconFor(token: string): ModalitySpec | null {
|
|
86
|
+
return Object.hasOwn(ICONS, token) ? ICONS[token] : null;
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
interface ModalityIconsProps {
|
|
90
|
+
modality?: string | null;
|
|
91
|
+
/**
|
|
92
|
+
* Router models (OpenRouter Auto) advertise the union of everything they
|
|
93
|
+
* might route to, which reads as a guarantee it isn't. They already show
|
|
94
|
+
* "Dynamic" for price; capabilities get the same treatment rather than
|
|
95
|
+
* concrete icons.
|
|
96
|
+
*/
|
|
97
|
+
dynamic?: boolean;
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
export default function ModalityIcons({ modality, dynamic }: ModalityIconsProps) {
|
|
101
|
+
if (dynamic) {
|
|
102
|
+
return (
|
|
103
|
+
<span
|
|
104
|
+
title="Routed to a different model per request — capabilities vary"
|
|
105
|
+
style={{ fontSize: 10, color: 'var(--text-dim)', fontStyle: 'italic' }}
|
|
106
|
+
>
|
|
107
|
+
varies
|
|
108
|
+
</span>
|
|
109
|
+
);
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
const parsed = parseModality(modality);
|
|
113
|
+
if (!parsed) return null;
|
|
114
|
+
|
|
115
|
+
// Anything the model can produce beyond plain text is worth surfacing:
|
|
116
|
+
// only 15 of 422 upstream models do, and "reads images" vs "makes images"
|
|
117
|
+
// are very different capabilities.
|
|
118
|
+
const generates = parsed.outputs.filter((o) => o !== 'text');
|
|
119
|
+
|
|
120
|
+
return (
|
|
121
|
+
<span style={{ display: 'inline-flex', alignItems: 'center', gap: 5, verticalAlign: 'middle' }}>
|
|
122
|
+
{parsed.inputs.map((input) => {
|
|
123
|
+
const spec = iconFor(input);
|
|
124
|
+
if (!spec) return null;
|
|
125
|
+
return (
|
|
126
|
+
<span
|
|
127
|
+
key={input}
|
|
128
|
+
title={`Accepts ${spec.label.toLowerCase()}`}
|
|
129
|
+
aria-label={`Accepts ${spec.label.toLowerCase()}`}
|
|
130
|
+
style={{ display: 'inline-flex', color: 'var(--text-dim)', opacity: 0.75 }}
|
|
131
|
+
>
|
|
132
|
+
{spec.icon}
|
|
133
|
+
</span>
|
|
134
|
+
);
|
|
135
|
+
})}
|
|
136
|
+
|
|
137
|
+
{generates.map((out) => {
|
|
138
|
+
const spec = iconFor(out);
|
|
139
|
+
if (!spec) return null;
|
|
140
|
+
return (
|
|
141
|
+
<span
|
|
142
|
+
key={`out-${out}`}
|
|
143
|
+
title={`Generates ${spec.label.toLowerCase()}`}
|
|
144
|
+
aria-label={`Generates ${spec.label.toLowerCase()}`}
|
|
145
|
+
style={{
|
|
146
|
+
display: 'inline-flex', alignItems: 'center', gap: 2,
|
|
147
|
+
color: 'var(--violet)', fontSize: 9, fontWeight: 600,
|
|
148
|
+
}}
|
|
149
|
+
>
|
|
150
|
+
<span aria-hidden="true">→</span>
|
|
151
|
+
{spec.icon}
|
|
152
|
+
</span>
|
|
153
|
+
);
|
|
154
|
+
})}
|
|
155
|
+
</span>
|
|
156
|
+
);
|
|
157
|
+
}
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
'use client';
|
|
2
2
|
|
|
3
3
|
import { useCallback, useEffect, useRef, useState } from 'react';
|
|
4
|
-
import { Button, ConfirmModal, LoadingSpinner, Metric, Page, PageHeader, Pill, Surface, Tabs, type Tone } from '../components/ui';
|
|
4
|
+
import { Button, ConfirmModal, Input, LoadingSpinner, Metric, ModalityIcons, Page, PageHeader, Pill, Surface, Tabs, type Tone } from '../components/ui';
|
|
5
5
|
import PasswordInput from '../components/PasswordInput';
|
|
6
6
|
import { Skeleton } from '../components/Skeleton';
|
|
7
7
|
|
|
@@ -14,6 +14,12 @@ interface ModelInfo {
|
|
|
14
14
|
id: string;
|
|
15
15
|
name: string;
|
|
16
16
|
enabled: boolean;
|
|
17
|
+
/** USD per 1M tokens. -1/-1 means the model routes dynamically. */
|
|
18
|
+
pricing?: { input: number; output: number };
|
|
19
|
+
/** Set when the price came from the live OpenRouter catalogue, not the static file. */
|
|
20
|
+
pricingLive?: boolean;
|
|
21
|
+
/** e.g. "text+image+file->text" — curated, matches what sync.ts provisions. */
|
|
22
|
+
modality?: string;
|
|
17
23
|
}
|
|
18
24
|
|
|
19
25
|
interface GatewayProvider {
|
|
@@ -345,6 +351,9 @@ export default function GatewayPageClient() {
|
|
|
345
351
|
const [revealedKeys, setRevealedKeys] = useState<Record<string, string | null>>({});
|
|
346
352
|
const [revealing, setRevealing] = useState<Record<string, boolean>>({});
|
|
347
353
|
|
|
354
|
+
// Per-provider model filter (purely local, no requests)
|
|
355
|
+
const [modelSearch, setModelSearch] = useState<Record<string, string>>({});
|
|
356
|
+
|
|
348
357
|
// Agent edit state
|
|
349
358
|
const [editingAgent, setEditingAgent] = useState<string | null>(null);
|
|
350
359
|
const [agentModelInput, setAgentModelInput] = useState('');
|
|
@@ -431,6 +440,24 @@ export default function GatewayPageClient() {
|
|
|
431
440
|
return `${f(p.input)} / ${f(p.output)}`;
|
|
432
441
|
}
|
|
433
442
|
|
|
443
|
+
/**
|
|
444
|
+
* Local model filter. Matches against both the display name and the id,
|
|
445
|
+
* because the upstream vendor often only appears in one of them — e.g.
|
|
446
|
+
* "Kimi K2 (OR)" has id `openrouter/moonshotai/kimi-k2`, so searching
|
|
447
|
+
* either "kimi" or "moonshot" has to find it.
|
|
448
|
+
*
|
|
449
|
+
* Every whitespace-separated term must match somewhere, so "kimi code"
|
|
450
|
+
* narrows rather than widening.
|
|
451
|
+
*/
|
|
452
|
+
function filterModels<T extends { id: string; name: string }>(models: T[], query: string): T[] {
|
|
453
|
+
const terms = query.toLowerCase().split(/\s+/).filter(Boolean);
|
|
454
|
+
if (terms.length === 0) return models;
|
|
455
|
+
return models.filter((m) => {
|
|
456
|
+
const haystack = `${m.name} ${m.id}`.toLowerCase();
|
|
457
|
+
return terms.every((t) => haystack.includes(t));
|
|
458
|
+
});
|
|
459
|
+
}
|
|
460
|
+
|
|
434
461
|
/* ---- Overlay helper ---- */
|
|
435
462
|
async function withOverlay<T>(message: string, fn: () => Promise<T>): Promise<T> {
|
|
436
463
|
setOverlay({ active: true, message });
|
|
@@ -904,33 +931,93 @@ export default function GatewayPageClient() {
|
|
|
904
931
|
)}
|
|
905
932
|
|
|
906
933
|
{/* Models with checkboxes */}
|
|
907
|
-
{p.models.length > 0 && (
|
|
934
|
+
{p.models.length > 0 && (() => {
|
|
935
|
+
const query = modelSearch[p.provider] || '';
|
|
936
|
+
const visibleModels = filterModels(p.models, query);
|
|
937
|
+
// Below this many, everything fits on screen and a
|
|
938
|
+
// search box is just noise.
|
|
939
|
+
const showSearch = p.models.length > 6;
|
|
940
|
+
return (
|
|
908
941
|
<div style={{ borderTop: '1px solid var(--border)', paddingTop: 12 }}>
|
|
909
|
-
<div style={{
|
|
910
|
-
|
|
911
|
-
|
|
912
|
-
|
|
913
|
-
|
|
942
|
+
<div style={{ display: 'flex', alignItems: 'baseline', justifyContent: 'space-between', gap: 8, marginBottom: 8, flexWrap: 'wrap' }}>
|
|
943
|
+
<div style={{ fontSize: 11, color: 'var(--text-dim)', textTransform: 'uppercase', letterSpacing: '0.06em' }}>
|
|
944
|
+
Models
|
|
945
|
+
<span style={{ marginLeft: 8, textTransform: 'none', letterSpacing: 'normal', color: 'var(--violet)', opacity: 0.7, fontSize: 10 }}>
|
|
946
|
+
in / out per 1M tokens
|
|
947
|
+
</span>
|
|
948
|
+
</div>
|
|
949
|
+
{query && (
|
|
950
|
+
<span style={{ fontSize: 10, color: 'var(--text-dim)' }}>
|
|
951
|
+
{visibleModels.length} of {p.models.length}
|
|
952
|
+
</span>
|
|
953
|
+
)}
|
|
914
954
|
</div>
|
|
955
|
+
|
|
956
|
+
{showSearch && (
|
|
957
|
+
<div style={{ marginBottom: 8 }}>
|
|
958
|
+
{/* Deliberately type="text": Safari clears type="search"
|
|
959
|
+
on Escape via a `search` event React never sees,
|
|
960
|
+
leaving an empty box over a still-filtered list. */}
|
|
961
|
+
<Input
|
|
962
|
+
type="text"
|
|
963
|
+
value={query}
|
|
964
|
+
placeholder="Filter by name or provider — e.g. kimi, moonshot, flash"
|
|
965
|
+
aria-label={`Filter ${p.label} models`}
|
|
966
|
+
onChange={(e) => setModelSearch((prev) => ({ ...prev, [p.provider]: e.target.value }))}
|
|
967
|
+
/>
|
|
968
|
+
</div>
|
|
969
|
+
)}
|
|
970
|
+
|
|
915
971
|
<div style={{ display: 'grid', gap: 4, maxHeight: 360, overflowY: 'auto' }}>
|
|
916
|
-
{
|
|
972
|
+
{visibleModels.length === 0 && (
|
|
973
|
+
<div style={{ fontSize: 12, color: 'var(--text-dim)', padding: '8px 4px' }}>
|
|
974
|
+
No model matches “{query}”.
|
|
975
|
+
</div>
|
|
976
|
+
)}
|
|
977
|
+
{visibleModels.map((m) => {
|
|
917
978
|
const modelMsg = messages[`model-${m.id}`];
|
|
979
|
+
const pricing = m.pricing;
|
|
980
|
+
// -1/-1 is OpenRouter's marker for router models,
|
|
981
|
+
// whose price and capabilities both vary per call.
|
|
982
|
+
const isDynamic = pricing?.input === -1 && pricing?.output === -1;
|
|
918
983
|
return (
|
|
919
|
-
<label
|
|
920
|
-
|
|
984
|
+
// Only the name/id are inside the <label>: clicking
|
|
985
|
+
// anywhere in a label toggles its checkbox, and the
|
|
986
|
+
// badges below carry tooltips people will click to
|
|
987
|
+
// read — which would silently flip the model and
|
|
988
|
+
// restart every agent container.
|
|
989
|
+
<div key={m.id} style={{
|
|
990
|
+
display: 'flex', alignItems: 'center', gap: 10, padding: '5px 8px',
|
|
921
991
|
background: m.enabled ? 'rgba(var(--green-rgb,34,197,94),0.06)' : 'transparent',
|
|
922
|
-
|
|
992
|
+
fontSize: 12,
|
|
923
993
|
}}>
|
|
924
994
|
<input type="checkbox" checked={m.enabled} disabled={overlay.active}
|
|
995
|
+
id={`model-${m.id}`}
|
|
925
996
|
onChange={(e) => handleToggleModel(m.id, e.target.checked)}
|
|
926
997
|
style={{ accentColor: 'var(--green)', cursor: 'pointer' }} />
|
|
927
|
-
<div style={{ flex: 1 }}>
|
|
928
|
-
<div style={{
|
|
998
|
+
<div style={{ flex: 1, minWidth: 0 }}>
|
|
999
|
+
<div style={{ display: 'flex', alignItems: 'center', gap: 8, flexWrap: 'wrap' }}>
|
|
1000
|
+
<label
|
|
1001
|
+
htmlFor={`model-${m.id}`}
|
|
1002
|
+
style={{ fontWeight: m.enabled ? 500 : 400, cursor: overlay.active ? 'wait' : 'pointer' }}
|
|
1003
|
+
>
|
|
1004
|
+
{m.name}
|
|
1005
|
+
</label>
|
|
1006
|
+
<ModalityIcons modality={m.modality} dynamic={isDynamic} />
|
|
1007
|
+
</div>
|
|
929
1008
|
<div style={{ fontSize: 11, color: 'var(--text-dim)', fontFamily: 'monospace' }}>
|
|
930
1009
|
{m.id}
|
|
931
1010
|
<span style={{ marginLeft: 8, color: 'var(--violet)', fontWeight: 500 }}>
|
|
932
|
-
{fmtPrice(
|
|
1011
|
+
{fmtPrice(pricing)}
|
|
933
1012
|
</span>
|
|
1013
|
+
{m.pricingLive && (
|
|
1014
|
+
<span
|
|
1015
|
+
title="Live price from the OpenRouter API"
|
|
1016
|
+
style={{ marginLeft: 6, display: 'inline-block', verticalAlign: 'middle' }}
|
|
1017
|
+
>
|
|
1018
|
+
<Pill tone="success">LIVE</Pill>
|
|
1019
|
+
</span>
|
|
1020
|
+
)}
|
|
934
1021
|
</div>
|
|
935
1022
|
</div>
|
|
936
1023
|
{modelMsg && (
|
|
@@ -938,12 +1025,13 @@ export default function GatewayPageClient() {
|
|
|
938
1025
|
{modelMsg.text}
|
|
939
1026
|
</span>
|
|
940
1027
|
)}
|
|
941
|
-
</
|
|
1028
|
+
</div>
|
|
942
1029
|
);
|
|
943
1030
|
})}
|
|
944
1031
|
</div>
|
|
945
1032
|
</div>
|
|
946
|
-
|
|
1033
|
+
);
|
|
1034
|
+
})()}
|
|
947
1035
|
|
|
948
1036
|
{msg && (
|
|
949
1037
|
<div className={msg.type === 'success' ? 'msg-success' : 'msg-error'}>
|
package/bin/rev4a.js
CHANGED
|
@@ -29,8 +29,14 @@ const os = require('os');
|
|
|
29
29
|
const ROOT = join(__dirname, '..');
|
|
30
30
|
const NEXT_DIR = join(ROOT, '.next');
|
|
31
31
|
const PKG_JSON = join(ROOT, 'package.json');
|
|
32
|
-
// Config lives outside the npm package so it survives npm update / reinstall
|
|
33
|
-
|
|
32
|
+
// Config lives outside the npm package so it survives npm update / reinstall.
|
|
33
|
+
// REV4A_DATA_DIR must be honoured here exactly as rev4aDataDir() honours it in
|
|
34
|
+
// lib/rev4a-paths.ts: this file creates the shared directories that the app then
|
|
35
|
+
// bind-mounts into agents, so a divergence would have serve writing the system
|
|
36
|
+
// rules to one path while every agent mounted another — silently empty.
|
|
37
|
+
// It can only come from the real process environment, never from the .env file:
|
|
38
|
+
// that file is looked up under this very directory.
|
|
39
|
+
const CONFIG_DIR = process.env.REV4A_DATA_DIR || join(os.homedir(), '.config', 'rev4a');
|
|
34
40
|
const DATA_DIR = join(CONFIG_DIR, 'data');
|
|
35
41
|
const ENV_SOURCE = join(CONFIG_DIR, '.env');
|
|
36
42
|
const ENV_LINK = join(ROOT, '.env');
|
|
@@ -213,14 +219,26 @@ function ensureNetwork() {
|
|
|
213
219
|
// ── Rev4a system rules ───────────────────────────────────────────────────
|
|
214
220
|
|
|
215
221
|
function ensureRev4aRules() {
|
|
216
|
-
//
|
|
217
|
-
//
|
|
222
|
+
// These must mirror SHARED_SKILLS_DIR / REV4A_RULES_DIR in lib/rev4a-paths.ts.
|
|
223
|
+
// This file is CommonJS and runs before .next exists (it is what builds it),
|
|
224
|
+
// so it cannot import from lib/ — same reason CONFIG_DIR is recomputed above.
|
|
225
|
+
//
|
|
226
|
+
// It previously tried `require('.next/server/lib/agent-setup.js')` to reuse
|
|
227
|
+
// ensureHostDirs(). Next bundles route code and never emits that path, so the
|
|
228
|
+
// require always threw into an empty catch and the directories were never
|
|
229
|
+
// created here at all.
|
|
230
|
+
const sharedDir = join(CONFIG_DIR, 'shared');
|
|
231
|
+
const sharedSkillsDir = join(sharedDir, 'shared-skills');
|
|
232
|
+
const rev4aRulesDir = join(sharedDir, 'rev4a-rules');
|
|
233
|
+
|
|
218
234
|
try {
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
} catch {
|
|
235
|
+
mkdirSync(sharedSkillsDir, { recursive: true });
|
|
236
|
+
mkdirSync(rev4aRulesDir, { recursive: true });
|
|
237
|
+
} catch (err) {
|
|
238
|
+
log('RULES', `⚠ Could not create shared directories: ${err.message}`);
|
|
239
|
+
return;
|
|
240
|
+
}
|
|
222
241
|
|
|
223
|
-
const rev4aRulesDir = '/docker/rev4a-rules';
|
|
224
242
|
const ruleFile = join(rev4aRulesDir, 'AGENTS.md');
|
|
225
243
|
const source = join(ROOT, 'rev4a-rules', 'AGENTS.md');
|
|
226
244
|
|
|
@@ -241,14 +259,10 @@ function ensureRev4aRules() {
|
|
|
241
259
|
}
|
|
242
260
|
|
|
243
261
|
try {
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
}
|
|
248
|
-
const tmpFile = `/tmp/rev4a-rules-${Date.now()}.md`;
|
|
249
|
-
writeFileSync(tmpFile, sourceContent, 'utf-8');
|
|
250
|
-
execSync(`${sudo}cp ${tmpFile} ${ruleFile} && ${sudo}rm ${tmpFile}`);
|
|
251
|
-
log('RULES', 'Deployed rev4a system rules to /docker/rev4a-rules/');
|
|
262
|
+
// Direct write — the old /tmp + `sudo cp` + `sudo rm` dance was only
|
|
263
|
+
// needed because the destination sat outside the user's home.
|
|
264
|
+
writeFileSync(ruleFile, sourceContent, 'utf-8');
|
|
265
|
+
log('RULES', `Deployed rev4a system rules to ${rev4aRulesDir}/`);
|
|
252
266
|
} catch (err) {
|
|
253
267
|
log('RULES', `⚠ Failed to deploy rev4a rules: ${err.message}`);
|
|
254
268
|
}
|
|
@@ -539,14 +553,11 @@ const args = process.argv.slice(2);
|
|
|
539
553
|
const cmd = args[0];
|
|
540
554
|
|
|
541
555
|
const BANNER = `
|
|
542
|
-
|
|
543
|
-
|
|
544
|
-
|
|
545
|
-
|
|
546
|
-
|
|
547
|
-
███████╔╝ ██████╗ ██║ ██║
|
|
548
|
-
╚══════╝ ╚═════╝ ╚═╝ ╚═╝
|
|
549
|
-
|
|
556
|
+
████ █████ █ █ █ █ ███
|
|
557
|
+
█ █ █ █ █ █ █ █ █
|
|
558
|
+
████ ████ █ █ █████ █████
|
|
559
|
+
█ █ █ █ █ █ █ █
|
|
560
|
+
█ █ █████ █ █ █ █
|
|
550
561
|
`;
|
|
551
562
|
|
|
552
563
|
const HELP_TEXT = `
|
package/docs/ARCHITECTURE.md
CHANGED
|
@@ -55,7 +55,7 @@
|
|
|
55
55
|
│ │
|
|
56
56
|
│ ┌──────────────────────────────────────────────────────────┐ │
|
|
57
57
|
│ │ SHARED VOLUMES │ │
|
|
58
|
-
│ │ • /
|
|
58
|
+
│ │ • ~/.config/rev4a/shared/shared-skills → all agents (ro) │ │
|
|
59
59
|
│ │ • /docker/shared-repos → shared project repos │ │
|
|
60
60
|
│ └──────────────────────────────────────────────────────────┘ │
|
|
61
61
|
│ └──────────────────────────────────────────────────────────┘ │
|
|
@@ -107,14 +107,14 @@ The central container, running the Next.js dashboard + orchestration API.
|
|
|
107
107
|
|
|
108
108
|
**Volume mounts (target):**
|
|
109
109
|
```
|
|
110
|
-
/path/to/rev4a/data
|
|
111
|
-
/var/run/docker.sock
|
|
112
|
-
/
|
|
113
|
-
/docker/shared-repos
|
|
110
|
+
/path/to/rev4a/data → /data (persistent)
|
|
111
|
+
/var/run/docker.sock → /var/run/docker.sock
|
|
112
|
+
~/.config/rev4a/shared/shared-skills → /data/.openclaw/shared-skills (ro)
|
|
113
|
+
/docker/shared-repos → /data/repos (rw)
|
|
114
114
|
```
|
|
115
115
|
|
|
116
116
|
**Rev4a System Rules** (`rev4a-rules/AGENTS.md`):
|
|
117
|
-
- Versioned in the repo, deployed to
|
|
117
|
+
- Versioned in the repo, deployed to `~/.config/rev4a/shared/rev4a-rules/` on every `rev4a serve` start
|
|
118
118
|
- Mounted read-only into every agent at `/root/.openclaw/workspace/.rev4a/AGENTS.md`
|
|
119
119
|
- Injected into every session via the `bootstrap-extra-files` hook (glob `.rev4a/*.md`)
|
|
120
120
|
- Immutable by agents — enforced by the `:ro` mount
|
|
@@ -132,9 +132,9 @@ copies the entrypoint script, and applies baseline gateway config patches.
|
|
|
132
132
|
|
|
133
133
|
**Volume mounts (per agent):**
|
|
134
134
|
```
|
|
135
|
-
/
|
|
136
|
-
/
|
|
137
|
-
agent-{name}-data (named vol)
|
|
135
|
+
~/.config/rev4a/shared/shared-skills → /data/.openclaw/shared-skills (ro)
|
|
136
|
+
~/.config/rev4a/shared/rev4a-rules → /root/.openclaw/workspace/.rev4a (ro)
|
|
137
|
+
agent-{name}-data (named vol) → /root (rw, per-agent persistent state)
|
|
138
138
|
```
|
|
139
139
|
|
|
140
140
|
All shared volumes are defined centrally in `lib/agent-setup.ts`
|
|
@@ -287,9 +287,9 @@ GET /api/agents List all agents (Docker containers with AGENT
|
|
|
287
287
|
|
|
288
288
|
## 6. Shared Volumes
|
|
289
289
|
|
|
290
|
-
### Shared Skills (
|
|
290
|
+
### Shared Skills (`~/.config/rev4a/shared/shared-skills`)
|
|
291
291
|
```
|
|
292
|
-
/
|
|
292
|
+
~/.config/rev4a/shared/shared-skills/
|
|
293
293
|
my-custom-skill/ # promoted from an agent workspace
|
|
294
294
|
SKILL.md
|
|
295
295
|
another-skill/
|
|
@@ -306,12 +306,30 @@ This keeps the layout simple and avoids hierarchy debates at this stage.
|
|
|
306
306
|
**Auto-configuration:**
|
|
307
307
|
- The agent base image (`openclaw-agent-base`) pre-configures `skills.load.extraDirs: ["/data/.openclaw/shared-skills"]` so every new agent discovers shared skills automatically.
|
|
308
308
|
- Both create and recreate routes guarantee this via `applyRuntimeConfig()` from `lib/agent-setup.ts`.
|
|
309
|
-
-
|
|
309
|
+
- No manual setup: `rev4a serve` creates both shared directories during
|
|
310
|
+
preflight (`ensureRev4aRules()` in `bin/rev4a.js`). They live under
|
|
311
|
+
`REV4A_DATA`, so a plain `mkdir` suffices — no `sudo`, no permission
|
|
312
|
+
bits to set.
|
|
313
|
+
|
|
314
|
+
> These directories used to live at `/docker/shared-skills` and
|
|
315
|
+
> `/docker/rev4a-rules`, which needed `sudo` on Linux and could not exist at
|
|
316
|
+
> all on macOS — the root volume is a sealed APFS system volume, so `/docker`
|
|
317
|
+
> is not creatable even as root, and agent creation failed with
|
|
318
|
+
> `mounts denied`. Being under the user's home also puts them inside the
|
|
319
|
+
> directories Docker Desktop shares with its Linux VM by default, so bind
|
|
320
|
+
> mounts work with no extra configuration.
|
|
321
|
+
>
|
|
322
|
+
> Upgrading an existing install: move any promoted skills from
|
|
323
|
+
> `/docker/shared-skills` into the new location, then hit **Recreate** on each
|
|
324
|
+
> agent so its container picks up the new mount source. Containers freeze
|
|
325
|
+
> their mounts at creation time, so running agents keep using the old path
|
|
326
|
+
> until recreated. `rev4a-rules/AGENTS.md` needs no migration — it is
|
|
327
|
+
> regenerated from the package on every start.
|
|
310
328
|
|
|
311
329
|
### Rev4a System Rules (`rev4a-rules/`)
|
|
312
330
|
|
|
313
331
|
Versioned in the repo at `rev4a-rules/AGENTS.md`. Deployed to
|
|
314
|
-
|
|
332
|
+
`~/.config/rev4a/shared/rev4a-rules/` on every `rev4a serve` startup via
|
|
315
333
|
`ensureRev4aRules()` in `bin/rev4a.js`. Mounted read-only into every
|
|
316
334
|
agent container and injected into sessions through the
|
|
317
335
|
`bootstrap-extra-files` hook (glob `.rev4a/*.md`).
|
|
@@ -34,6 +34,7 @@ All shared UI primitives live in `app/components/ui/` and are exported from `app
|
|
|
34
34
|
| `PropertyList` | `PropertyList.tsx` | 2-column grid for label/value pairs. Children rendered as `<Fragment>` — do not wrap in extra `<div>`. |
|
|
35
35
|
| `TemplateOption` | `TemplateOption.tsx` | Agent template selection card with avatar, description, optional badge. Centralises create-agent card styling. |
|
|
36
36
|
| `Pill` | `Pill.tsx` | Small status/attribute label. Variants: `default`, `accent`. |
|
|
37
|
+
| `ModalityIcons` | `ModalityIcons.tsx` | Capability badges parsed from a model's `modality` string (`"text+image->text"`). Input types render as icons; a non-text **output** is called out separately, since reading an image and generating one are different capabilities. Pass `dynamic` for router models, which advertise the union of everything they might route to and so show "varies" instead. |
|
|
37
38
|
| `Metric` | `Metric.tsx` | Metric card with title, value, subtitle, tone. |
|
|
38
39
|
| `StatusCard` | `StatusCard.tsx` | Health/status report card. |
|
|
39
40
|
| `Surface` | `Surface.tsx` | Shared panel/card surface, variant prop. |
|
package/docs/REV4A.md
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
# Rev4a — VPS Dashboard
|
|
2
2
|
|
|
3
|
-
> **Last updated:** 2026-
|
|
3
|
+
> **Last updated:** 2026-08-09
|
|
4
4
|
|
|
5
5
|
A Next.js 16 dashboard for monitoring and managing the OpenClaw ecosystem.
|
|
6
6
|
|
|
@@ -52,15 +52,24 @@ A Next.js 16 dashboard for monitoring and managing the OpenClaw ecosystem.
|
|
|
52
52
|
|
|
53
53
|
### Environment
|
|
54
54
|
|
|
55
|
-
Rev4a reads the following environment variables. Set them in `/config` (UI) or directly in `.env`.
|
|
55
|
+
Rev4a reads the following environment variables. Set them in `/config` (UI) or directly in `.env`. See [.env.example](../.env.example) for the canonical, always-current list.
|
|
56
56
|
|
|
57
57
|
| Variable | Required | Description |
|
|
58
58
|
|---|---|---|
|
|
59
59
|
| `REV4A_PASSWORD` | Yes | Login password for the dashboard |
|
|
60
60
|
| `REV4A_TOKEN` | Yes | Token used for programmatic API access |
|
|
61
61
|
| `REV4A_JWT_SECRET` | Yes | Secret used to sign session JWT tokens |
|
|
62
|
-
| `
|
|
62
|
+
| `REV4A_API_KEY` | No | API key for the Gateway provider endpoint (auto-generated) |
|
|
63
|
+
| `GATEWAY_TOKEN` | No | Shared token handed to agent containers |
|
|
64
|
+
| `REV4A_DATA_DIR` | No | Root of all persistent data (default: `~/.config/rev4a`) |
|
|
65
|
+
| `REV4A_DB` | No | Path to the events database (default: `<data dir>/data/events.db`) |
|
|
66
|
+
| `REV4A_CREDENTIALS_DB` | No | Path to the credentials vault database |
|
|
63
67
|
| `REV4A_WORKSPACE_ROOT` | No | Filesystem path for the local file explorer |
|
|
68
|
+
| `REV4A_ROOT` | No | Installation root, used by `rev4a update` to detect a git checkout |
|
|
69
|
+
| `REV4A_TIMEZONE` | No | IANA timezone used for scheduling and timestamps |
|
|
70
|
+
| `REV4A_FORCE_INSECURE_COOKIE` | No | Allow a non-Secure session cookie — plain-HTTP deployments only |
|
|
71
|
+
|
|
72
|
+
See [Alerts](#alerts-telegram) below for the Telegram alert variables.
|
|
64
73
|
|
|
65
74
|
## Agent creation
|
|
66
75
|
|
|
@@ -138,6 +147,29 @@ REV4A_JWT_SECRET=*** # Secret for signing JWTs
|
|
|
138
147
|
|
|
139
148
|
---
|
|
140
149
|
|
|
150
|
+
## Alerts (Telegram)
|
|
151
|
+
|
|
152
|
+
Rev4a can send Telegram alerts when the events database goes stale (no
|
|
153
|
+
recent writes — see `/api/system-health`). Implemented in `lib/alerts.ts`.
|
|
154
|
+
|
|
155
|
+
**Env vars:**
|
|
156
|
+
```
|
|
157
|
+
REV4A_ALERTS_ENABLED=false # Master switch — no alerts sent unless "true"
|
|
158
|
+
REV4A_TELEGRAM_BOT_TOKEN= # Telegram bot token
|
|
159
|
+
REV4A_TELEGRAM_CHAT_ID= # Telegram chat to notify
|
|
160
|
+
REV4A_ALERT_COOLDOWN_MS=600000 # Minimum time between repeat alerts for the same condition
|
|
161
|
+
REV4A_ALERT_STALE_SECONDS=120 # How long without a DB write before the system is considered stale
|
|
162
|
+
REV4A_ALERT_SMOKE=false # Set true to send a one-off test alert on startup
|
|
163
|
+
```
|
|
164
|
+
|
|
165
|
+
**Behavior:**
|
|
166
|
+
- If `REV4A_ALERTS_ENABLED` is not `"true"`, `maybeSendAlert` is a no-op — nothing is sent, nothing is recorded.
|
|
167
|
+
- If enabled but `REV4A_TELEGRAM_BOT_TOKEN` or `REV4A_TELEGRAM_CHAT_ID` is missing, alerts run in **dry-run**: state transitions are still recorded in the `alert_state` table (SQLite), but no Telegram message is sent — visible in server logs as `[alerts] dry-run`.
|
|
168
|
+
- Alert state (`ok` / `alerted` / `resolved`) is tracked per `alert_key` in `alert_state`, so a resolved condition sends a follow-up "✅ resolved" message instead of staying silent.
|
|
169
|
+
- `REV4A_ALERT_COOLDOWN_MS` prevents repeat-sending the same stale alert while the condition persists.
|
|
170
|
+
|
|
171
|
+
---
|
|
172
|
+
|
|
141
173
|
## Service Management
|
|
142
174
|
|
|
143
175
|
```bash
|
|
@@ -167,8 +199,6 @@ Environment=REV4A_PASSWORD=***
|
|
|
167
199
|
Environment=REV4A_TOKEN=***
|
|
168
200
|
Environment=REV4A_JWT_SECRET=***
|
|
169
201
|
Environment=REV4A_DB=/path/to/rev4a/data/events.db
|
|
170
|
-
Environment=OPENCLAW_CONFIG_PATH=/path/to/openclaw-core.json
|
|
171
|
-
Environment=SHARED_CONTEXT_DIR=/path/to/shared-context
|
|
172
202
|
Environment=NODE_ENV=production
|
|
173
203
|
Environment=NEXT_TELEMETRY_DISABLED=1
|
|
174
204
|
```
|
|
@@ -498,7 +528,7 @@ curl -b /tmp/cookies.txt -X PUT http://localhost:3740/api/workspace \
|
|
|
498
528
|
├── public/ # Static assets
|
|
499
529
|
├── scripts/ # Utility scripts (setup, helpers)
|
|
500
530
|
├── .env # Local env vars (gitignored)
|
|
501
|
-
├── proxy.ts
|
|
531
|
+
├── proxy.ts # Auth gate — redirects to /login without a valid JWT cookie
|
|
502
532
|
├── next.config.mjs
|
|
503
533
|
└── package.json
|
|
504
534
|
```
|
|
@@ -197,6 +197,11 @@ Returns current provider configuration state.
|
|
|
197
197
|
|
|
198
198
|
**Auth:** any auth method
|
|
199
199
|
|
|
200
|
+
**Query params:**
|
|
201
|
+
| Param | Effect |
|
|
202
|
+
|---|---|
|
|
203
|
+
| `summary=1` | Returns only `{ provider, label, configured }` per provider — no `models`, no `pricing`, no `apiKey`. The full response awaits live pricing from OpenRouter, a network round trip costing ~2.4s on a cold cache; callers that only need to know whether *any* provider is configured should use this. |
|
|
204
|
+
|
|
200
205
|
**Response:**
|
|
201
206
|
```json
|
|
202
207
|
{
|
|
@@ -1562,7 +1567,7 @@ Enable or disable a plugin.
|
|
|
1562
1567
|
## Skills
|
|
1563
1568
|
|
|
1564
1569
|
Skill discovery across three sources:
|
|
1565
|
-
- **Shared:** skills in
|
|
1570
|
+
- **Shared:** skills in `~/.config/rev4a/shared/shared-skills` on the VPS host (global, available to all agents)
|
|
1566
1571
|
- **Workspace:** skills in `<workspace>/skills` inside each agent container (discovered via `docker exec`)
|
|
1567
1572
|
- **Bundled:** skills shipped with OpenClaw (read-only)
|
|
1568
1573
|
|
|
@@ -1578,8 +1583,8 @@ List all available skills from all sources.
|
|
|
1578
1583
|
{
|
|
1579
1584
|
"name": "code-review",
|
|
1580
1585
|
"type": "shared",
|
|
1581
|
-
"path": "/
|
|
1582
|
-
"skillMdPath": "/
|
|
1586
|
+
"path": "~/.config/rev4a/shared/shared-skills/code-review",
|
|
1587
|
+
"skillMdPath": "~/.config/rev4a/shared/shared-skills/code-review/SKILL.md",
|
|
1583
1588
|
"hasSkillMd": true,
|
|
1584
1589
|
"description": "Systematic code review patterns...",
|
|
1585
1590
|
"version": "1.0"
|
|
@@ -1643,7 +1648,7 @@ Copy a skill from an agent's workspace to the shared skills directory, making it
|
|
|
1643
1648
|
"skillName": "my-custom-skill",
|
|
1644
1649
|
"agentId": "atlas",
|
|
1645
1650
|
"containerName": "openclaw-atlas",
|
|
1646
|
-
"destPath": "/
|
|
1651
|
+
"destPath": "~/.config/rev4a/shared/shared-skills/my-custom-skill",
|
|
1647
1652
|
"overwritten": false,
|
|
1648
1653
|
"extraCopied": 0
|
|
1649
1654
|
}
|