- Implement tests for random ID generation, ensuring preference for crypto.randomUUID. - Create tests for runtime context capabilities, validating the injection of enabled skill capabilities. - Add tests for skill capability parsing, including classification and command example extraction. - Introduce tests for the skill planner, verifying tool call planning based on user requests and attachment requirements. - Establish tests for UV setup, ensuring proper handling of Python installation scenarios and environment checks.
229 lines
6.0 KiB
TypeScript
229 lines
6.0 KiB
TypeScript
import { existsSync, readdirSync, readFileSync } from 'node:fs';
|
|
import { join } from 'node:path';
|
|
import {
|
|
getAllSkillConfigs,
|
|
type SkillConfigRecord,
|
|
} from '@electron/utils/skill-config';
|
|
import { getOpenClawConfigDir } from '@electron/utils/paths';
|
|
import {
|
|
parseSkillCapability,
|
|
type SkillCapability,
|
|
} from './skill-capability-parser';
|
|
|
|
type SkillConfigEntry = {
|
|
enabled?: boolean;
|
|
apiKey?: string;
|
|
env?: Record<string, string>;
|
|
};
|
|
|
|
type RegistrySnapshot = {
|
|
capabilities: SkillCapability[];
|
|
loadedAt: string;
|
|
source: 'disk-sync' | 'skill-config';
|
|
};
|
|
|
|
const OPENCLAW_CONFIG_FILE = 'openclaw.json';
|
|
const OPENCLAW_SKILLS_DIR = 'skills';
|
|
const SKILL_MANIFEST_FILE = 'SKILL.md';
|
|
|
|
let registrySnapshot: RegistrySnapshot | null = null;
|
|
let refreshPromise: Promise<SkillCapability[]> | null = null;
|
|
|
|
function uniq(values: string[]): string[] {
|
|
return Array.from(new Set(values));
|
|
}
|
|
|
|
function cloneCapability(capability: SkillCapability): SkillCapability {
|
|
return {
|
|
...capability,
|
|
allowedTools: [...capability.allowedTools],
|
|
operationHints: [...capability.operationHints],
|
|
triggerHints: [...capability.triggerHints],
|
|
inputExtensions: [...capability.inputExtensions],
|
|
requiredEnvVars: [...capability.requiredEnvVars],
|
|
commandExamples: capability.commandExamples ? [...capability.commandExamples] : undefined,
|
|
renderHints: capability.renderHints
|
|
? {
|
|
...capability.renderHints,
|
|
metadata: capability.renderHints.metadata
|
|
? { ...capability.renderHints.metadata }
|
|
: undefined,
|
|
}
|
|
: undefined,
|
|
};
|
|
}
|
|
|
|
function sortCapabilities(capabilities: SkillCapability[]): SkillCapability[] {
|
|
return [...capabilities].sort((left, right) => {
|
|
if (left.enabled !== right.enabled) {
|
|
return left.enabled ? -1 : 1;
|
|
}
|
|
|
|
return left.slug.localeCompare(right.slug);
|
|
});
|
|
}
|
|
|
|
function readManifestContent(manifestPath?: string): string {
|
|
if (!manifestPath || !existsSync(manifestPath)) {
|
|
return '';
|
|
}
|
|
|
|
try {
|
|
return readFileSync(manifestPath, 'utf-8');
|
|
} catch {
|
|
return '';
|
|
}
|
|
}
|
|
|
|
function setRegistrySnapshot(
|
|
capabilities: SkillCapability[],
|
|
source: RegistrySnapshot['source'],
|
|
): SkillCapability[] {
|
|
registrySnapshot = {
|
|
capabilities: sortCapabilities(capabilities),
|
|
loadedAt: new Date().toISOString(),
|
|
source,
|
|
};
|
|
|
|
return registrySnapshot.capabilities.map(cloneCapability);
|
|
}
|
|
|
|
function buildCapabilityFromConfig(
|
|
skillKey: string,
|
|
config: Partial<SkillConfigRecord> & {
|
|
enabled?: boolean;
|
|
apiKey?: string;
|
|
env?: Record<string, string>;
|
|
},
|
|
): SkillCapability {
|
|
const manifestPath = config.filePath || (config.baseDir ? join(config.baseDir, SKILL_MANIFEST_FILE) : undefined);
|
|
const manifestContent = readManifestContent(manifestPath);
|
|
|
|
return parseSkillCapability({
|
|
skillKey,
|
|
config: {
|
|
...config,
|
|
filePath: manifestPath,
|
|
},
|
|
manifestContent,
|
|
});
|
|
}
|
|
|
|
function readSkillConfigEntriesSync(): Record<string, SkillConfigEntry> {
|
|
const configPath = join(getOpenClawConfigDir(), OPENCLAW_CONFIG_FILE);
|
|
if (!existsSync(configPath)) {
|
|
return {};
|
|
}
|
|
|
|
try {
|
|
const raw = readFileSync(configPath, 'utf-8');
|
|
const parsed = JSON.parse(raw) as {
|
|
skills?: {
|
|
entries?: Record<string, SkillConfigEntry>;
|
|
};
|
|
};
|
|
return parsed.skills?.entries || {};
|
|
} catch {
|
|
return {};
|
|
}
|
|
}
|
|
|
|
function readInstalledSkillsSync(): Record<string, Partial<SkillConfigRecord>> {
|
|
const skillsDir = join(getOpenClawConfigDir(), OPENCLAW_SKILLS_DIR);
|
|
if (!existsSync(skillsDir)) {
|
|
return {};
|
|
}
|
|
|
|
const result: Record<string, Partial<SkillConfigRecord>> = {};
|
|
|
|
for (const entry of readdirSync(skillsDir, { withFileTypes: true })) {
|
|
if (!entry.isDirectory()) {
|
|
continue;
|
|
}
|
|
|
|
const baseDir = join(skillsDir, entry.name);
|
|
const manifestPath = join(baseDir, SKILL_MANIFEST_FILE);
|
|
if (!existsSync(manifestPath)) {
|
|
continue;
|
|
}
|
|
|
|
result[entry.name] = {
|
|
slug: entry.name,
|
|
name: entry.name,
|
|
baseDir,
|
|
filePath: manifestPath,
|
|
};
|
|
}
|
|
|
|
return result;
|
|
}
|
|
|
|
function buildSynchronousSnapshot(): SkillCapability[] {
|
|
const installedSkills = readInstalledSkillsSync();
|
|
const configEntries = readSkillConfigEntriesSync();
|
|
const skillKeys = uniq([
|
|
...Object.keys(installedSkills),
|
|
...Object.keys(configEntries),
|
|
]);
|
|
|
|
const capabilities = skillKeys.map((skillKey) => {
|
|
const installedSkill = installedSkills[skillKey] || {};
|
|
const configEntry = configEntries[skillKey] || {};
|
|
return buildCapabilityFromConfig(skillKey, {
|
|
...installedSkill,
|
|
enabled: configEntry.enabled,
|
|
apiKey: configEntry.apiKey,
|
|
env: configEntry.env,
|
|
});
|
|
});
|
|
|
|
return setRegistrySnapshot(capabilities, 'disk-sync');
|
|
}
|
|
|
|
function ensureRegistrySnapshot(): RegistrySnapshot {
|
|
if (!registrySnapshot) {
|
|
buildSynchronousSnapshot();
|
|
}
|
|
|
|
return registrySnapshot || {
|
|
capabilities: [],
|
|
loadedAt: new Date().toISOString(),
|
|
source: 'disk-sync',
|
|
};
|
|
}
|
|
|
|
export function hydrateSkillCapabilityRegistry(
|
|
configs: Record<string, SkillConfigRecord>,
|
|
): SkillCapability[] {
|
|
const capabilities = Object.entries(configs).map(([skillKey, config]) =>
|
|
buildCapabilityFromConfig(skillKey, config),
|
|
);
|
|
|
|
return setRegistrySnapshot(capabilities, 'skill-config');
|
|
}
|
|
|
|
export async function refreshSkillCapabilityRegistry(): Promise<SkillCapability[]> {
|
|
if (!refreshPromise) {
|
|
refreshPromise = getAllSkillConfigs()
|
|
.then((configs) => hydrateSkillCapabilityRegistry(configs))
|
|
.finally(() => {
|
|
refreshPromise = null;
|
|
});
|
|
}
|
|
|
|
return refreshPromise;
|
|
}
|
|
|
|
export function listSkillCapabilities(options?: { enabledOnly?: boolean }): SkillCapability[] {
|
|
const snapshot = ensureRegistrySnapshot();
|
|
const capabilities = options?.enabledOnly
|
|
? snapshot.capabilities.filter((capability) => capability.enabled)
|
|
: snapshot.capabilities;
|
|
|
|
return capabilities.map(cloneCapability);
|
|
}
|
|
|
|
export function getEnabledSkillCapabilities(): SkillCapability[] {
|
|
return listSkillCapabilities({ enabledOnly: true });
|
|
}
|