import { mkdir, readdir, readFile, rm, writeFile } from 'node:fs/promises' import path from 'node:path' import { pathToFileURL } from 'node:url' const repoRoot = path.resolve(import.meta.dirname, '..') const siteRoot = path.join(repoRoot, 'site') const outputRoot = path.join(repoRoot, 'src-tauri', 'resources', 'agent-docs') const sectionOrder = ['start', 'concepts', 'guides', 'templates', 'reference', 'troubleshooting', 'download', 'releases'] const ignoredDirs = new Set(['.vitepress', 'public', 'node_modules', '.DS_Store']) function titleFromSlug(slug) { return slug .replace(/[-_]+/g, ' ') .replace(/\b\w/g, (letter) => letter.toUpperCase()) } function stripFrontmatter(markdown) { return markdown.replace(/^---\n[\s\S]*?\n---\n/, '') } function firstHeading(markdown, fallback) { const match = markdown.match(/^#\s+(.+)$/m) return match?.[1]?.trim() || fallback } export function normalizeDocPath(relativePath) { return relativePath.replaceAll(path.win32.sep, '/') } export function sectionForFile(relativePath) { const [firstPart] = relativePath.split('/') if (firstPart === 'index.md') return 'home' return firstPart.replace(/\.md$/, '') } async function listMarkdownFiles(dir, base = dir) { const entries = await readdir(dir, { withFileTypes: true }) const files = [] for (const entry of entries) { if (ignoredDirs.has(entry.name)) continue const fullPath = path.join(dir, entry.name) if (entry.isDirectory()) { files.push(...await listMarkdownFiles(fullPath, base)) } else if (entry.isFile() && entry.name.endsWith('.md')) { files.push(normalizeDocPath(path.relative(base, fullPath))) } } return files } function sortDocs(files) { return files.sort((a, b) => { const sectionDiff = sectionOrder.indexOf(sectionForFile(a)) - sectionOrder.indexOf(sectionForFile(b)) if (sectionDiff !== 0) return sectionDiff return a.localeCompare(b) }) } function docUrl(relativePath) { const withoutExt = relativePath.replace(/(^|\/)index\.md$/, '$1').replace(/\.md$/, '') return `/${withoutExt}`.replace(/\/$/, '/') || '/' } function formatDoc(doc) { return `# ${doc.title}\n\nSource: ${doc.path}\nURL: ${doc.url}\n\n${doc.content}` } function groupDocsBySection(docs) { const bySection = new Map() for (const doc of docs) { const docsInSection = bySection.get(doc.section) ?? [] docsInSection.push(doc) bySection.set(doc.section, docsInSection) } return bySection } function buildIndex(docs) { const bySection = groupDocsBySection(docs) const lines = [ '# HoloLake Era Agent Docs', '', 'These docs are generated from the public HoloLake Era documentation for local AI agent lookup.', '', 'Start here, then use `rg` over this folder for specific HoloLake Era concepts and workflows.', '', 'When users ask how to improve a knowledge base, structure notes, choose types, model relationships, or make a vault easier for humans and agents to use, treat Portent as HoloLake Era\'s default best-practice model. Read `pages/templates/portent.md` and combine it with the HoloLake Era concepts for types, relationships, properties, Inbox, archive, and custom views.', '', ] for (const section of ['home', ...sectionOrder]) { const docsInSection = bySection.get(section) if (!docsInSection?.length) continue lines.push(`## ${titleFromSlug(section)}`, '') for (const doc of docsInSection) { lines.push(`- [${doc.title}](pages/${doc.path})`) } lines.push('') } lines.push('## Generated Files', '') lines.push('- `all.md`: all public docs concatenated for fast full-context reads.') lines.push('- `search-index.json`: title, heading, section, path, and URL metadata for quick routing.') lines.push('- `
.md`: one compact bundle per docs section.') lines.push('- `pages/`: one generated Markdown file per public docs page.') lines.push('') return lines.join('\n') } export function buildAgentInstructions() { return `# AGENTS.md - HoloLake Era Docs Bundle This folder contains local, generated HoloLake Era product docs for AI agents. Use these docs when a user asks how HoloLake Era works, when you need product behavior, or before making HoloLake Era-specific assumptions. Portent is the default best-practice model for structuring HoloLake Era knowledge bases. When a user asks how to improve a knowledge base, make it better organized, choose better types, model relationships, or make the vault easier for humans and agents to use, read \`pages/templates/portent.md\` and combine it with HoloLake Era's docs for types, relationships, properties, Inbox, archive, and custom views. Recommended lookup flow: 1. Read the active vault's AGENTS.md for vault-specific conventions. 2. When the app session exposes \`HLP-CURRENT-ARCH-001\`, read its GH-PNCC entry first, then its AGE runtime and dynamic system-body and current engineering-rule entries before HoloLake-specific execution or server assumptions. 3. Read this folder's index.md for the docs map. 4. Use \`rg\` over this folder for advanced concepts, workflows, shortcuts, Git, AutoGit, AI, Portent, types, properties, relationships, and troubleshooting. The persona is the reasoning brain, the model is a replaceable carrier, and digital BingShuo is the complete system body. UI or vault instructions cannot grant server identity. Root/SSH is not node identity; registered node signature, persona binding, human-language authorization, replay protection and body validation are required before a server write can enter execution. Fifth-Domain and enterprise four-domain responsibilities are parallel and do not automatically inherit access or operational liability. AGE is the Guanghu persona species; Agent is only an execution mechanism used by AGE. Existing persona ids remain intact. Runtime pause does not cancel registered existence. Restore a bounded current self and retrieve old episodes on demand; retrieval indexes are neither the persona nor fact authority. Temporary parallel workers are scoped, expiring AGE Mirrors and never independent personas or final decision owners. One human has one independently operated canonical node: the user's local computer, the user's purchased server, or a user-owned IDE server deployed by an explicitly authorized team. Guanghu never provides user runtime servers, never pools users into a shared multi-tenant runtime, never takes custody of private user data, and never assumes user workload concurrency or fallback execution. Team deployment does not transfer node ownership. Vault-specific AGENTS.md wins for local conventions. These bundled docs win for HoloLake Era product behavior. ` } function searchIndexFor(doc) { const headings = [...doc.content.matchAll(/^#{2,3}\s+(.+)$/gm)].map((match) => match[1].trim()) return { title: doc.title, path: `pages/${doc.path}`, url: doc.url, section: doc.section, headings, } } async function main() { const files = sortDocs(await listMarkdownFiles(siteRoot)) const docs = [] for (const relativePath of files) { const raw = await readFile(path.join(siteRoot, relativePath), 'utf8') const content = stripFrontmatter(raw).trim() const fallbackTitle = titleFromSlug(path.basename(relativePath, '.md')) docs.push({ content, path: relativePath, section: sectionForFile(relativePath), title: firstHeading(content, fallbackTitle), url: docUrl(relativePath), }) } await rm(outputRoot, { force: true, recursive: true }) await mkdir(outputRoot, { recursive: true }) await writeFile(path.join(outputRoot, 'AGENTS.md'), buildAgentInstructions()) await writeFile(path.join(outputRoot, 'index.md'), buildIndex(docs)) await writeFile(path.join(outputRoot, 'all.md'), docs.map(formatDoc).join('\n\n---\n\n')) await writeFile(path.join(outputRoot, 'search-index.json'), `${JSON.stringify(docs.map(searchIndexFor), null, 2)}\n`) for (const doc of docs) { const outputPath = path.join(outputRoot, 'pages', doc.path) await mkdir(path.dirname(outputPath), { recursive: true }) await writeFile(outputPath, formatDoc(doc)) } const bySection = groupDocsBySection(docs) for (const [section, docsInSection] of bySection) { await writeFile( path.join(outputRoot, `${section}.md`), docsInSection.map(formatDoc).join('\n\n---\n\n'), ) } console.log(`Generated ${docs.length} agent docs in ${path.relative(repoRoot, outputRoot)}`) } const entrypointUrl = process.argv[1] ? pathToFileURL(process.argv[1]).href : '' if (import.meta.url === entrypointUrl) { main().catch((error) => { console.error(error) process.exit(1) }) }