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:
@@ -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`);
|
||||
Reference in New Issue
Block a user