@flame0510/project-aether 1.6.1 → 1.7.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +21 -0
- package/README.md +5 -2
- package/agent-templates/atlas/HEARTBEAT.md +1 -1
- package/app/agents/ImageDownloadBanner.tsx +171 -37
- package/app/api/agents/download-image/route.ts +29 -5
- package/app/api/agents/image-status/route.ts +17 -2
- package/app/api/assistant/route.ts +13 -4
- package/app/api/auth/login/route.ts +2 -2
- package/app/api/setup/agent-image/route.ts +6 -4
- package/app/api/system-health/route.ts +3 -3
- package/app/components/DashboardToolbar.tsx +2 -2
- package/app/components/LineageGraphPage.tsx +4 -4
- package/app/components/SessionDrawer.tsx +8 -8
- package/app/components/SystemCockpit.tsx +1 -1
- package/app/globals.css +1 -1
- package/app/setup/PageClient.tsx +1 -1
- package/bin/postinstall.js +5 -1
- package/daemon.js +13 -34
- package/docs/ARCHITECTURE.md +44 -26
- package/docs/CONTAINER-TERMINAL.md +17 -8
- package/docs/DESIGN-SYSTEM.md +21 -12
- package/docs/FRONTEND-ARCHITECTURE.md +2 -2
- package/docs/REV4A.md +35 -85
- package/docs/dev/API-REFERENCE.md +73 -19
- package/docs/dev/DATABASE.md +18 -7
- package/docs/dev/GATEWAY.md +23 -0
- package/docs/dev/SESSION-MAINTENANCE-PLAN.md +6 -6
- package/docs/rag/DATA-FRESHNESS.md +20 -9
- package/docs/rag/GLOSSARY.md +2 -2
- package/docs/rag/REV4A-OVERVIEW.md +10 -6
- package/docs/rag/WHAT-I-CAN-ANSWER.md +3 -3
- package/lib/agent-images.ts +43 -14
- package/lib/buildAgentImage.ts +142 -6
- package/lib/patterns/sessionPresentation.ts +4 -2
- package/lib/rev4a-auth.d.ts +1 -0
- package/lib/rev4a-auth.js +18 -2
- package/models.config.json +937 -45
- package/next.config.mjs +9 -1
- package/package.json +5 -3
- package/scripts/backup.sh +48 -54
- package/scripts/check-language.mjs +21 -3
- package/scripts/model-info-verify.mjs +194 -0
- package/scripts/restore.sh +77 -59
package/next.config.mjs
CHANGED
|
@@ -1,7 +1,15 @@
|
|
|
1
|
+
// Extra hostnames or IPs allowed to reach the dev server (`next dev` only — no effect on
|
|
2
|
+
// a production build), e.g. a LAN address when opening the dashboard from another device.
|
|
3
|
+
// Comma-separated, without scheme or port; none by default.
|
|
4
|
+
const extraDevOrigins = (process.env.REV4A_DEV_ORIGINS ?? '')
|
|
5
|
+
.split(',')
|
|
6
|
+
.map((origin) => origin.trim())
|
|
7
|
+
.filter(Boolean);
|
|
8
|
+
|
|
1
9
|
const nextConfig = {
|
|
2
10
|
reactStrictMode: true,
|
|
3
11
|
serverExternalPackages: ['better-sqlite3'],
|
|
4
|
-
allowedDevOrigins: ['
|
|
12
|
+
allowedDevOrigins: ['127.0.0.1', 'localhost', ...extraDevOrigins],
|
|
5
13
|
};
|
|
6
14
|
|
|
7
15
|
export default nextConfig;
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@flame0510/project-aether",
|
|
3
|
-
"version": "1.
|
|
3
|
+
"version": "1.7.0",
|
|
4
4
|
"description": "Rev4a — Revolution for Agents. OpenClaw agent fleet orchestrator.",
|
|
5
5
|
"keywords": [
|
|
6
6
|
"openclaw",
|
|
@@ -52,12 +52,14 @@
|
|
|
52
52
|
"scripts": {
|
|
53
53
|
"dev": "next dev -p 3720",
|
|
54
54
|
"build": "next build",
|
|
55
|
+
"typecheck": "tsc --noEmit",
|
|
55
56
|
"start": "rev4a serve",
|
|
56
57
|
"start:all": "rev4a serve",
|
|
57
58
|
"refresh:pricing": "node scripts/refresh-model-pricing.mjs",
|
|
58
|
-
"smoke:db-bootstrap": "node scripts/smoke-db-bootstrap.mjs",
|
|
59
59
|
"postinstall": "node bin/postinstall.js",
|
|
60
|
-
"check:language": "node scripts/check-language.mjs"
|
|
60
|
+
"check:language": "node scripts/check-language.mjs",
|
|
61
|
+
"info:suggest": "node scripts/model-info-suggest.mjs",
|
|
62
|
+
"info:verify": "node scripts/model-info-verify.mjs"
|
|
61
63
|
},
|
|
62
64
|
"dependencies": {
|
|
63
65
|
"@types/better-sqlite3": "^7.6.13",
|
package/scripts/backup.sh
CHANGED
|
@@ -1,66 +1,60 @@
|
|
|
1
1
|
#!/usr/bin/env bash
|
|
2
|
-
# backup.sh —
|
|
2
|
+
# backup.sh — archive Rev4a's own data directory
|
|
3
3
|
#
|
|
4
4
|
# Usage:
|
|
5
|
-
#
|
|
6
|
-
#
|
|
5
|
+
# bash scripts/backup.sh # archive into ~/rev4a-backups/
|
|
6
|
+
# bash scripts/backup.sh /path/to/backups # custom destination
|
|
7
7
|
#
|
|
8
|
-
# What it
|
|
9
|
-
#
|
|
10
|
-
#
|
|
11
|
-
#
|
|
12
|
-
#
|
|
13
|
-
#
|
|
8
|
+
# What it archives: the whole Rev4a data directory — $REV4A_DATA_DIR when set,
|
|
9
|
+
# otherwise ~/.config/rev4a (the same rule as lib/rev4a-paths.ts). That is `.env`
|
|
10
|
+
# (secrets and configuration), everything under `data/` (events.db and credentials.db
|
|
11
|
+
# with their -wal/-shm files, provider-keys.json, agents-token.json, model overrides,
|
|
12
|
+
# the update log) and `shared/` (shared skills and rules). New files there are included
|
|
13
|
+
# automatically.
|
|
14
14
|
#
|
|
15
|
-
#
|
|
16
|
-
#
|
|
17
|
-
#
|
|
15
|
+
# Not included: agent volumes (the cold backups in the dashboard cover those), the
|
|
16
|
+
# installed package, node_modules, .next.
|
|
17
|
+
#
|
|
18
|
+
# The archive holds every Rev4a secret in plain text: it is created with mode 0600, and a
|
|
19
|
+
# destination directory the script creates gets mode 0700 (an existing one is left as it
|
|
20
|
+
# is). Keep a copy off the machine, stored securely.
|
|
21
|
+
# For a fully consistent database copy, stop Rev4a first (`systemctl stop rev4a`);
|
|
22
|
+
# a copy taken while it runs is normally fine, since SQLite keeps its WAL alongside.
|
|
18
23
|
set -euo pipefail
|
|
19
24
|
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
REV4A_ROOT="$(cd "${SCRIPT_DIR}/.." && pwd)"
|
|
23
|
-
BACKUP_DIR="${1:-${REV4A_ROOT}/backups}"
|
|
24
|
-
|
|
25
|
-
mkdir -p "$BACKUP_DIR"
|
|
26
|
-
|
|
27
|
-
TS=$(date -u +%Y%m%d-%H%M%S)
|
|
28
|
-
ARCHIVE="${BACKUP_DIR}/rev4a-backup-${TS}.tar.gz"
|
|
29
|
-
|
|
30
|
-
echo "Backing up Rev4a data..."
|
|
31
|
-
echo " Source: ${REV4A_ROOT}"
|
|
32
|
-
echo " Target: ${ARCHIVE}"
|
|
25
|
+
DATA_DIR="${REV4A_DATA_DIR:-${HOME}/.config/rev4a}"
|
|
26
|
+
BACKUP_DIR="${1:-${HOME}/rev4a-backups}"
|
|
33
27
|
|
|
34
|
-
|
|
28
|
+
if [[ ! -d "$DATA_DIR" ]]; then
|
|
29
|
+
echo "Error: Rev4a data directory not found: $DATA_DIR" >&2
|
|
30
|
+
echo "Set REV4A_DATA_DIR if Rev4a keeps its data elsewhere." >&2
|
|
31
|
+
exit 1
|
|
32
|
+
fi
|
|
33
|
+
if [[ ! -f "$DATA_DIR/.env" && ! -d "$DATA_DIR/data" ]]; then
|
|
34
|
+
echo "Error: $DATA_DIR has neither .env nor data/ — refusing to archive an unrelated directory." >&2
|
|
35
|
+
exit 1
|
|
36
|
+
fi
|
|
35
37
|
|
|
36
|
-
# Restrict permissions during archive creation
|
|
37
38
|
umask 077
|
|
38
|
-
|
|
39
|
-
#
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
echo "=============================================="
|
|
55
|
-
echo " Backup complete!"
|
|
56
|
-
echo "=============================================="
|
|
57
|
-
echo ""
|
|
58
|
-
echo " Archive: ${ARCHIVE}"
|
|
59
|
-
echo " Size: $(du -h "$ARCHIVE" | cut -f1)"
|
|
60
|
-
echo ""
|
|
39
|
+
mkdir -p "$BACKUP_DIR"
|
|
40
|
+
# The archive must not end up inside what it archives. Both paths are compared once they
|
|
41
|
+
# exist and with symlinks resolved, so a relative or linked destination cannot slip past.
|
|
42
|
+
case "$(cd "$BACKUP_DIR" && pwd -P)/" in
|
|
43
|
+
"$(cd "$DATA_DIR" && pwd -P)/"*) echo "Error: the destination is inside the data directory." >&2; exit 1 ;;
|
|
44
|
+
esac
|
|
45
|
+
|
|
46
|
+
ARCHIVE="${BACKUP_DIR}/rev4a-backup-$(date -u +%Y%m%d-%H%M%S).tar.gz"
|
|
47
|
+
echo "Backing up Rev4a data"
|
|
48
|
+
echo " Source: $DATA_DIR"
|
|
49
|
+
echo " Target: $ARCHIVE"
|
|
50
|
+
|
|
51
|
+
# noclobber: a second backup in the same second fails instead of overwriting the first.
|
|
52
|
+
set -o noclobber
|
|
53
|
+
tar -C "$DATA_DIR" -czf - . > "$ARCHIVE"
|
|
54
|
+
set +o noclobber
|
|
61
55
|
chmod 600 "$ARCHIVE"
|
|
62
|
-
|
|
63
|
-
echo " Store it securely (encrypted at rest recommended)."
|
|
56
|
+
|
|
64
57
|
echo ""
|
|
65
|
-
echo "
|
|
66
|
-
echo "
|
|
58
|
+
echo "Backup complete: $(tar -tzf "$ARCHIVE" | grep -vc '/$') files, $(du -h "$ARCHIVE" | cut -f1)"
|
|
59
|
+
echo "It contains plain-text secrets (.env, keys, credentials): keep a copy off this machine, stored securely."
|
|
60
|
+
echo "To restore: bash scripts/restore.sh $ARCHIVE"
|
|
@@ -4,7 +4,7 @@
|
|
|
4
4
|
* identifiers, comments, docs, UI strings (AGENTS.md §2.1).
|
|
5
5
|
*
|
|
6
6
|
* The word list is deliberately conservative: only words that are **not** English words.
|
|
7
|
-
* `come`, `per`, `non`, `la`, `al
|
|
7
|
+
* `come`, `per`, `non`, `la`, `al` are left out on purpose, they would fire on
|
|
8
8
|
* English text; the list is content words plus unmistakable function words. A line
|
|
9
9
|
* carrying `lang-ok` is skipped, for the legitimate case where the Italian is the point
|
|
10
10
|
* (a fixture, a note about the rule):
|
|
@@ -12,8 +12,9 @@
|
|
|
12
12
|
* // lang-ok: the exact text a user reported
|
|
13
13
|
*
|
|
14
14
|
* It catches Italian *words*, not Italian thinking written in English, and it never
|
|
15
|
-
* translates: the fix is manual.
|
|
16
|
-
*
|
|
15
|
+
* translates: the fix is manual. Run by CI on every push to main and every pull request,
|
|
16
|
+
* again by the release workflow before publishing, and available locally as
|
|
17
|
+
* `npm run check:language` (and as a pre-commit hook, `.githooks/pre-commit`).
|
|
17
18
|
*
|
|
18
19
|
* Usage:
|
|
19
20
|
* node scripts/check-language.mjs # every tracked file
|
|
@@ -35,6 +36,23 @@ const WORDS = [
|
|
|
35
36
|
'essere', 'fare', 'dove', 'nuovo', 'nuova', 'vecchio', 'vecchia', 'davvero', 'invece',
|
|
36
37
|
'inoltre', 'quindi', 'della', 'delle', 'degli', 'nella', 'nelle', 'negli', 'dalla', 'dalle',
|
|
37
38
|
'dallo', 'allo', 'alla', 'sul', 'sulla', 'nessuno', 'nessuna', 'adesso', 'subito', 'oggi', 'ieri',
|
|
39
|
+
// Added 2026-09-24, after three lines survived the list above — one of them a UI string
|
|
40
|
+
// ('Include OpenRouter e altri provider…' in the system-health recommendations).
|
|
41
|
+
'compila', 'compilare', 'compilato', 'pubblica', 'pubblico', 'pubblicare', 'pubblicato',
|
|
42
|
+
'altro', 'altra', 'altri', 'altre', 'giorno', 'giorni', 'mese', 'settimana', 'settimane',
|
|
43
|
+
'aggiorna', 'aggiornare', 'aggiornamento', 'aggiornato', 'controllo', 'controlla', 'controllare',
|
|
44
|
+
'verifica', 'verificare', 'esempio', 'esempi', 'funziona', 'funzione', 'funzioni',
|
|
45
|
+
'creare', 'cancella', 'cancellare', 'scarica', 'scaricare', 'usare', 'usato', 'utilizzo',
|
|
46
|
+
'riga', 'parola', 'parole', 'elenco', 'codice', 'chiave', 'chiavi', 'prova', 'risultato',
|
|
47
|
+
'risultati', 'stesso', 'stessa', 'stessi', 'stesse', 'ancora', 'sempre', 'dopo', 'forse',
|
|
48
|
+
'puoi', 'posso', 'siamo', 'abbiamo', 'erano', 'servono', 'vuole', 'vuoi', 'quale', 'quali',
|
|
49
|
+
'disponibile', 'necessario', 'corretto', 'sbagliato', 'mancante', 'vuoto', 'ultimo', 'ultima',
|
|
50
|
+
'italiano', 'italiana', 'italiane', 'inglese', 'pero', 'cosi', 'piu', 'ecco', 'grazie',
|
|
51
|
+
// Added after a later review found UI labels, alert text and a doc line the list above
|
|
52
|
+
// missed (session drawer, lineage and dashboard toolbars, system-health alerts).
|
|
53
|
+
'stato', 'tipo', 'costo', 'costi', 'durata', 'inizio', 'aggiorn', 'azioni', 'recenti',
|
|
54
|
+
'grafo', 'reimposta', 'periodo', 'agenti', 'agente', 'apri', 'soglia', 'coprire',
|
|
55
|
+
'contenuti', 'allinea', 'allineare', 'campioni', 'consecutivi',
|
|
38
56
|
];
|
|
39
57
|
|
|
40
58
|
const EXTENSIONS = ['.ts', '.tsx', '.js', '.mjs', '.cjs', '.md', '.json', '.sh', '.yml', '.yaml', '.css', '.html'];
|
|
@@ -0,0 +1,194 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
/**
|
|
3
|
+
* Verifies proposed `info` blocks against the model cards they cite — the gate between a
|
|
4
|
+
* curator agent's proposal and `models.config.json`. Nothing is written: the script says
|
|
5
|
+
* which proposals hold and which do not.
|
|
6
|
+
*
|
|
7
|
+
* What it checks, per model:
|
|
8
|
+
* - the id exists in the catalogue, and the model has a Hugging Face card;
|
|
9
|
+
* - `evidence` is present (a proposal without it is not verifiable);
|
|
10
|
+
* - every number in `params` appears in the card text;
|
|
11
|
+
* - every benchmark value sits in a row named after the benchmark, under a column whose
|
|
12
|
+
* header names this model (or its instruct variant) — the nearest header row that
|
|
13
|
+
* carries the model's name, so a card with a stacked or merged header still resolves;
|
|
14
|
+
* - `docUrl`, when asked with `--check-links`, answers 200.
|
|
15
|
+
*
|
|
16
|
+
* Usage:
|
|
17
|
+
* node scripts/model-info-verify.mjs <file with the proposals|-> [--check-links]
|
|
18
|
+
* The file may be raw agent output: JSON objects are extracted from it.
|
|
19
|
+
*/
|
|
20
|
+
import * as fs from 'node:fs';
|
|
21
|
+
import * as path from 'node:path';
|
|
22
|
+
import { fileURLToPath } from 'node:url';
|
|
23
|
+
|
|
24
|
+
const ROOT = path.resolve(path.dirname(fileURLToPath(import.meta.url)), '..');
|
|
25
|
+
const args = process.argv.slice(2);
|
|
26
|
+
const input = args.find((a) => !a.startsWith('--')) ?? '-';
|
|
27
|
+
const checkLinks = args.includes('--check-links');
|
|
28
|
+
|
|
29
|
+
const catalogue = new Set(JSON.parse(fs.readFileSync(path.join(ROOT, 'models.config.json'), 'utf-8')).models.map((m) => m.id));
|
|
30
|
+
const details = (() => {
|
|
31
|
+
try { return JSON.parse(fs.readFileSync(path.join(ROOT, 'model-details.json'), 'utf-8')).models ?? {}; } catch { return {}; }
|
|
32
|
+
})();
|
|
33
|
+
|
|
34
|
+
const decode = (s) => s.replace(/<[^>]*>/g, ' ').replace(/</g, '<').replace(/>/g, '>').replace(/&/g, '&').replace(/"/g, '"').replace(/'/g, "'").replace(/\s+/g, ' ').trim();
|
|
35
|
+
const norm = (s) => s.toLowerCase().replace(/[^a-z0-9]/g, '');
|
|
36
|
+
|
|
37
|
+
/** Every table row of a card, markdown and HTML, as normalised `| a | b |` strings. */
|
|
38
|
+
function tableRows(md) {
|
|
39
|
+
const rows = md.split('\n').filter((l) => l.trim().startsWith('|')).map(decode);
|
|
40
|
+
for (const tr of md.matchAll(/<tr[^>]*>([\s\S]*?)<\/tr>/gi)) {
|
|
41
|
+
const cells = [...tr[1].matchAll(/<t[dh][^>]*>([\s\S]*?)<\/t[dh]>/gi)].map((c) => decode(c[1]));
|
|
42
|
+
if (cells.length) rows.push(`| ${cells.join(' | ')} |`);
|
|
43
|
+
}
|
|
44
|
+
return rows;
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
const cellsOf = (row) => row.split('|').slice(1, -1).map((c) => c.replace(/\*/g, '').trim());
|
|
48
|
+
|
|
49
|
+
/**
|
|
50
|
+
* A header cell names the model when it carries the same distinctive run of characters:
|
|
51
|
+
* cards label columns with a display name ("Kimi K2.5 (Thinking)", "Nemotron-3-Super")
|
|
52
|
+
* that is not the id, and an exact match would miss every one of them.
|
|
53
|
+
*/
|
|
54
|
+
function namesModel(cell, modelNames) {
|
|
55
|
+
const c = norm(cell);
|
|
56
|
+
if (!c) return false;
|
|
57
|
+
return modelNames.some((n) => {
|
|
58
|
+
const m = norm(n);
|
|
59
|
+
if (!m) return false;
|
|
60
|
+
for (let len = Math.min(c.length, m.length); len >= 6; len--) {
|
|
61
|
+
for (let i = 0; i + len <= c.length; i++) if (m.includes(c.slice(i, i + len))) return true;
|
|
62
|
+
}
|
|
63
|
+
return false;
|
|
64
|
+
});
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
/** The nearest row above `row` that names the model: the header this value belongs under. */
|
|
68
|
+
function headerNaming(rows, rowIndex, modelNames) {
|
|
69
|
+
for (let i = rowIndex - 1; i >= 0; i--) {
|
|
70
|
+
if (!rows[i]) break;
|
|
71
|
+
const cells = cellsOf(rows[i]);
|
|
72
|
+
if (cells.some((c) => namesModel(c, modelNames))) return cells;
|
|
73
|
+
}
|
|
74
|
+
return null;
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
function proposalObjects(text) {
|
|
78
|
+
const out = [];
|
|
79
|
+
for (let i = 0; i < text.length; i++) {
|
|
80
|
+
if (text[i] !== '{') continue;
|
|
81
|
+
let depth = 0, inString = false, escaped = false, end = -1;
|
|
82
|
+
for (let j = i; j < text.length; j++) {
|
|
83
|
+
const ch = text[j];
|
|
84
|
+
if (inString) {
|
|
85
|
+
if (escaped) escaped = false;
|
|
86
|
+
else if (ch === '\\') escaped = true;
|
|
87
|
+
else if (ch === '"') inString = false;
|
|
88
|
+
continue;
|
|
89
|
+
}
|
|
90
|
+
if (ch === '"') inString = true;
|
|
91
|
+
else if (ch === '{') depth++;
|
|
92
|
+
else if (ch === '}') { depth--; if (depth === 0) { end = j; break; } }
|
|
93
|
+
}
|
|
94
|
+
if (end < 0) continue; // an unclosed fragment (engine logs) must not stop the scan
|
|
95
|
+
try {
|
|
96
|
+
const parsed = JSON.parse(text.slice(i, end + 1));
|
|
97
|
+
if (parsed && parsed.id && (parsed.info || parsed.skipped)) out.push(parsed);
|
|
98
|
+
} catch { /* not a proposal object */ }
|
|
99
|
+
i = end;
|
|
100
|
+
}
|
|
101
|
+
return out;
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
async function cardFor(hfId) {
|
|
105
|
+
const cached = path.join('/tmp', `card-${hfId.replace('/', '_')}.md`);
|
|
106
|
+
if (fs.existsSync(cached)) return fs.readFileSync(cached, 'utf-8');
|
|
107
|
+
const res = await fetch(`https://huggingface.co/${hfId}/raw/main/README.md`, { signal: AbortSignal.timeout(30_000) });
|
|
108
|
+
if (!res.ok) throw new Error(`card HTTP ${res.status}`);
|
|
109
|
+
const md = await res.text();
|
|
110
|
+
fs.writeFileSync(cached, md);
|
|
111
|
+
return md;
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
async function verify(model, catalogueId) {
|
|
115
|
+
const problems = [];
|
|
116
|
+
const warnings = [];
|
|
117
|
+
if (!catalogue.has(catalogueId)) problems.push('id not in the catalogue');
|
|
118
|
+
if (model.skipped) return { problems, warnings: [`skipped: ${model.skipped}`] };
|
|
119
|
+
const hfId = details[catalogueId]?.hf?.id;
|
|
120
|
+
if (!hfId) return { problems, warnings: ['no Hugging Face card: not verifiable'] };
|
|
121
|
+
if (!model.evidence?.length) problems.push('evidence missing');
|
|
122
|
+
let md;
|
|
123
|
+
try {
|
|
124
|
+
md = await cardFor(hfId);
|
|
125
|
+
} catch (e) {
|
|
126
|
+
if (e instanceof Error) return { problems, warnings: [`card unavailable: ${e.message}`] };
|
|
127
|
+
return { problems, warnings: ['card unavailable'] };
|
|
128
|
+
}
|
|
129
|
+
const rows = tableRows(md);
|
|
130
|
+
const shortName = hfId.split('/').pop();
|
|
131
|
+
const modelNames = [shortName, catalogueId.split('/').pop(), shortName.replace(/-/g, '')];
|
|
132
|
+
|
|
133
|
+
const info = model.info ?? {};
|
|
134
|
+
if (info.params) {
|
|
135
|
+
const numbers = info.params.match(/\d+(?:\.\d+)?\s*[BT]/g) ?? [];
|
|
136
|
+
const missing = numbers.filter((n) => !md.replace(/\s/g, '').includes(n.replace(/\s/g, '')));
|
|
137
|
+
if (missing.length) problems.push(`params: numbers not in the card (${missing.join(', ')})`);
|
|
138
|
+
}
|
|
139
|
+
if (info.docUrl && checkLinks) {
|
|
140
|
+
try {
|
|
141
|
+
const res = await fetch(info.docUrl, { method: 'GET', signal: AbortSignal.timeout(20_000) });
|
|
142
|
+
if (!res.ok) problems.push(`docUrl: HTTP ${res.status}`);
|
|
143
|
+
} catch {
|
|
144
|
+
problems.push('docUrl: unreachable');
|
|
145
|
+
}
|
|
146
|
+
}
|
|
147
|
+
const sameNumber = (cell, value) => {
|
|
148
|
+
const parsed = Number(String(cell).replace(/[^0-9.\-]/g, ''));
|
|
149
|
+
return Number.isFinite(parsed) && Math.abs(parsed - value) < 1e-9;
|
|
150
|
+
};
|
|
151
|
+
for (const bench of info.benchmarks ?? []) {
|
|
152
|
+
// A benchmark name can appear in more than one table (or in prose rows); accept the
|
|
153
|
+
// value when any row named after it carries it, then check the column of that row.
|
|
154
|
+
const candidates = rows
|
|
155
|
+
.map((r, index) => ({ r, index }))
|
|
156
|
+
.filter(({ r }) => r.toLowerCase().includes(String(bench.name).toLowerCase()) && /\d/.test(r));
|
|
157
|
+
if (candidates.length === 0) { problems.push(`${bench.name}: no matching row`); continue; }
|
|
158
|
+
const hit = candidates.find(({ r }) => cellsOf(r).some((c) => sameNumber(c, bench.value)));
|
|
159
|
+
if (!hit) { problems.push(`${bench.name}: value ${bench.value} not in any row named after it`); continue; }
|
|
160
|
+
const cells = cellsOf(hit.r);
|
|
161
|
+
const header = headerNaming(rows, hit.index, modelNames);
|
|
162
|
+
if (!header) { warnings.push(`${bench.name}: no header naming the model above the row — check by hand`); continue; }
|
|
163
|
+
const valueAt = cells.map((c, i) => (sameNumber(c, bench.value) ? i : -1)).filter((i) => i >= 0);
|
|
164
|
+
const owns = valueAt.some((i) => header[i] && namesModel(header[i], modelNames));
|
|
165
|
+
if (!owns) problems.push(`${bench.name}: value sits under "${valueAt.map((i) => header[i] ?? '?').join(', ')}"`);
|
|
166
|
+
}
|
|
167
|
+
return { problems, warnings };
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
const raw = input === '-' ? fs.readFileSync(0, 'utf-8') : fs.readFileSync(input, 'utf-8');
|
|
171
|
+
let models;
|
|
172
|
+
try {
|
|
173
|
+
const parsed = JSON.parse(raw);
|
|
174
|
+
models = Array.isArray(parsed) ? parsed : Object.values(parsed);
|
|
175
|
+
} catch {
|
|
176
|
+
models = proposalObjects(raw);
|
|
177
|
+
}
|
|
178
|
+
|
|
179
|
+
let clean = 0, flagged = 0, warned = 0;
|
|
180
|
+
for (const model of models) {
|
|
181
|
+
const { problems, warnings } = await verify(model, model.id);
|
|
182
|
+
if (problems.length) {
|
|
183
|
+
flagged++;
|
|
184
|
+
console.log(`✗ ${model.id}\n ${problems.join('\n ')}`);
|
|
185
|
+
} else if (warnings.length) {
|
|
186
|
+
warned++;
|
|
187
|
+
console.log(`! ${model.id}\n ${warnings.join('\n ')}`);
|
|
188
|
+
} else {
|
|
189
|
+
clean++;
|
|
190
|
+
console.log(`✓ ${model.id}`);
|
|
191
|
+
}
|
|
192
|
+
}
|
|
193
|
+
console.log(`\n${models.length} proposal(s): ${clean} verified, ${warned} to check by hand, ${flagged} rejected`);
|
|
194
|
+
process.exit(flagged > 0 ? 1 : 0);
|
package/scripts/restore.sh
CHANGED
|
@@ -1,82 +1,100 @@
|
|
|
1
1
|
#!/usr/bin/env bash
|
|
2
|
-
# restore.sh —
|
|
2
|
+
# restore.sh — restore Rev4a's data directory from a backup.sh archive
|
|
3
3
|
#
|
|
4
4
|
# Usage:
|
|
5
|
-
#
|
|
5
|
+
# bash scripts/restore.sh ~/rev4a-backups/rev4a-backup-20260926-120000.tar.gz
|
|
6
6
|
#
|
|
7
|
-
#
|
|
8
|
-
#
|
|
9
|
-
#
|
|
10
|
-
# - data/agents-token.json
|
|
11
|
-
# - models.config.json
|
|
12
|
-
# - .env
|
|
7
|
+
# Restores into $REV4A_DATA_DIR when set, otherwise ~/.config/rev4a — the directory
|
|
8
|
+
# backup.sh archived. Run it as the user Rev4a runs as: under `sudo` HOME becomes /root
|
|
9
|
+
# and REV4A_DATA_DIR is dropped, so pass it explicitly (`sudo REV4A_DATA_DIR=… bash …`).
|
|
13
10
|
#
|
|
14
|
-
#
|
|
11
|
+
# The data directory is replaced as a whole, not overwritten file by file: a file the
|
|
12
|
+
# backup does not have (a leftover SQLite -wal, for one) would otherwise be mixed into
|
|
13
|
+
# the restored state. The current contents are archived first into ~/rev4a-backups, so a
|
|
14
|
+
# restore can itself be undone.
|
|
15
|
+
#
|
|
16
|
+
# Rev4a must not be running. When the rev4a systemd service is active it is stopped (and
|
|
17
|
+
# must be started again afterwards); a `rev4a serve` started any other way makes the
|
|
18
|
+
# script refuse until it is stopped.
|
|
15
19
|
set -euo pipefail
|
|
16
20
|
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
21
|
+
DATA_DIR="${REV4A_DATA_DIR:-${HOME}/.config/rev4a}"
|
|
22
|
+
PRE_DIR="${HOME}/rev4a-backups"
|
|
20
23
|
ARCHIVE="${1:-}"
|
|
21
24
|
|
|
22
25
|
if [[ -z "$ARCHIVE" ]]; then
|
|
23
|
-
echo "Usage:
|
|
26
|
+
echo "Usage: bash scripts/restore.sh <rev4a-backup-….tar.gz>"
|
|
24
27
|
echo ""
|
|
25
|
-
echo "
|
|
26
|
-
|
|
27
|
-
ls -lh "${REV4A_ROOT}/backups/"*.tar.gz 2>/dev/null || echo " (none)"
|
|
28
|
-
else
|
|
29
|
-
echo " (no backups directory)"
|
|
30
|
-
fi
|
|
28
|
+
echo "Backups in ~/rev4a-backups:"
|
|
29
|
+
ls -lh "${HOME}/rev4a-backups/"rev4a-backup-*.tar.gz 2>/dev/null || echo " (none)"
|
|
31
30
|
exit 1
|
|
32
31
|
fi
|
|
33
|
-
|
|
34
32
|
if [[ ! -f "$ARCHIVE" ]]; then
|
|
35
|
-
echo "Error:
|
|
33
|
+
echo "Error: archive not found: $ARCHIVE" >&2
|
|
36
34
|
exit 1
|
|
37
35
|
fi
|
|
36
|
+
if [[ ! -d "$DATA_DIR" ]]; then
|
|
37
|
+
# Restoring into a directory nothing reads would "succeed" and change nothing. On a new
|
|
38
|
+
# machine, create it first — the check is only there to catch a wrong path.
|
|
39
|
+
echo "Error: Rev4a data directory not found: $DATA_DIR" >&2
|
|
40
|
+
echo "Set REV4A_DATA_DIR to the directory Rev4a uses, or create it first on a new machine." >&2
|
|
41
|
+
exit 1
|
|
42
|
+
fi
|
|
43
|
+
# The real directory: when the path is a symlink, the swap below replaces its target.
|
|
44
|
+
DATA_DIR="$(cd "$DATA_DIR" && pwd -P)"
|
|
38
45
|
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
done
|
|
46
|
+
# Refuse an archive that is not a backup.sh archive of a Rev4a data directory. The listing
|
|
47
|
+
# is read whole: `grep -q` would stop reading early and, with pipefail, turn tar's SIGPIPE
|
|
48
|
+
# into a false refusal on large archives.
|
|
49
|
+
LISTING="$(tar -tvzf "$ARCHIVE")"
|
|
50
|
+
if ! grep -E '[[:space:]]\./(\.env|data/)' <<<"$LISTING" >/dev/null; then
|
|
51
|
+
echo "Error: $ARCHIVE does not look like a Rev4a data backup (no .env or data/ inside)." >&2
|
|
52
|
+
exit 1
|
|
53
|
+
fi
|
|
54
|
+
# backup.sh never stores links; a link in the archive could point outside the data directory.
|
|
55
|
+
if grep -E '^[lh]' <<<"$LISTING" >/dev/null; then
|
|
56
|
+
echo "Error: $ARCHIVE contains symbolic or hard links — refusing to restore it." >&2
|
|
57
|
+
exit 1
|
|
58
|
+
fi
|
|
53
59
|
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
60
|
+
if command -v systemctl >/dev/null 2>&1 && systemctl is-active --quiet rev4a.service 2>/dev/null; then
|
|
61
|
+
echo "Stopping rev4a.service..."
|
|
62
|
+
systemctl stop rev4a.service
|
|
63
|
+
STOPPED=1
|
|
64
|
+
fi
|
|
65
|
+
if pgrep -f 'rev4a(\.js)? serve' >/dev/null 2>&1; then
|
|
66
|
+
echo "Error: Rev4a is running (rev4a serve). Stop it first: it keeps the databases open." >&2
|
|
67
|
+
exit 1
|
|
59
68
|
fi
|
|
60
69
|
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
70
|
+
umask 077
|
|
71
|
+
mkdir -p "$PRE_DIR"
|
|
72
|
+
PRE="${PRE_DIR}/rev4a-pre-restore-$(date -u +%Y%m%d-%H%M%S).tar.gz"
|
|
73
|
+
echo "Archiving the current state first: $PRE"
|
|
74
|
+
set -o noclobber
|
|
75
|
+
tar -C "$DATA_DIR" -czf - . > "$PRE"
|
|
76
|
+
set +o noclobber
|
|
77
|
+
chmod 600 "$PRE"
|
|
64
78
|
|
|
65
|
-
#
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
79
|
+
# Extract next to the data directory, then swap: a failed extraction leaves the current
|
|
80
|
+
# data untouched, and nothing from the current state survives into the restored one.
|
|
81
|
+
PARENT="$(dirname "$DATA_DIR")"
|
|
82
|
+
STAGE="$(mktemp -d "${PARENT}/.rev4a-restore.XXXXXX")"
|
|
83
|
+
trap 'rm -rf "$STAGE"' EXIT
|
|
84
|
+
echo "Restoring into: $DATA_DIR"
|
|
85
|
+
tar -C "$STAGE" --no-same-owner -xzf "$ARCHIVE"
|
|
86
|
+
find "$STAGE" -type d -exec chmod 700 {} +
|
|
87
|
+
[[ -f "$STAGE/.env" ]] && chmod 600 "$STAGE/.env"
|
|
88
|
+
[[ -d "$STAGE/data" ]] && find "$STAGE/data" -type f -exec chmod 600 {} +
|
|
89
|
+
|
|
90
|
+
OLD="${PARENT}/.rev4a-replaced.$$"
|
|
91
|
+
mv "$DATA_DIR" "$OLD"
|
|
92
|
+
mv "$STAGE" "$DATA_DIR"
|
|
93
|
+
trap - EXIT
|
|
94
|
+
rm -rf "$OLD"
|
|
69
95
|
|
|
70
96
|
echo ""
|
|
71
|
-
echo "
|
|
72
|
-
echo "
|
|
73
|
-
echo "
|
|
74
|
-
|
|
75
|
-
echo " Previous state saved to: ${PRE_DIR}"
|
|
76
|
-
echo ""
|
|
77
|
-
echo " To restart Rev4a:"
|
|
78
|
-
echo " sudo systemctl start rev4a.service"
|
|
79
|
-
echo ""
|
|
80
|
-
echo " If something went wrong, restore previous state from:"
|
|
81
|
-
echo " ${PRE_DIR}"
|
|
82
|
-
echo "=============================================="
|
|
97
|
+
echo "Restore complete."
|
|
98
|
+
echo "The previous state is in: $PRE"
|
|
99
|
+
[[ -n "${STOPPED:-}" ]] && echo "Start Rev4a again: systemctl start rev4a.service"
|
|
100
|
+
exit 0
|