// 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(`(?