Initialize repository with harness code and assets
Initial import of all source code, config, and README assets: the packages workspace (cli, core, server, web, docs, landing, skills), build scripts, tooling config, and CI workflows. Includes the data-layout revision made on this branch: the local data root defaults to ~/.penguin/data (PENGUIN_HOME still overrides; the installer keeps its binaries in ~/.penguin), and every Agent lives under <project>/agents/<agent>/ — path helpers, the three agent-enumeration scans, the system prompt, built-in Skills, tests and docs all follow the new layout. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_018ihk8iQuo3kv2aPjAYEPuR
This commit is contained in:
@@ -0,0 +1,60 @@
|
||||
/**
|
||||
* Docs index: local Markdown pages imported at build time via import.meta.glob.
|
||||
* File naming: content/<slug>.<lang>.md — one file per page per language; a page
|
||||
* missing the active language falls back to the other one, so navigation is always
|
||||
* complete in both locales. Same architecture as the landing page blog.
|
||||
*/
|
||||
import { parseFrontmatter } from "./frontmatter";
|
||||
import type { Locale } from "../state/locale";
|
||||
|
||||
export interface DocPage {
|
||||
slug: string;
|
||||
lang: Locale;
|
||||
title: string;
|
||||
/** One-line summary rendered under the title (optional). */
|
||||
description: string;
|
||||
body: string;
|
||||
}
|
||||
|
||||
const files = import.meta.glob("../../content/*.md", {
|
||||
query: "?raw",
|
||||
import: "default",
|
||||
eager: true,
|
||||
}) as Record<string, string>;
|
||||
|
||||
function toDoc(path: string, raw: string): DocPage | null {
|
||||
const file = path.split("/").pop() ?? "";
|
||||
const match = /^(.+)\.(zh|en)\.md$/.exec(file);
|
||||
if (!match) return null;
|
||||
const { meta, body } = parseFrontmatter(raw);
|
||||
return {
|
||||
slug: match[1]!,
|
||||
lang: match[2] as Locale,
|
||||
title: meta.title ?? match[1]!,
|
||||
description: meta.description ?? "",
|
||||
body,
|
||||
};
|
||||
}
|
||||
|
||||
const ALL: DocPage[] = Object.entries(files)
|
||||
.map(([path, raw]) => toDoc(path, raw))
|
||||
.filter((doc): doc is DocPage => doc !== null);
|
||||
|
||||
/** The locale's version of a page (fallback to the other language). */
|
||||
export function getDoc(slug: string, locale: Locale): DocPage | undefined {
|
||||
const candidates = ALL.filter((doc) => doc.slug === slug);
|
||||
return candidates.find((doc) => doc.lang === locale) ?? candidates[0];
|
||||
}
|
||||
|
||||
/** Localized page title for sidebar / pagination labels. */
|
||||
export function docTitle(slug: string, locale: Locale): string {
|
||||
return getDoc(slug, locale)?.title ?? slug;
|
||||
}
|
||||
|
||||
/**
|
||||
* The page as plain Markdown (title heading + body) — what the per-page
|
||||
* "Copy Markdown" button puts on the clipboard.
|
||||
*/
|
||||
export function docMarkdown(doc: DocPage): string {
|
||||
return `# ${doc.title}\n\n${doc.body}\n`;
|
||||
}
|
||||
@@ -0,0 +1,31 @@
|
||||
/**
|
||||
* Minimal frontmatter parser for doc pages: a leading `---` block of `key: value`
|
||||
* lines (values may contain colons; quotes optional). Kept dependency-free and pure
|
||||
* so it is unit-testable without Vite. Same format as the landing page blog.
|
||||
*/
|
||||
|
||||
export interface Frontmatter {
|
||||
meta: Record<string, string>;
|
||||
body: string;
|
||||
}
|
||||
|
||||
export function parseFrontmatter(raw: string): Frontmatter {
|
||||
const normalized = raw.replace(/\r\n/g, "\n");
|
||||
const match = /^---\n([\s\S]*?)\n---\n?/.exec(normalized);
|
||||
if (!match) return { meta: {}, body: normalized.trim() };
|
||||
const meta: Record<string, string> = {};
|
||||
for (const line of match[1]!.split("\n")) {
|
||||
const idx = line.indexOf(":");
|
||||
if (idx === -1) continue;
|
||||
const key = line.slice(0, idx).trim();
|
||||
let value = line.slice(idx + 1).trim();
|
||||
if (
|
||||
(value.startsWith('"') && value.endsWith('"')) ||
|
||||
(value.startsWith("'") && value.endsWith("'"))
|
||||
) {
|
||||
value = value.slice(1, -1);
|
||||
}
|
||||
if (key) meta[key] = value;
|
||||
}
|
||||
return { meta, body: normalized.slice(match[0].length).trim() };
|
||||
}
|
||||
@@ -0,0 +1,12 @@
|
||||
/** External links and language-independent constants used across the docs site. */
|
||||
|
||||
export const REPO_URL = "https://github.com/Prism-Shadow/penguin-harness";
|
||||
export const LICENSE_URL = `${REPO_URL}/blob/main/LICENSE`;
|
||||
|
||||
/**
|
||||
* The main site sits one level above the docs (both ship in one GitHub Pages
|
||||
* artifact: landing at "/<repo>/", docs at "/<repo>/docs/"). In local dev the docs
|
||||
* base is "/" so this resolves to the docs root itself — the landing page runs on
|
||||
* its own dev server there.
|
||||
*/
|
||||
export const SITE_URL = import.meta.env.BASE_URL.replace(/docs\/$/, "");
|
||||
@@ -0,0 +1,43 @@
|
||||
/**
|
||||
* Docs navigation: the single source of truth for sidebar sections, page order and
|
||||
* prev/next pagination. Section labels live in the strings dictionaries (S.sections);
|
||||
* page titles come from each Markdown file's frontmatter. Kept pure (no import.meta)
|
||||
* so the content-integrity test can import it under plain node.
|
||||
*/
|
||||
|
||||
export interface DocsSectionDef {
|
||||
/** Section id — also the key into S.sections for the localized label. */
|
||||
id: "start" | "design" | "guides" | "reference";
|
||||
/** Page slugs in display order; content files are content/<slug>.<zh|en>.md. */
|
||||
slugs: string[];
|
||||
}
|
||||
|
||||
export const DOCS_NAV: DocsSectionDef[] = [
|
||||
{ id: "start", slugs: ["introduction", "installation", "quickstart"] },
|
||||
{
|
||||
id: "design",
|
||||
slugs: [
|
||||
"architecture",
|
||||
"omni-message",
|
||||
"agent-loop",
|
||||
"message-flow",
|
||||
"interfaces",
|
||||
"tools",
|
||||
"skills",
|
||||
"models",
|
||||
"sessions-and-traces",
|
||||
],
|
||||
},
|
||||
{ id: "guides", slugs: ["web-app", "self-improvement"] },
|
||||
{ id: "reference", slugs: ["cli", "server-api", "configuration"] },
|
||||
];
|
||||
|
||||
/** All slugs in display order (pagination order). */
|
||||
export const DOC_SLUGS: string[] = DOCS_NAV.flatMap((section) => section.slugs);
|
||||
|
||||
/** The docs landing page ("/" renders this slug). */
|
||||
export const HOME_SLUG = DOC_SLUGS[0]!;
|
||||
|
||||
export function sectionOf(slug: string): DocsSectionDef | undefined {
|
||||
return DOCS_NAV.find((section) => section.slugs.includes(slug));
|
||||
}
|
||||
@@ -0,0 +1,52 @@
|
||||
/** English dictionary for the docs UI (same shape as `zh` in strings.ts). */
|
||||
import type { Strings } from "./strings";
|
||||
|
||||
export const en: Strings = {
|
||||
siteName: "PenguinHarness",
|
||||
docsBadge: "Docs",
|
||||
|
||||
nav: {
|
||||
home: "Website",
|
||||
github: "GitHub",
|
||||
openMenu: "Open navigation",
|
||||
closeMenu: "Close navigation",
|
||||
},
|
||||
|
||||
theme: {
|
||||
label: "Theme",
|
||||
light: "Light",
|
||||
dark: "Dark",
|
||||
system: "System",
|
||||
},
|
||||
|
||||
lang: {
|
||||
label: "Language",
|
||||
zh: "中文",
|
||||
en: "English",
|
||||
system: "System",
|
||||
},
|
||||
|
||||
sections: {
|
||||
start: "Get Started",
|
||||
design: "Core Design",
|
||||
guides: "Guides",
|
||||
reference: "Reference",
|
||||
} as Record<string, string>,
|
||||
|
||||
doc: {
|
||||
toc: "On this page",
|
||||
copyMarkdown: "Copy Markdown",
|
||||
copied: "Copied",
|
||||
prev: "Previous",
|
||||
next: "Next",
|
||||
notFound: "Page not found",
|
||||
backHome: "Back to docs home",
|
||||
},
|
||||
|
||||
footer: {
|
||||
repo: "GitHub repository",
|
||||
license: "Apache-2.0 License",
|
||||
site: "Website",
|
||||
copyright: "© 2026 Prism Shadow · Open source under Apache-2.0",
|
||||
},
|
||||
};
|
||||
@@ -0,0 +1,70 @@
|
||||
/**
|
||||
* Docs UI copy (bilingual): this file holds the Chinese dictionary `zh` and the runtime
|
||||
* active dictionary `S`; the English dictionary lives in strings-en.ts (constrained to
|
||||
* the same shape by the `Strings` type). Locale switching is handled by state/locale.tsx,
|
||||
* which calls `setActiveStrings` and remounts the tree keyed by locale — keep `S.x`
|
||||
* reads inside components. Doc page bodies are Markdown files under content/, not here.
|
||||
*/
|
||||
export const zh = {
|
||||
siteName: "PenguinHarness",
|
||||
docsBadge: "Docs",
|
||||
|
||||
nav: {
|
||||
home: "产品主页",
|
||||
github: "GitHub",
|
||||
openMenu: "打开目录",
|
||||
closeMenu: "关闭目录",
|
||||
},
|
||||
|
||||
theme: {
|
||||
label: "主题",
|
||||
light: "浅色",
|
||||
dark: "深色",
|
||||
system: "跟随系统",
|
||||
},
|
||||
|
||||
lang: {
|
||||
label: "语言",
|
||||
zh: "中文",
|
||||
en: "English",
|
||||
system: "跟随系统",
|
||||
},
|
||||
|
||||
sections: {
|
||||
start: "开始",
|
||||
design: "核心设计",
|
||||
guides: "使用指南",
|
||||
reference: "参考",
|
||||
} as Record<string, string>,
|
||||
|
||||
doc: {
|
||||
toc: "本页目录",
|
||||
copyMarkdown: "复制 Markdown",
|
||||
copied: "已复制",
|
||||
prev: "上一页",
|
||||
next: "下一页",
|
||||
notFound: "页面不存在",
|
||||
backHome: "返回文档首页",
|
||||
},
|
||||
|
||||
footer: {
|
||||
repo: "GitHub 仓库",
|
||||
license: "Apache-2.0 License",
|
||||
site: "产品主页",
|
||||
copyright: "© 2026 Prism Shadow · 基于 Apache-2.0 协议开源",
|
||||
},
|
||||
};
|
||||
|
||||
/** Dictionary shape (constrains the English dictionary so keys line up). */
|
||||
export type Strings = typeof zh;
|
||||
|
||||
/**
|
||||
* Runtime active dictionary (live binding): the locale Provider calls setActiveStrings
|
||||
* to switch before render, and remounts the whole tree keyed by locale so every `S.x`
|
||||
* read reflects the current language.
|
||||
*/
|
||||
export let S: Strings = zh;
|
||||
|
||||
export function setActiveStrings(next: Strings): void {
|
||||
S = next;
|
||||
}
|
||||
@@ -0,0 +1,42 @@
|
||||
/**
|
||||
* Table-of-contents helpers: extract ##/### headings from a Markdown body (skipping
|
||||
* fenced code blocks) and slugify them the same way the rendered headings do, so TOC
|
||||
* anchors and heading ids always match. Pure and unit-testable; same behavior as the
|
||||
* landing page blog.
|
||||
*/
|
||||
|
||||
export interface TocEntry {
|
||||
id: string;
|
||||
text: string;
|
||||
depth: 2 | 3;
|
||||
}
|
||||
|
||||
/** Heading text -> anchor id (keeps CJK, lowercases latin, hyphenates spaces). */
|
||||
export function slugifyHeading(text: string): string {
|
||||
return text
|
||||
.trim()
|
||||
.toLowerCase()
|
||||
.replace(/[^\p{L}\p{N}\s-]/gu, "")
|
||||
.replace(/\s+/g, "-");
|
||||
}
|
||||
|
||||
export function extractToc(body: string): TocEntry[] {
|
||||
const entries: TocEntry[] = [];
|
||||
let inFence = false;
|
||||
for (const line of body.split("\n")) {
|
||||
if (/^\s*(```|~~~)/.test(line)) {
|
||||
inFence = !inFence;
|
||||
continue;
|
||||
}
|
||||
if (inFence) continue;
|
||||
const match = /^(#{2,3})\s+(.+?)\s*$/.exec(line);
|
||||
if (!match) continue;
|
||||
const text = match[2]!;
|
||||
entries.push({
|
||||
id: slugifyHeading(text),
|
||||
text,
|
||||
depth: match[1]!.length === 2 ? 2 : 3,
|
||||
});
|
||||
}
|
||||
return entries;
|
||||
}
|
||||
Reference in New Issue
Block a user