clawdie-ai/scripts/gen-changelog.ts
Charlie Root 9498ad28bd fix: replace hardcoded 'clawdie' with AGENT_NAME across 22 files
All hardcoded 'clawdie' references in production code now derive from
AGENT_NAME (default: 'clawdie'). This makes the mevy canary strategy
reliable — changing AGENT_NAME is all that's needed.

Changes:
- Hardcoded paths: CMS_WEBROOT, ASTRO_SITE_PATH, verify checks,
  controlplane dashboard dir, sessions dir, output dir, chown user
- Prometheus metrics: prefixed with AGENT_NAME for multi-install dashboards
- hostd log strings: use AGENT_NAME instead of 'clawdie-hostd'
- MCP server name: derived from AGENT_NAME
- Skill modify patches: container image and mount allowlist use AGENT_NAME
- SQL migration file renamed: clawdie-brain-hybrid-upgrade → brain-hybrid-upgrade
- Temp dir prefixes: all use AGENT_NAME

Kept as-is (correct pattern):
- 'clawdie' as default fallback when AGENT_NAME is unset
- .pi/extensions/clawdie-harness/ directory (pi package identity)
- html/docs-clawdie-si/ (public docs site URL)

---
Build: pass | Tests: pass — 1527 passed, 3 failed (2 files, pre-existing)
2026-04-15 21:41:41 +00:00

395 lines
13 KiB
TypeScript

/**
* Generate changelog HTML from annotated git tags.
*
* Usage:
* npm run gen-changelog
* npx tsx scripts/gen-changelog.ts
*
* Run after each release tag:
* git tag -a v0.6.0 -m "v0.6.0 - Release Name\n\n- key change\n- key change"
* npm run gen-changelog
* git add html/docs-clawdie-si/changelog.html && git commit -m "docs: regenerate changelog for v0.6.0"
*/
import { execSync } from 'child_process';
import fs from 'fs';
import path from 'path';
import { formatDisplayDate } from '../src/display-date.js';
const OUT_FILE = path.join(
process.cwd(),
'html/docs-clawdie-si/changelog.html',
);
function run(cmd: string): string {
try {
return execSync(cmd, {
encoding: 'utf-8',
stdio: ['ignore', 'pipe', 'pipe'],
}).trim();
} catch {
return '';
}
}
interface Commit {
hash: string;
message: string;
}
interface Release {
tag: string;
version: string;
name: string;
date: string;
body: string;
commits: Commit[];
}
function getReleases(): Release[] {
const tagList = run('git tag --sort=-version:refname')
.split('\n')
.filter(Boolean);
if (!tagList.length) {
console.log('No git tags found. Tag a release first:');
console.log(' git tag -a v0.5.0 -m "v0.5.0 - Release Name"');
process.exit(0);
}
return tagList.map((tag, i): Release => {
const prev = tagList[i + 1] ?? '';
// Date of the tag commit
const date = formatDisplayDate(run(`git log -1 --format=%aI "${tag}"`), {
includeTime: false,
});
// Annotated tag subject and body (lightweight tags return empty subject)
const subject = run(`git tag -l --format='%(subject)' "${tag}"`);
const body = run(`git tag -l --format='%(body)' "${tag}"`);
// Strip leading version prefix "v0.5.0 - " or "v0.5.0: "
const name = subject.replace(/^v?\d+\.\d+\.\d+[-:\s]+/, '').trim() || tag;
// Commits included in this release
const range = prev ? `${prev}..${tag}` : tag;
const lines = run(`git log ${range} --oneline --no-merges`)
.split('\n')
.filter(Boolean);
const commits: Commit[] = lines.map((line) => {
const sp = line.indexOf(' ');
return { hash: line.slice(0, sp), message: line.slice(sp + 1) };
});
return { tag, version: tag.replace(/^v/, ''), name, date, body, commits };
});
}
// Parse conventional commit prefix
function parseType(message: string): {
type: string;
scope: string;
rest: string;
} {
const m = message.match(
/^(feat|fix|docs|design|chore|refactor|perf|test|ci|build|style)(\(([^)]+)\))?!?:\s*(.*)/,
);
if (m) return { type: m[1], scope: m[3] ?? '', rest: m[4] };
return { type: '', scope: '', rest: message };
}
const TYPE_COLOR: Record<string, string> = {
feat: '#4ade80',
fix: '#f87171',
design: '#c8922a',
docs: '#00b4d8',
refactor: '#a78bfa',
chore: '#6e7d8f',
build: '#6e7d8f',
ci: '#6e7d8f',
perf: '#fcd34d',
test: '#6e7d8f',
style: '#6e7d8f',
};
function esc(s: string): string {
return s.replace(/&/g, '&amp;').replace(/</g, '&lt;').replace(/>/g, '&gt;');
}
function renderCommitList(commits: Commit[]): string {
if (!commits.length)
return '<p style="color:var(--muted);font-size:0.85rem;margin:0.8rem 0">No commits in this range.</p>';
return `<ul class="cl-commits">\n${commits
.map((c) => {
const { type, scope, rest } = parseType(c.message);
const typeHtml = type
? `<span class="cl-type" style="color:${TYPE_COLOR[type] ?? '#6e7d8f'};border-color:${TYPE_COLOR[type] ?? '#6e7d8f'}33">${type}${scope ? `(${esc(scope)})` : ''}</span>`
: '';
return ` <li><span class="cl-hash">${esc(c.hash)}</span>${typeHtml}${esc(rest)}</li>`;
})
.join('\n')}\n </ul>`;
}
function renderSections(releases: Release[]): string {
return releases
.map(
(r, i) => ` <section>
<div class="cl-release">
<div class="cl-release-header">
<span class="cl-version">${esc(r.tag)}</span>
<span class="cl-release-name">${esc(r.name)}</span>
<span class="cl-date">${esc(r.date)}</span>
</div>
${r.body ? `<p class="cl-body">${esc(r.body).replace(/\n/g, '<br>')}</p>` : ''}
${renderCommitList(r.commits)}
</div>
</section>${i < releases.length - 1 ? '\n\n <div class="divider"></div>' : ''}`,
)
.join('\n\n');
}
function buildHtml(releases: Release[]): string {
const generated = formatDisplayDate(new Date(), { includeTime: false });
return `<!doctype html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>Changelog — Clawdie Docs</title>
<link rel="preconnect" href="https://fonts.googleapis.com" />
<link
href="https://fonts.googleapis.com/css2?family=Cormorant+Garamond:ital,wght@0,300;0,400;0,600;1,300;1,400&family=DM+Mono:wght@300;400&display=swap"
rel="stylesheet"
/>
<link rel="stylesheet" href="/css/shared.css" />
<style>
.cl-release-header {
display: flex;
align-items: baseline;
gap: 1rem;
flex-wrap: wrap;
margin-bottom: 1.2rem;
padding-bottom: 0.8rem;
border-bottom: 1px solid var(--grey-light, #e5e5e5);
}
.cl-version {
font-family: 'DM Mono', monospace;
font-size: 1rem;
color: var(--amber, #b58900);
letter-spacing: 0.06em;
flex-shrink: 0;
}
.cl-release-name {
font-family: 'Cormorant Garamond', Georgia, serif;
font-size: 1.5rem;
font-style: italic;
font-weight: 300;
color: var(--charcoal, #333);
line-height: 1.2;
}
.cl-date {
font-family: 'DM Mono', monospace;
font-size: 0.6rem;
letter-spacing: 0.12em;
color: var(--muted, #888);
text-transform: uppercase;
margin-left: auto;
}
.cl-body {
font-size: 0.9rem;
color: var(--grey, #666);
font-style: italic;
margin-bottom: 1rem;
line-height: 1.7;
}
.cl-commits {
list-style: none;
padding: 0;
margin: 0;
}
.cl-commits li {
padding: 0.42rem 0;
border-bottom: 1px solid var(--grey-light, #e5e5e5);
font-size: 0.87rem;
font-weight: 300;
color: var(--ink, #222);
display: flex;
align-items: baseline;
gap: 0.5rem;
flex-wrap: wrap;
line-height: 1.5;
}
.cl-hash {
font-family: 'DM Mono', monospace;
font-size: 0.65rem;
color: var(--muted, #888);
flex-shrink: 0;
letter-spacing: 0.04em;
}
.cl-type {
font-family: 'DM Mono', monospace;
font-size: 0.57rem;
letter-spacing: 0.1em;
text-transform: uppercase;
padding: 0.08rem 0.4rem;
flex-shrink: 0;
border: 1px solid;
}
</style>
</head>
<body>
<div class="hex-bg"></div>
<div class="sidebar-overlay" id="overlay"></div>
<header class="top-bar">
<button class="mobile-menu-btn" id="menuBtn" aria-label="Toggle menu">&#9776;</button>
<a href="/" class="brand"><span>&#9651;</span> Clawdie Docs</a>
<div class="nav-links">
<a href="https://clawdie.si">Home</a>
<a href="https://codeberg.org/Clawdie" target="_blank" rel="noopener">Source</a>
</div>
</header>
<div class="docs-layout">
<nav class="sidebar" id="sidebar">
<div class="sidebar-section">
<span class="section-label">Getting Started</span>
<ul>
<li><a href="/">Introduction</a></li>
<li><a href="/docs/install.html">Installation</a></li>
<li><a href="/docs/iso.html">ISO Install</a></li>
<li><a href="/docs/split-brain.html">Split Brain</a></li>
</ul>
</div>
<div class="sidebar-section">
<span class="section-label">Architecture</span>
<ul>
<li><a href="/docs/">How It Works</a></li>
<li><a href="/docs/#jails-not-docker">Jails, Not Docker</a></li>
<li><a href="/docs/#wayland-first-display">Wayland Display</a></li>
<li><a href="/docs/#prompt-injection-and-web-browsing">Prompt Injection</a></li>
<li><a href="/guides/nanoclaw-upstream.html">NanoClaw Upstream</a></li>
<li><a href="https://codeberg.org/Clawdie/Clawdie-AI/src/branch/main/docs/public/operate/monitoring.md" target="_blank" rel="noopener">Monitoring</a></li>
<li><a href="https://codeberg.org/Clawdie/Clawdie-AI/src/branch/main/docs/public/operate/security.md" target="_blank" rel="noopener">Security</a></li>
</ul>
</div>
<div class="sidebar-section">
<span class="section-label">Setup Guides</span>
<ul>
<li><a href="/guides/nginx-ssl.html">Nginx + SSL</a></li>
<li><a href="/guides/tailscale-vpn.html">Tailscale VPN</a></li>
</ul>
</div>
<div class="sidebar-section">
<span class="section-label">Integrations</span>
<ul>
<li><a href="/guides/stripe-agents.html">Stripe Agents</a></li>
<li><a href="/guides/protonmail.html">ProtonMail</a></li>
</ul>
</div>
<div class="sidebar-section">
<span class="section-label">Project</span>
<ul>
<li><a href="/changelog.html" class="active">Changelog</a></li>
<li><a href="/license.html">License</a></li>
</ul>
</div>
</nav>
<main class="content">
<div class="breadcrumb">
<a href="/">Home</a><span class="sep">/</span>
Changelog
</div>
<div class="page-header">
<h1>Change<span>log</span></h1>
<p class="subtitle">Tagged releases &mdash; what changed and why</p>
</div>
<p>
Annotated git tags, regenerated with <code>npm run gen-changelog</code> on each release.
Full history at <a href="https://codeberg.org/Clawdie/Clawdie-AI/commits/branch/main" target="_blank" rel="noopener">Codeberg</a>.
</p>
<div class="divider"></div>
${renderSections(releases)}
<footer>
<div class="footer-left">
<a href="https://clawdie.si">Clawdie AI</a> &middot;
<a href="https://codeberg.org/Clawdie/Clawdie-AI/src/branch/main/html/docs-clawdie-si/changelog.html" target="_blank" rel="noopener">Page source</a><br>
<a href="https://codeberg.org/Clawdie/Clawdie-AI/src/branch/main/CHANGELOG.md" target="_blank" rel="noopener">Release notes</a>
&middot;
<a href="https://codeberg.org/Clawdie/Clawdie-AI/src/branch/main/README.md" target="_blank" rel="noopener">Project context</a><br>
<a href="https://osa.smilepowered.org" target="_blank" rel="noopener">OSA — Mission Statement</a><br>
Generated ${generated}
</div>
<div class="footer-hex">&#9651;</div>
</footer>
</main>
<aside class="toc">
<p class="toc-title">On this page</p>
<nav id="toc-list"></nav>
</aside>
</div>
<script>
const toc = document.getElementById('toc-list');
if (toc) {
document.querySelectorAll('.content h2, .content h3').forEach((h) => {
if (!h.id)
h.id = h.textContent.trim().toLowerCase().replace(/[^a-z0-9]+/g, '-');
const a = document.createElement('a');
a.href = '#' + h.id;
a.className = 'toc-link' + (h.tagName === 'H3' ? ' toc-sub' : '');
a.textContent = h.textContent;
toc.appendChild(a);
});
}
const observer = new IntersectionObserver(
(entries) => {
entries.forEach((e, i) => {
if (e.isIntersecting)
setTimeout(() => e.target.classList.add('visible'), i * 80);
});
},
{ threshold: 0.08 },
);
document.querySelectorAll('section').forEach((s) => observer.observe(s));
const menuBtn = document.getElementById('menuBtn');
const sidebar = document.getElementById('sidebar');
const overlay = document.getElementById('overlay');
if (menuBtn && sidebar) {
menuBtn.addEventListener('click', () => {
sidebar.classList.toggle('open');
overlay?.classList.toggle('open');
});
overlay?.addEventListener('click', () => {
sidebar.classList.remove('open');
overlay.classList.remove('open');
});
}
</script>
</body>
</html>
`;
}
const releases = getReleases();
const html = buildHtml(releases);
fs.writeFileSync(OUT_FILE, html);
console.log(
`${OUT_FILE}${releases.length} release${releases.length !== 1 ? 's' : ''}`,
);
releases.forEach((r) =>
console.log(
` ${r.tag} ${r.date} ${r.commits.length} commit(s) — ${r.name}`,
),
);