feat(docs,content,ui): the documentation, twelve modules with the synced config beside the text, search, layers, themes and screenshots; the mock vault as templates

This commit is contained in:
hh
2026-09-05 04:57:37 +02:00
parent 2fcbbb1bf8
commit 1fbc8ed59d
296 changed files with 13337 additions and 1097 deletions
+301
View File
@@ -0,0 +1,301 @@
// bun scripts/sync-content.mjs
//
// Copies the pieces of the Beaver stack that carry no personal data into
// src/content/: prompt granules and skills from the vault, config modules and
// examples from beaver-agent and beaver-gateway. Every source is listed here by
// hand and copied verbatim - nothing is rewritten on the way out.
//
// A file that would need rewriting to be publishable is NOT synced. Its
// approximate, hand-written counterpart lives at its real vault path inside
// src/content/{ru,en}/vault/, says what kind of thing belongs there, and is
// maintained by hand. See README.
//
// The scan below is the backstop, not the mechanism: it aborts the sync when a
// name from `👤 люди/`, a handle, an id or a host reaches src/content/. Neither
// list lives in this repo: names are read from the vault at run time, the rest
// from a file outside the checkout (BEAVER_FORBIDDEN, default
// ~/.config/beaver-land/forbidden.txt).
import { existsSync, mkdirSync, readFileSync, readdirSync, rmSync, writeFileSync } from 'node:fs';
import { basename, dirname, join, resolve } from 'node:path';
const VAULT = process.env.BEAVER_VAULT ?? `${process.env.HOME}/Documents/obsidian/hh`;
const ZONE = join(VAULT, 'мета/бобер');
const REPOS = resolve(import.meta.dirname, '../..');
const OUT = resolve(import.meta.dirname, '../src/content');
const SYNCED = join(OUT, 'synced');
const FORBIDDEN_FILE =
process.env.BEAVER_FORBIDDEN ?? `${process.env.HOME}/.config/beaver-land/forbidden.txt`;
// [source, target under src/content]
const FILES = [
// voice + quick overlay: the Raycast prompt is assembled from these
['vault', 'промпты/гранулы/голос/role.md', 'prompts/voice/role.md'],
['vault', 'промпты/гранулы/голос/philosophy.md', 'prompts/voice/philosophy.md'],
['vault', 'промпты/гранулы/голос/user_profile.md', 'prompts/voice/user_profile.md'],
['vault', 'промпты/гранулы/голос/operating_modes.md', 'prompts/voice/operating_modes.md'],
['vault', 'промпты/гранулы/голос/how_you_operate.md', 'prompts/voice/how_you_operate.md'],
[
'vault',
'промпты/гранулы/голос/interaction_guidelines.md',
'prompts/voice/interaction_guidelines.md'
],
['vault', 'промпты/гранулы/быстрый.md', 'prompts/overlays/быстрый.md'],
// overlays and environments
['vault', 'промпты/гранулы/ветка.md', 'prompts/overlays/ветка.md'],
['vault', 'промпты/гранулы/глубокий.md', 'prompts/overlays/глубокий.md'],
['vault', 'промпты/гранулы/триаж.md', 'prompts/overlays/триаж.md'],
['vault', 'промпты/гранулы/хендаут.md', 'prompts/overlays/хендаут.md'],
['vault', 'промпты/гранулы/слив.md', 'prompts/overlays/слив.md'],
['vault', 'промпты/гранулы/карта-vault.md', 'prompts/карта-vault.md'],
['vault', 'промпты/гранулы/окружения/мастер.md', 'prompts/environments/мастер.md'],
['vault', 'промпты/гранулы/окружения/ветка.md', 'prompts/environments/ветка.md'],
['vault', 'промпты/гранулы/окружения/глубокий.md', 'prompts/environments/глубокий.md'],
['vault', 'промпты/гранулы/окружения/джоб.md', 'prompts/environments/джоб.md'],
// skills
['vault', 'скиллы/общие/чаты/SKILL.md', 'skills/общие/чаты/SKILL.md'],
['vault', 'скиллы/vault/графики/SKILL.md', 'skills/vault/графики/SKILL.md'],
// beaver-agent: the real setup
['repo', 'beaver-agent/config.py', 'code/beaver-agent/config.py'],
['repo', 'beaver-agent/beaver_agent/vault.py', 'code/beaver-agent/beaver_agent/vault.py'],
['repo', 'beaver-agent/beaver_agent/prompts.py', 'code/beaver-agent/beaver_agent/prompts.py'],
['repo', 'beaver-agent/beaver_agent/skills.py', 'code/beaver-agent/beaver_agent/skills.py'],
['repo', 'beaver-agent/beaver_agent/agents.py', 'code/beaver-agent/beaver_agent/agents.py'],
['repo', 'beaver-agent/beaver_agent/frontends.py', 'code/beaver-agent/beaver_agent/frontends.py'],
['repo', 'beaver-agent/beaver_agent/policy.py', 'code/beaver-agent/beaver_agent/policy.py'],
['repo', 'beaver-agent/beaver_agent/texts.py', 'code/beaver-agent/beaver_agent/texts.py'],
[
'repo',
'beaver-agent/beaver_agent/hands/__init__.py',
'code/beaver-agent/beaver_agent/hands/__init__.py'
],
[
'repo',
'beaver-agent/beaver_agent/hands/komodo.py',
'code/beaver-agent/beaver_agent/hands/komodo.py'
],
[
'repo',
'beaver-agent/beaver_agent/hands/homeassistant.py',
'code/beaver-agent/beaver_agent/hands/homeassistant.py'
],
[
'repo',
'beaver-agent/beaver_agent/hands/calendars.py',
'code/beaver-agent/beaver_agent/hands/calendars.py'
],
[
'repo',
'beaver-agent/beaver_agent/memory/watch.py',
'code/beaver-agent/beaver_agent/memory/watch.py'
],
[
'repo',
'beaver-agent/beaver_agent/memory/recall.py',
'code/beaver-agent/beaver_agent/memory/recall.py'
],
[
'repo',
'beaver-agent/beaver_agent/memory/handouts.py',
'code/beaver-agent/beaver_agent/memory/handouts.py'
],
[
'repo',
'beaver-agent/beaver_agent/memory/curator.py',
'code/beaver-agent/beaver_agent/memory/curator.py'
],
[
'repo',
'beaver-agent/beaver_agent/jobs/__init__.py',
'code/beaver-agent/beaver_agent/jobs/__init__.py'
],
[
'repo',
'beaver-agent/beaver_agent/jobs/rotation.py',
'code/beaver-agent/beaver_agent/jobs/rotation.py'
],
[
'repo',
'beaver-agent/beaver_agent/jobs/closing.py',
'code/beaver-agent/beaver_agent/jobs/closing.py'
],
[
'repo',
'beaver-agent/beaver_agent/jobs/memory.py',
'code/beaver-agent/beaver_agent/jobs/memory.py'
],
[
'repo',
'beaver-agent/beaver_agent/jobs/deploy.py',
'code/beaver-agent/beaver_agent/jobs/deploy.py'
],
[
'repo',
'beaver-agent/beaver_agent/jobs/t3code.py',
'code/beaver-agent/beaver_agent/jobs/t3code.py'
],
[
'repo',
'beaver-agent/beaver_agent/jobs/vibegram.py',
'code/beaver-agent/beaver_agent/jobs/vibegram.py'
],
['repo', 'beaver-agent/.env.example', 'code/beaver-agent/.env.example'],
['repo', 'beaver-agent/caddy/site.caddy', 'code/beaver-agent/caddy/site.caddy'],
['repo', 'beaver-agent/Makefile', 'code/beaver-agent/Makefile'],
// beaver-gateway: primitives
['repo', 'beaver-gateway/examples/config.py', 'code/beaver-gateway/examples/config.py'],
[
'repo',
'beaver-gateway/examples/docker-compose.yml',
'code/beaver-gateway/examples/docker-compose.yml'
],
['repo', 'beaver-gateway/src/beaver_gateway/config.py', 'code/beaver-gateway/config.py'],
[
'repo',
'beaver-gateway/src/beaver_gateway/agents/prompts.py',
'code/beaver-gateway/agents/prompts.py'
],
[
'repo',
'beaver-gateway/src/beaver_gateway/conversations/rotation.py',
'code/beaver-gateway/conversations/rotation.py'
],
[
'repo',
'beaver-gateway/src/beaver_gateway/conversations/injects.py',
'code/beaver-gateway/conversations/injects.py'
],
[
'repo',
'beaver-gateway/src/beaver_gateway/conversations/distill.py',
'code/beaver-gateway/conversations/distill.py'
],
['repo', 'beaver-gateway/src/beaver_gateway/jobs/job.py', 'code/beaver-gateway/jobs/job.py'],
[
'repo',
'beaver-gateway/src/beaver_gateway/agents/policy.py',
'code/beaver-gateway/agents/policy.py'
],
['repo', 'beaver-gateway/src/beaver_gateway/mcp/types.py', 'code/beaver-gateway/mcp/types.py'],
['repo', 'beaver-plugin-obsidian/src/settings.ts', 'code/beaver-plugin-obsidian/settings.ts']
];
// Anything the scan finds in an output file aborts the sync. Names come from
// the vault at run time (the first name of every card and alias in `👤 люди/`);
// hosts, ids and nicknames come from FORBIDDEN_FILE. Without that file the
// backstop is blind, so its absence is an error, not a warning.
function forbiddenStatic() {
if (!existsSync(FORBIDDEN_FILE)) {
throw new Error(`sync: ${FORBIDDEN_FILE} is missing (BEAVER_FORBIDDEN overrides the path)`);
}
return readFileSync(FORBIDDEN_FILE, 'utf8')
.split('\n')
.map((line) => line.trim())
.filter((line) => line && !line.startsWith('#'));
}
function firstName(text) {
const [head] = text.split(/[^\p{L}]+/u).filter(Boolean);
return head && head.length >= 4 && /^\p{Lu}/u.test(head) ? [head] : [];
}
function peopleNames() {
const dir = join(VAULT, '\u{1F464} \u043B\u044E\u0434\u0438');
const out = new Set();
const walk = (path) => {
for (const entry of readdirSync(path, { withFileTypes: true })) {
const child = join(path, entry.name);
if (entry.isDirectory()) {
walk(child);
continue;
}
if (!entry.name.endsWith('.md')) continue;
const stem = entry.name.slice(0, -3);
if (stem === basename(path)) continue;
for (const part of firstName(stem)) out.add(part);
const head = readFileSync(child, 'utf8').slice(0, 2000);
for (const handle of head.match(/@[A-Za-z0-9_]{4,}/g) ?? []) out.add(handle);
for (const mail of head.match(/[\w.+-]+@[\w-]+\.[a-z]{2,}/gi) ?? []) out.add(mail);
const aliases = head.match(/^aliases:\s*(.*)$/m);
if (!aliases) continue;
const inline = aliases[1].trim().replace(/^\[|\]$/g, '');
const items = inline
? inline.split(',')
: head
.split(/^aliases:/m)[1]
.split(/\n(?!\s*-)/)[0]
.split('\n');
for (const item of items) {
const name = item
.replace(/^\s*-\s*/, '')
.trim()
.replace(/^["']|["']$/g, '');
if (name.includes(':')) continue;
for (const part of firstName(name)) out.add(part);
}
}
};
try {
walk(dir);
} catch {
console.warn('sync: no people directory, name scan limited to FORBIDDEN_FILE');
}
return [...out];
}
const FORBIDDEN = [...forbiddenStatic(), ...peopleNames()];
function read(kind, rel) {
const base = kind === 'vault' ? ZONE : REPOS;
return readFileSync(join(base, rel), 'utf8');
}
function check(target, text) {
const hits = FORBIDDEN.filter((word) =>
new RegExp(`(?<!\\p{L})${word.replace(/[.*+?^${}()|[\]\\]/g, '\\$&')}(?!\\p{L})`, 'u').test(
text
)
);
if (hits.length) throw new Error(`${target}: forbidden strings ${hits.join(', ')}`);
}
rmSync(SYNCED, { recursive: true, force: true });
let count = 0;
for (const [kind, rel, target] of FILES) {
const text = read(kind, rel);
check(target, text);
const path = join(SYNCED, target);
mkdirSync(dirname(path), { recursive: true });
writeFileSync(path, text);
count += 1;
}
// The hand-written trees (the mock vaults with their approximations, the
// translations) are never rewritten here - only scanned, so a name cannot creep
// in by hand either.
function scan(dir) {
let checked = 0;
for (const entry of readdirSync(dir, { withFileTypes: true })) {
const path = join(dir, entry.name);
if (entry.isDirectory()) {
checked += scan(path);
continue;
}
if (!/\.(md|ya?ml|py|ts|caddy|example)$/.test(entry.name) && entry.name !== 'Makefile')
continue;
check(path.slice(OUT.length + 1), readFileSync(path, 'utf8'));
checked += 1;
}
return checked;
}
let scanned = 0;
for (const dir of ['ru', 'en']) {
try {
scanned += scan(join(OUT, dir));
} catch (error) {
if (error.code !== 'ENOENT') throw error;
}
}
console.log(`synced ${count} files into ${SYNCED}, scanned ${scanned} hand-written ones`);