Files
wehub-resource-sync acf542cac6
Sync SKILL.md and rules / sync (push) Failing after 1s
chore: import upstream snapshot with attribution
2026-07-13 12:07:10 +08:00

298 lines
12 KiB
JavaScript

// caveman → OpenClaw install / uninstall helper.
//
// OpenClaw is a self-hosted gateway that orchestrates Claude Code, Codex,
// Pi, OpenCode, and others. It has its own workspace + skills system at
// ~/.openclaw/workspace/. Skills there appear in a compact list and are
// loaded on-demand by the model — they are NOT injected as system prompt
// each turn. The bootstrap files (AGENTS.md, SOUL.md, TOOLS.md, MEMORY.md)
// ARE injected each turn under "Project Context", subject to a 12K-per-file
// and 60K-total cap.
//
// To make caveman always-on through OpenClaw, we do two writes:
// 1. Drop a copy of skills/caveman/SKILL.md into <workspace>/skills/caveman/
// with OpenClaw-required frontmatter (`version`, `always: true`) merged
// in. Makes the skill discoverable via `openclaw skills list` and lets
// the orchestrated agent `read` it on demand.
// 2. Append a tiny marker-fenced bootstrap snippet to <workspace>/SOUL.md
// pointing the agent at the skill. SOUL.md is auto-injected each turn,
// so this is what actually drives always-on behavior.
//
// Idempotent on both writes. Uninstall removes the skill folder and strips
// the marker block from SOUL.md while preserving any user-authored content.
'use strict';
const fs = require('fs');
const os = require('os');
const path = require('path');
const SKILL_NAME = 'caveman';
const SKILL_VERSION = '1.0.0';
const MARK_BEGIN = '<!-- caveman-begin -->';
const MARK_END = '<!-- caveman-end -->';
const SOUL_FILE = 'SOUL.md';
function resolveWorkspace(env = process.env) {
if (env.OPENCLAW_WORKSPACE) return path.resolve(env.OPENCLAW_WORKSPACE);
return path.join(os.homedir(), '.openclaw', 'workspace');
}
function readIfExists(p) {
try { return fs.readFileSync(p, 'utf8'); } catch (_) { return null; }
}
// ── Frontmatter helpers ───────────────────────────────────────────────────
// Lightweight YAML merge — we only need to insert `version` and `always` if
// they're absent. Avoids pulling in a YAML dep for a job this small. The
// caveman SKILL.md uses block-scalar `description: >`, which a naive split
// would mangle — but since we're only ever appending top-level keys (never
// editing existing ones), a string-prepend after the leading `---\n` is safe.
function splitFrontmatter(src) {
if (!src.startsWith('---\n') && !src.startsWith('---\r\n')) {
return { frontmatter: '', body: src };
}
const after = src.slice(src.indexOf('\n') + 1);
const endRe = /(^|\n)---\s*(\r?\n|$)/;
const m = endRe.exec(after);
if (!m) return { frontmatter: '', body: src };
const fmEnd = m.index + (m[1] ? 1 : 0);
const fm = after.slice(0, fmEnd);
const rest = after.slice(m.index + m[0].length);
return { frontmatter: fm, body: rest };
}
function frontmatterHasKey(fm, key) {
const re = new RegExp('(^|\\n)' + key + '\\s*:', 'i');
return re.test(fm);
}
function mergeOpenclawFrontmatter(src) {
const { frontmatter, body } = splitFrontmatter(src);
const additions = [];
if (!frontmatterHasKey(frontmatter, 'name')) additions.push(`name: ${SKILL_NAME}`);
if (!frontmatterHasKey(frontmatter, 'version')) additions.push(`version: ${SKILL_VERSION}`);
if (!frontmatterHasKey(frontmatter, 'always')) additions.push('always: true');
if (additions.length === 0 && frontmatter) return src;
const fmBody = (frontmatter ? frontmatter.trimEnd() + '\n' : '') + additions.join('\n') + (additions.length ? '\n' : '');
return '---\n' + fmBody + '---\n' + body;
}
// ── Bootstrap snippet load ────────────────────────────────────────────────
function loadBootstrapSnippet(repoRoot) {
if (repoRoot) {
const p = path.join(repoRoot, 'src', 'rules', 'caveman-openclaw-bootstrap.md');
const body = readIfExists(p);
if (body) return body.endsWith('\n') ? body : body + '\n';
}
// Standalone fallback (curl|node case where there's no repo on disk).
// Keep this in sync with src/rules/caveman-openclaw-bootstrap.md.
return [
MARK_BEGIN,
'## Caveman mode (always on)',
'',
'Respond terse like smart caveman. All technical substance stay. Only fluff die.',
'',
"The full ruleset and intensity levels live in this workspace's caveman skill:",
'',
' skills/caveman/SKILL.md',
'',
'Default intensity: `full`. Switch with `/caveman lite|full|ultra|wenyan`.',
'Stop with: "stop caveman" / "normal mode" / "deactivate caveman".',
'',
'Auto-Clarity: drop caveman for security warnings, irreversible action',
'confirmations, multi-step sequences where fragments risk misread, or when',
'user is confused or repeating. Resume after.',
'',
'Boundaries: code, commit messages, and PR descriptions stay normal prose.',
MARK_END,
'',
].join('\n');
}
function loadSkillBody(repoRoot) {
if (!repoRoot) return null;
return readIfExists(path.join(repoRoot, 'skills', 'caveman', 'SKILL.md'));
}
// ── SOUL.md marker-block append/strip ─────────────────────────────────────
//
// Damage tolerance (#596): a stray or truncated marker (interrupted write,
// partial user edit) used to chain into data loss — append saw "no complete
// block" and added a SECOND block; strip then cut from the FIRST begin to the
// FIRST end, which spanned all user content between the stray marker and the
// appended block. The scan below pairs each begin with the nearest end BEFORE
// the next begin; an unpaired marker is removed as just the marker itself,
// never as a span over user content.
function stripAllBootstrapBlocks(text) {
let result = '';
let found = false;
let i = 0;
while (i < text.length) {
const b = text.indexOf(MARK_BEGIN, i);
if (b === -1) { result += text.slice(i); break; }
result += text.slice(i, b);
found = true;
const nextB = text.indexOf(MARK_BEGIN, b + MARK_BEGIN.length);
const e = text.indexOf(MARK_END, b + MARK_BEGIN.length);
if (e !== -1 && (nextB === -1 || e < nextB)) {
i = e + MARK_END.length; // well-formed block — drop begin..end inclusive
} else {
i = b + MARK_BEGIN.length; // orphan begin — drop only the marker itself
}
// Collapse the blank-line scar around the cut (same cosmetic rule the
// old single-cut code applied): keep at most one newline on each side.
result = result.replace(/\n+$/, '\n');
const lead = /^\n+/.exec(text.slice(i));
if (lead) i += lead[0].length - (result ? 1 : 0);
}
// Orphan end markers (begin already gone or never written) — drop marker only.
while (result.includes(MARK_END)) { found = true; result = result.replace(MARK_END, ''); }
return { next: result, found };
}
function appendBootstrapToSoul(soulPath, snippet) {
const existing = readIfExists(soulPath);
const count = (s, sub) => s.split(sub).length - 1;
let base = existing;
let repaired = false;
if (existing) {
const nb = count(existing, MARK_BEGIN);
const ne = count(existing, MARK_END);
if (nb === 1 && ne === 1 && existing.indexOf(MARK_END) > existing.indexOf(MARK_BEGIN)) {
return { changed: false, reason: 'already present' };
}
if (nb > 0 || ne > 0) {
// Damaged markers — strip them safely first, then append one clean block.
base = stripAllBootstrapBlocks(existing).next;
repaired = true;
}
}
let next;
if (base && base.length) {
const sep = base.endsWith('\n\n') ? '' : (base.endsWith('\n') ? '\n' : '\n\n');
next = base + sep + snippet;
} else {
next = snippet;
}
fs.writeFileSync(soulPath, next, { mode: 0o644 });
return repaired ? { changed: true, repaired: true } : { changed: true };
}
function stripBootstrapFromSoul(soulPath) {
const existing = readIfExists(soulPath);
if (!existing) return { changed: false, reason: 'no SOUL.md' };
const { next: stripped, found } = stripAllBootstrapBlocks(existing);
if (!found) return { changed: false, reason: 'no marker block' };
let next = stripped.trimEnd();
next = next ? next + '\n' : '';
if (next === '') {
// SOUL.md only contained our block — remove the file so OpenClaw doesn't
// bootstrap an empty section every turn.
try { fs.unlinkSync(soulPath); } catch (_) {}
return { changed: true, removed: true };
}
fs.writeFileSync(soulPath, next, { mode: 0o644 });
return { changed: true };
}
// ── Public API ────────────────────────────────────────────────────────────
function installOpenclaw({ workspace, repoRoot, dryRun = false, force = false, log = noopLog() } = {}) {
const ws = workspace || resolveWorkspace();
const skillBody = loadSkillBody(repoRoot);
if (!skillBody) {
log.warn(' openclaw install requires the caveman repo on disk (skills/caveman/SKILL.md missing).');
log.note(' Re-run from a clone or via `npx -y github:JuliusBrussee/caveman -- --only openclaw`.');
return { ok: false, reason: 'repo not available' };
}
const snippet = loadBootstrapSnippet(repoRoot);
if (!fs.existsSync(ws)) {
if (!force) {
log.warn(` openclaw workspace not found at ${ws}.`);
log.note(' Either install OpenClaw (https://openclaw.ai) and re-run, or pass --force to mkdir.');
return { ok: false, reason: 'workspace missing' };
}
if (!dryRun) fs.mkdirSync(ws, { recursive: true });
}
const skillDir = path.join(ws, 'skills', SKILL_NAME);
const skillFile = path.join(skillDir, 'SKILL.md');
const soulFile = path.join(ws, SOUL_FILE);
if (dryRun) {
log.note(` would write ${skillFile} (with version/always frontmatter)`);
log.note(` would ${fs.existsSync(soulFile) ? 'append to' : 'create'} ${soulFile} (caveman bootstrap block)`);
return { ok: true, dryRun: true };
}
fs.mkdirSync(skillDir, { recursive: true });
const merged = mergeOpenclawFrontmatter(skillBody);
fs.writeFileSync(skillFile, merged, { mode: 0o644 });
log.write(` installed: ${skillFile}\n`);
const soul = appendBootstrapToSoul(soulFile, snippet);
if (soul.changed) log.write(` wrote bootstrap block to ${soulFile}\n`);
else log.note(` ${soulFile} already contains caveman bootstrap`);
return { ok: true };
}
function uninstallOpenclaw({ workspace, dryRun = false, log = noopLog() } = {}) {
const ws = workspace || resolveWorkspace();
const skillDir = path.join(ws, 'skills', SKILL_NAME);
const soulFile = path.join(ws, SOUL_FILE);
let touched = false;
if (fs.existsSync(skillDir)) {
if (dryRun) {
log.note(` would remove ${skillDir}/`);
} else {
try { fs.rmSync(skillDir, { recursive: true, force: true }); } catch (_) {}
log.note(` removed ${skillDir}`);
}
touched = true;
}
if (fs.existsSync(soulFile)) {
if (dryRun) {
log.note(` would strip caveman block from ${soulFile}`);
touched = true;
} else {
const r = stripBootstrapFromSoul(soulFile);
if (r.changed) {
log.note(r.removed ? ` removed ${soulFile}` : ` stripped caveman block from ${soulFile}`);
touched = true;
}
}
}
return { ok: true, touched };
}
function noopLog() {
return {
write: (_) => {},
note: (_) => {},
warn: (_) => {},
};
}
module.exports = {
installOpenclaw,
uninstallOpenclaw,
resolveWorkspace,
// exported for tests
mergeOpenclawFrontmatter,
splitFrontmatter,
appendBootstrapToSoul,
stripBootstrapFromSoul,
loadBootstrapSnippet,
MARK_BEGIN,
MARK_END,
SKILL_NAME,
SKILL_VERSION,
};