mirror of
https://github.com/openclaw/openclaw.git
synced 2026-08-12 21:53:00 -06:00
99ffd714ce
* refactor: extract markdown core package * refactor: remove old markdown sources * fix: use source paths for markdown core imports * fix: clean markdown package dependency ownership * fix: refresh root shrinkwrap for markdown dependency move
224 lines
6.1 KiB
TypeScript
224 lines
6.1 KiB
TypeScript
import { parseFrontmatterBlock } from "../../../packages/markdown-core/src/frontmatter.js";
|
|
import { validateRegistryNpmSpec } from "../../infra/npm-registry-spec.js";
|
|
import {
|
|
applyOpenClawManifestInstallCommonFields,
|
|
getFrontmatterString,
|
|
normalizeStringList,
|
|
parseOpenClawManifestInstallBase,
|
|
parseFrontmatterBool,
|
|
resolveOpenClawManifestBlock,
|
|
resolveOpenClawManifestInstall,
|
|
resolveOpenClawManifestOs,
|
|
resolveOpenClawManifestRequires,
|
|
} from "../../shared/frontmatter.js";
|
|
import { readStringValue } from "../../shared/string-coerce.js";
|
|
import type {
|
|
OpenClawSkillMetadata,
|
|
ParsedSkillFrontmatter,
|
|
SkillEntry,
|
|
SkillInstallSpec,
|
|
SkillInvocationPolicy,
|
|
} from "../types.js";
|
|
import type { Skill } from "./skill-contract.js";
|
|
|
|
export function parseFrontmatter(content: string): ParsedSkillFrontmatter {
|
|
return parseFrontmatterBlock(content);
|
|
}
|
|
|
|
const BREW_FORMULA_PATTERN = /^[A-Za-z0-9][A-Za-z0-9@+._/-]*$/;
|
|
const GO_MODULE_PATTERN = /^[A-Za-z0-9][A-Za-z0-9._~+\-/]*(?:@[A-Za-z0-9][A-Za-z0-9._~+\-/]*)?$/;
|
|
const UV_PACKAGE_PATTERN = /^[A-Za-z0-9][A-Za-z0-9._\-[\]=<>!~+,]*$/;
|
|
|
|
function normalizeSafeBrewFormula(raw: unknown): string | undefined {
|
|
if (typeof raw !== "string") {
|
|
return undefined;
|
|
}
|
|
const formula = raw.trim();
|
|
if (!formula || formula.startsWith("-") || formula.includes("\\") || formula.includes("..")) {
|
|
return undefined;
|
|
}
|
|
if (!BREW_FORMULA_PATTERN.test(formula)) {
|
|
return undefined;
|
|
}
|
|
return formula;
|
|
}
|
|
|
|
function normalizeSafeNpmSpec(raw: unknown): string | undefined {
|
|
if (typeof raw !== "string") {
|
|
return undefined;
|
|
}
|
|
const spec = raw.trim();
|
|
if (!spec || spec.startsWith("-")) {
|
|
return undefined;
|
|
}
|
|
if (validateRegistryNpmSpec(spec) !== null) {
|
|
return undefined;
|
|
}
|
|
return spec;
|
|
}
|
|
|
|
function normalizeSafeGoModule(raw: unknown): string | undefined {
|
|
if (typeof raw !== "string") {
|
|
return undefined;
|
|
}
|
|
const moduleSpec = raw.trim();
|
|
if (
|
|
!moduleSpec ||
|
|
moduleSpec.startsWith("-") ||
|
|
moduleSpec.includes("\\") ||
|
|
moduleSpec.includes("://")
|
|
) {
|
|
return undefined;
|
|
}
|
|
if (!GO_MODULE_PATTERN.test(moduleSpec)) {
|
|
return undefined;
|
|
}
|
|
return moduleSpec;
|
|
}
|
|
|
|
function normalizeSafeUvPackage(raw: unknown): string | undefined {
|
|
if (typeof raw !== "string") {
|
|
return undefined;
|
|
}
|
|
const pkg = raw.trim();
|
|
if (!pkg || pkg.startsWith("-") || pkg.includes("\\") || pkg.includes("://")) {
|
|
return undefined;
|
|
}
|
|
if (!UV_PACKAGE_PATTERN.test(pkg)) {
|
|
return undefined;
|
|
}
|
|
return pkg;
|
|
}
|
|
|
|
function normalizeSafeDownloadUrl(raw: unknown): string | undefined {
|
|
if (typeof raw !== "string") {
|
|
return undefined;
|
|
}
|
|
const value = raw.trim();
|
|
if (!value || /\s/.test(value)) {
|
|
return undefined;
|
|
}
|
|
try {
|
|
const parsed = new URL(value);
|
|
if (parsed.protocol !== "http:" && parsed.protocol !== "https:") {
|
|
return undefined;
|
|
}
|
|
return parsed.toString();
|
|
} catch {
|
|
return undefined;
|
|
}
|
|
}
|
|
|
|
function parseInstallSpec(input: unknown): SkillInstallSpec | undefined {
|
|
const parsed = parseOpenClawManifestInstallBase(input, ["brew", "node", "go", "uv", "download"]);
|
|
if (!parsed) {
|
|
return undefined;
|
|
}
|
|
const { raw } = parsed;
|
|
const spec = applyOpenClawManifestInstallCommonFields<SkillInstallSpec>(
|
|
{
|
|
kind: parsed.kind as SkillInstallSpec["kind"],
|
|
},
|
|
parsed,
|
|
);
|
|
const osList = normalizeStringList(raw.os);
|
|
if (osList.length > 0) {
|
|
spec.os = osList;
|
|
}
|
|
const formula = normalizeSafeBrewFormula(raw.formula);
|
|
if (formula) {
|
|
spec.formula = formula;
|
|
}
|
|
const cask = normalizeSafeBrewFormula(raw.cask);
|
|
if (!spec.formula && cask) {
|
|
spec.formula = cask;
|
|
}
|
|
if (spec.kind === "node") {
|
|
const pkg = normalizeSafeNpmSpec(raw.package);
|
|
if (pkg) {
|
|
spec.package = pkg;
|
|
}
|
|
} else if (spec.kind === "uv") {
|
|
const pkg = normalizeSafeUvPackage(raw.package);
|
|
if (pkg) {
|
|
spec.package = pkg;
|
|
}
|
|
}
|
|
const moduleSpec = normalizeSafeGoModule(raw.module);
|
|
if (moduleSpec) {
|
|
spec.module = moduleSpec;
|
|
}
|
|
const downloadUrl = normalizeSafeDownloadUrl(raw.url);
|
|
if (downloadUrl) {
|
|
spec.url = downloadUrl;
|
|
}
|
|
if (typeof raw.archive === "string") {
|
|
spec.archive = raw.archive;
|
|
}
|
|
if (typeof raw.extract === "boolean") {
|
|
spec.extract = raw.extract;
|
|
}
|
|
if (typeof raw.stripComponents === "number") {
|
|
spec.stripComponents = raw.stripComponents;
|
|
}
|
|
if (typeof raw.targetDir === "string") {
|
|
spec.targetDir = raw.targetDir;
|
|
}
|
|
|
|
if (spec.kind === "brew" && !spec.formula) {
|
|
return undefined;
|
|
}
|
|
if (spec.kind === "node" && !spec.package) {
|
|
return undefined;
|
|
}
|
|
if (spec.kind === "go" && !spec.module) {
|
|
return undefined;
|
|
}
|
|
if (spec.kind === "uv" && !spec.package) {
|
|
return undefined;
|
|
}
|
|
if (spec.kind === "download" && !spec.url) {
|
|
return undefined;
|
|
}
|
|
|
|
return spec;
|
|
}
|
|
|
|
export function resolveOpenClawMetadata(
|
|
frontmatter: ParsedSkillFrontmatter,
|
|
): OpenClawSkillMetadata | undefined {
|
|
const metadataObj = resolveOpenClawManifestBlock({ frontmatter });
|
|
if (!metadataObj) {
|
|
return undefined;
|
|
}
|
|
const requires = resolveOpenClawManifestRequires(metadataObj);
|
|
const install = resolveOpenClawManifestInstall(metadataObj, parseInstallSpec);
|
|
const osRaw = resolveOpenClawManifestOs(metadataObj);
|
|
return {
|
|
always: typeof metadataObj.always === "boolean" ? metadataObj.always : undefined,
|
|
emoji: readStringValue(metadataObj.emoji),
|
|
homepage: readStringValue(metadataObj.homepage),
|
|
skillKey: readStringValue(metadataObj.skillKey),
|
|
primaryEnv: readStringValue(metadataObj.primaryEnv),
|
|
os: osRaw.length > 0 ? osRaw : undefined,
|
|
requires: requires,
|
|
install: install.length > 0 ? install : undefined,
|
|
};
|
|
}
|
|
|
|
export function resolveSkillInvocationPolicy(
|
|
frontmatter: ParsedSkillFrontmatter,
|
|
): SkillInvocationPolicy {
|
|
return {
|
|
userInvocable: parseFrontmatterBool(getFrontmatterString(frontmatter, "user-invocable"), true),
|
|
disableModelInvocation: parseFrontmatterBool(
|
|
getFrontmatterString(frontmatter, "disable-model-invocation"),
|
|
false,
|
|
),
|
|
};
|
|
}
|
|
|
|
export function resolveSkillKey(skill: Skill, entry?: SkillEntry): string {
|
|
return entry?.metadata?.skillKey ?? skill.name;
|
|
}
|