Adds immutable "start-here" system guide as the master orientation doc for agents. Enriches all tool descriptions with key rules at point of use so agents can execute simple tasks without reading guides. Co-Authored-By: Claude Sonnet 4.5 <noreply@anthropic.com>
205 lines
5.3 KiB
JavaScript
205 lines
5.3 KiB
JavaScript
'use strict';
|
|
|
|
const fs = require('node:fs');
|
|
const path = require('node:path');
|
|
const os = require('node:os');
|
|
|
|
const MAX_USER_GUIDES = 10;
|
|
|
|
// Where guides live on disk (shared with the app)
|
|
const GUIDES_DIR = path.join(
|
|
os.homedir(),
|
|
'Library', 'Application Support', 'RA-H', 'guides'
|
|
);
|
|
|
|
// System guides bundled with this package
|
|
const BUNDLED_SYSTEM_DIR = path.join(__dirname, '..', 'guides', 'system');
|
|
const BUNDLED_USER_DIR = path.join(__dirname, '..', 'guides');
|
|
|
|
// System guide names (immutable, always re-seeded)
|
|
const SYSTEM_GUIDE_NAMES = new Set([
|
|
'start-here',
|
|
'schema',
|
|
'creating-nodes',
|
|
'edges',
|
|
'dimensions',
|
|
'extract',
|
|
]);
|
|
|
|
/**
|
|
* Parse YAML frontmatter from markdown without external deps.
|
|
* Returns { data: {}, content: string }
|
|
*/
|
|
function parseFrontmatter(raw) {
|
|
const match = raw.match(/^---\r?\n([\s\S]*?)\r?\n---\r?\n?([\s\S]*)$/);
|
|
if (!match) return { data: {}, content: raw.trim() };
|
|
|
|
const yamlBlock = match[1];
|
|
const content = match[2];
|
|
const data = {};
|
|
|
|
for (const line of yamlBlock.split('\n')) {
|
|
const colonIdx = line.indexOf(':');
|
|
if (colonIdx === -1) continue;
|
|
const key = line.slice(0, colonIdx).trim();
|
|
let value = line.slice(colonIdx + 1).trim();
|
|
// Handle booleans
|
|
if (value === 'true') value = true;
|
|
else if (value === 'false') value = false;
|
|
data[key] = value;
|
|
}
|
|
|
|
return { data, content: content.trim() };
|
|
}
|
|
|
|
function isSystemGuide(filename) {
|
|
const name = filename.replace('.md', '');
|
|
return SYSTEM_GUIDE_NAMES.has(name);
|
|
}
|
|
|
|
function ensureGuidesDir() {
|
|
if (!fs.existsSync(GUIDES_DIR)) {
|
|
fs.mkdirSync(GUIDES_DIR, { recursive: true });
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Seed guides on first run.
|
|
* System guides always overwrite. User guides only seed if missing.
|
|
*/
|
|
function seedGuides() {
|
|
ensureGuidesDir();
|
|
|
|
// Always re-seed system guides (immutable)
|
|
if (fs.existsSync(BUNDLED_SYSTEM_DIR)) {
|
|
const files = fs.readdirSync(BUNDLED_SYSTEM_DIR).filter(f => f.endsWith('.md'));
|
|
for (const file of files) {
|
|
fs.copyFileSync(path.join(BUNDLED_SYSTEM_DIR, file), path.join(GUIDES_DIR, file));
|
|
}
|
|
}
|
|
|
|
// Seed default user guides only if they don't exist
|
|
if (fs.existsSync(BUNDLED_USER_DIR)) {
|
|
const files = fs.readdirSync(BUNDLED_USER_DIR).filter(f => f.endsWith('.md'));
|
|
for (const file of files) {
|
|
const dest = path.join(GUIDES_DIR, file);
|
|
if (!fs.existsSync(dest)) {
|
|
fs.copyFileSync(path.join(BUNDLED_USER_DIR, file), dest);
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
let initialized = false;
|
|
|
|
function init() {
|
|
if (initialized) return;
|
|
seedGuides();
|
|
initialized = true;
|
|
}
|
|
|
|
/**
|
|
* List all guides with name, description, immutable flag.
|
|
*/
|
|
function listGuides() {
|
|
init();
|
|
if (!fs.existsSync(GUIDES_DIR)) return [];
|
|
|
|
const files = fs.readdirSync(GUIDES_DIR).filter(f => f.endsWith('.md'));
|
|
|
|
const guides = files.map(file => {
|
|
const raw = fs.readFileSync(path.join(GUIDES_DIR, file), 'utf-8');
|
|
const { data } = parseFrontmatter(raw);
|
|
const immutable = isSystemGuide(file) || data.immutable === true;
|
|
return {
|
|
name: data.name || file.replace('.md', ''),
|
|
description: data.description || '',
|
|
immutable,
|
|
};
|
|
});
|
|
|
|
// System guides first, then user guides alphabetically
|
|
return guides.sort((a, b) => {
|
|
if (a.immutable && !b.immutable) return -1;
|
|
if (!a.immutable && b.immutable) return 1;
|
|
return a.name.localeCompare(b.name);
|
|
});
|
|
}
|
|
|
|
/**
|
|
* Read a guide by name. Returns full content.
|
|
*/
|
|
function readGuide(name) {
|
|
init();
|
|
const candidates = [`${name}.md`, `${name.toLowerCase()}.md`];
|
|
|
|
for (const filename of candidates) {
|
|
const filepath = path.join(GUIDES_DIR, filename);
|
|
if (fs.existsSync(filepath)) {
|
|
const raw = fs.readFileSync(filepath, 'utf-8');
|
|
const { data, content } = parseFrontmatter(raw);
|
|
const immutable = isSystemGuide(filename) || data.immutable === true;
|
|
return {
|
|
name: data.name || name,
|
|
description: data.description || '',
|
|
immutable,
|
|
content,
|
|
};
|
|
}
|
|
}
|
|
|
|
return null;
|
|
}
|
|
|
|
/**
|
|
* Write or update a guide. Rejects writes to system guides.
|
|
*/
|
|
function writeGuide(name, content) {
|
|
init();
|
|
const filename = `${name.toLowerCase()}.md`;
|
|
|
|
if (isSystemGuide(filename)) {
|
|
return { success: false, error: `Guide "${name}" is a system guide and cannot be modified.` };
|
|
}
|
|
|
|
const filepath = path.join(GUIDES_DIR, filename);
|
|
if (!fs.existsSync(filepath)) {
|
|
const userCount = listGuides().filter(g => !g.immutable).length;
|
|
if (userCount >= MAX_USER_GUIDES) {
|
|
return { success: false, error: `Maximum of ${MAX_USER_GUIDES} custom guides reached. Delete a guide first.` };
|
|
}
|
|
}
|
|
|
|
ensureGuidesDir();
|
|
fs.writeFileSync(filepath, content, 'utf-8');
|
|
return { success: true };
|
|
}
|
|
|
|
/**
|
|
* Delete a guide. Rejects deletes of system guides.
|
|
*/
|
|
function deleteGuide(name) {
|
|
init();
|
|
const candidates = [`${name}.md`, `${name.toLowerCase()}.md`];
|
|
|
|
for (const filename of candidates) {
|
|
if (isSystemGuide(filename)) {
|
|
return { success: false, error: `Guide "${name}" is a system guide and cannot be deleted.` };
|
|
}
|
|
const filepath = path.join(GUIDES_DIR, filename);
|
|
if (fs.existsSync(filepath)) {
|
|
fs.unlinkSync(filepath);
|
|
return { success: true };
|
|
}
|
|
}
|
|
|
|
return { success: false, error: `Guide "${name}" not found.` };
|
|
}
|
|
|
|
module.exports = {
|
|
listGuides,
|
|
readGuide,
|
|
writeGuide,
|
|
deleteGuide,
|
|
};
|