mirror of
https://github.com/wu736139669/hapi.git
synced 2026-08-05 06:24:37 +00:00
* feat(codex): resolve slash controls before sending to Codex * feat(codex): discover commands and skills * fix(codex): handle slash commands before attachments * fix(codex): block remaining unsupported built-ins
187 lines
6.0 KiB
TypeScript
187 lines
6.0 KiB
TypeScript
import { access, readdir, readFile } from 'fs/promises';
|
|
import { basename, dirname, join, resolve } from 'path';
|
|
import { homedir } from 'os';
|
|
import { parse as parseYaml } from 'yaml';
|
|
|
|
export interface SkillSummary {
|
|
name: string;
|
|
description?: string;
|
|
}
|
|
|
|
export interface ListSkillsRequest {
|
|
}
|
|
|
|
export interface ListSkillsResponse {
|
|
success: boolean;
|
|
skills?: SkillSummary[];
|
|
error?: string;
|
|
}
|
|
|
|
function getHomeDirectory(): string {
|
|
return process.env.HOME ?? process.env.USERPROFILE ?? homedir();
|
|
}
|
|
|
|
function getUserSkillsRoots(): string[] {
|
|
const home = getHomeDirectory();
|
|
return [
|
|
join(home, '.agents', 'skills'),
|
|
join(home, '.claude', 'skills'),
|
|
join(home, '.codex', 'skills'),
|
|
];
|
|
}
|
|
|
|
function getAdminSkillsRoot(): string {
|
|
return join('/etc', 'codex', 'skills');
|
|
}
|
|
|
|
function getProjectSkillsRoots(directory: string): string[] {
|
|
return [
|
|
join(directory, '.agents', 'skills'),
|
|
join(directory, '.claude', 'skills'),
|
|
join(directory, '.codex', 'skills'),
|
|
];
|
|
}
|
|
|
|
async function pathExists(path: string): Promise<boolean> {
|
|
try {
|
|
await access(path);
|
|
return true;
|
|
} catch {
|
|
return false;
|
|
}
|
|
}
|
|
|
|
async function listProjectSkillsRoots(workingDirectory?: string): Promise<string[]> {
|
|
if (!workingDirectory) {
|
|
return [];
|
|
}
|
|
|
|
const resolvedWorkingDirectory = resolve(workingDirectory);
|
|
const directories = [resolvedWorkingDirectory];
|
|
let currentDirectory = resolvedWorkingDirectory;
|
|
|
|
while (true) {
|
|
if (await pathExists(join(currentDirectory, '.git'))) {
|
|
return directories.flatMap(getProjectSkillsRoots);
|
|
}
|
|
|
|
const parentDirectory = dirname(currentDirectory);
|
|
if (parentDirectory === currentDirectory) {
|
|
return getProjectSkillsRoots(resolvedWorkingDirectory);
|
|
}
|
|
|
|
currentDirectory = parentDirectory;
|
|
directories.push(currentDirectory);
|
|
}
|
|
}
|
|
|
|
function parseFrontmatter(fileContent: string): { frontmatter?: Record<string, unknown>; body: string } {
|
|
const match = fileContent.match(/^---\r?\n([\s\S]*?)\r?\n---\r?\n?([\s\S]*)$/);
|
|
if (!match) {
|
|
return { body: fileContent.trim() };
|
|
}
|
|
|
|
const yamlContent = match[1];
|
|
const body = match[2].trim();
|
|
try {
|
|
const parsed = parseYaml(yamlContent) as Record<string, unknown> | null;
|
|
return { frontmatter: parsed ?? undefined, body };
|
|
} catch {
|
|
return { body: fileContent.trim() };
|
|
}
|
|
}
|
|
|
|
function extractSkillSummary(skillDir: string, fileContent: string): SkillSummary | null {
|
|
const parsed = parseFrontmatter(fileContent);
|
|
const nameFromFrontmatter = typeof parsed.frontmatter?.name === 'string' ? parsed.frontmatter.name.trim() : '';
|
|
const name = nameFromFrontmatter || basename(skillDir);
|
|
if (!name) {
|
|
return null;
|
|
}
|
|
|
|
const description = typeof parsed.frontmatter?.description === 'string'
|
|
? parsed.frontmatter.description.trim()
|
|
: undefined;
|
|
|
|
return { name, description };
|
|
}
|
|
|
|
async function listTopLevelSkillDirs(skillsRoot: string, options: { includeCodexSystem?: boolean } = {}): Promise<string[]> {
|
|
try {
|
|
const entries = await readdir(skillsRoot, { withFileTypes: true });
|
|
const result: string[] = [];
|
|
|
|
for (const entry of entries) {
|
|
if (!entry.isDirectory()) {
|
|
continue;
|
|
}
|
|
|
|
if (entry.name.startsWith('.')) {
|
|
if (options.includeCodexSystem && entry.name === '.system') {
|
|
const systemEntries = await readdir(join(skillsRoot, entry.name), { withFileTypes: true }).catch(() => []);
|
|
for (const systemEntry of systemEntries) {
|
|
if (systemEntry.isDirectory() && !systemEntry.name.startsWith('.')) {
|
|
result.push(join(skillsRoot, entry.name, systemEntry.name));
|
|
}
|
|
}
|
|
}
|
|
continue;
|
|
}
|
|
|
|
result.push(join(skillsRoot, entry.name));
|
|
}
|
|
|
|
return result;
|
|
} catch {
|
|
return [];
|
|
}
|
|
}
|
|
|
|
async function readSkillsFromDirs(skillDirs: string[]): Promise<SkillSummary[]> {
|
|
const skills = await Promise.all(skillDirs.map(async (dir): Promise<SkillSummary | null> => {
|
|
const filePath = join(dir, 'SKILL.md');
|
|
try {
|
|
const fileContent = await readFile(filePath, 'utf-8');
|
|
return extractSkillSummary(dir, fileContent);
|
|
} catch {
|
|
return null;
|
|
}
|
|
}));
|
|
|
|
return skills.filter((skill): skill is SkillSummary => skill !== null);
|
|
}
|
|
|
|
function isCodexSkillsRoot(root: string): boolean {
|
|
return root.endsWith(join('.codex', 'skills'));
|
|
}
|
|
|
|
export async function listSkills(workingDirectory?: string): Promise<SkillSummary[]> {
|
|
const projectRoots = await listProjectSkillsRoots(workingDirectory);
|
|
const userRoots = getUserSkillsRoots();
|
|
const adminRoot = getAdminSkillsRoot();
|
|
const [projectSkillDirs, userSkillDirs, adminSkillDirs] = await Promise.all([
|
|
Promise.all(projectRoots.map(async (root) => await listTopLevelSkillDirs(root, { includeCodexSystem: isCodexSkillsRoot(root) }))).then((dirs) => dirs.flat()),
|
|
Promise.all(userRoots.map(async (root) => await listTopLevelSkillDirs(root, { includeCodexSystem: isCodexSkillsRoot(root) }))).then((dirs) => dirs.flat()),
|
|
listTopLevelSkillDirs(adminRoot, { includeCodexSystem: true }),
|
|
]);
|
|
|
|
const [projectSkills, userSkills, adminSkills] = await Promise.all([
|
|
readSkillsFromDirs(projectSkillDirs),
|
|
readSkillsFromDirs(userSkillDirs),
|
|
readSkillsFromDirs(adminSkillDirs),
|
|
]);
|
|
|
|
const dedupedSkills = new Map<string, SkillSummary>();
|
|
for (const skill of [
|
|
...projectSkills,
|
|
...userSkills,
|
|
...adminSkills,
|
|
]) {
|
|
if (!dedupedSkills.has(skill.name)) {
|
|
dedupedSkills.set(skill.name, skill);
|
|
}
|
|
}
|
|
|
|
return [...dedupedSkills.values()].sort((a, b) => a.name.localeCompare(b.name));
|
|
}
|