diff --git a/.oxlintrc.json b/.oxlintrc.json index ad2293e9ca8f..233e576de1bd 100644 --- a/.oxlintrc.json +++ b/.oxlintrc.json @@ -186,7 +186,7 @@ "node_modules/", "patches/", "pnpm-lock.yaml", - "skills/", + "skills/**", "src/auto-reply/reply/export-html/template.js", "src/canvas-host/a2ui/a2ui.bundle.js", "vendor/", diff --git a/src/agents/agent-command.ts b/src/agents/agent-command.ts index 47d7851b5c9f..59b5401ff9cc 100644 --- a/src/agents/agent-command.ts +++ b/src/agents/agent-command.ts @@ -42,7 +42,6 @@ import { normalizeOptionalString } from "../shared/string-coerce.js"; import { resolveEffectiveAgentSkillFilter } from "../skills/discovery/agent-filter.js"; import type { getRemoteSkillEligibility } from "../skills/runtime/remote.js"; import type { resolveReusableWorkspaceSkillSnapshot } from "../skills/runtime/session-snapshot.js"; -import type { SkillSnapshot } from "../skills/types.js"; import { sanitizeForLog } from "../terminal/ansi.js"; import { createTrajectoryRuntimeRecorder } from "../trajectory/runtime.js"; import { resolveUserPath } from "../utils.js"; diff --git a/src/skills/discovery/service.test.ts b/src/skills/discovery/service.test.ts index 83dca0db340c..c78628dba57e 100644 --- a/src/skills/discovery/service.test.ts +++ b/src/skills/discovery/service.test.ts @@ -224,7 +224,7 @@ describe("SkillsService", () => { return workspaceDir; }), ); - const firstWorkspace = workspaces[0]!; + const firstWorkspace = workspaces[0]; const first = service.getIndex({ workspaceDir: firstWorkspace, ...isolatedSkillRoots(firstWorkspace), diff --git a/src/skills/discovery/trust.ts b/src/skills/discovery/trust.ts index 4d1b105662e9..4f76bbe5edb3 100644 --- a/src/skills/discovery/trust.ts +++ b/src/skills/discovery/trust.ts @@ -46,13 +46,27 @@ export function resolveSkillOwner(params: { sourceLabel: string; skillPath: string; }): string { - if (params.sourceKind === "workspace") return "workspace"; - if (params.sourceKind === "generated") return "workspace"; - if (params.sourceKind === "bundled") return "openclaw-release"; - if (params.sourceKind === "clawhub") return "clawhub"; - if (params.sourceKind === "plugin") return "plugin"; - if (params.sourceKind === "system") return "openclaw-system"; - if (params.sourceLabel === "agents-skills-personal") return "user"; + if (params.sourceKind === "workspace") { + return "workspace"; + } + if (params.sourceKind === "generated") { + return "workspace"; + } + if (params.sourceKind === "bundled") { + return "openclaw-release"; + } + if (params.sourceKind === "clawhub") { + return "clawhub"; + } + if (params.sourceKind === "plugin") { + return "plugin"; + } + if (params.sourceKind === "system") { + return "openclaw-system"; + } + if (params.sourceLabel === "agents-skills-personal") { + return "user"; + } return path.basename(path.dirname(params.skillPath)) || "extra"; } @@ -72,6 +86,7 @@ export function resolveSkillWritablePolicy(sourceKind: SkillSourceKind): SkillWr case "extra": return { writable: false, reason: "extra-root-load-only" }; } + return { writable: false, reason: "unknown-source-load-only" }; } export function resolveSkillTrustInfo(entry: SkillEntry): SkillTrustInfo { diff --git a/src/skills/lifecycle/upload-install.ts b/src/skills/lifecycle/upload-install.ts index 727792c228c5..a02dbc243ab1 100644 --- a/src/skills/lifecycle/upload-install.ts +++ b/src/skills/lifecycle/upload-install.ts @@ -1,4 +1,5 @@ import type { OpenClawConfig } from "../../config/types.openclaw.js"; +import type { ArchiveLogger } from "../../infra/archive.js"; import { formatErrorMessage } from "../../infra/errors.js"; import { installSkillArchiveFromPath, @@ -52,7 +53,7 @@ export async function installUploadedSkillArchive(params: { timeoutMs?: number; workspaceDir: string; config: OpenClawConfig; - log?: (message: string) => void; + log?: ArchiveLogger; store?: SkillUploadStore; }): Promise { const store = params.store ?? defaultSkillUploadStore; diff --git a/src/skills/loading/local-loader.ts b/src/skills/loading/local-loader.ts index 879df5d78137..6b5630990247 100644 --- a/src/skills/loading/local-loader.ts +++ b/src/skills/loading/local-loader.ts @@ -93,7 +93,7 @@ function listCandidateSkillDirs(dir: string): string[] { entry.isDirectory() && !entry.name.startsWith(".") && entry.name !== "node_modules", ) .map((entry) => path.join(dir, entry.name)) - .sort((left, right) => left.localeCompare(right)); + .toSorted((left, right) => left.localeCompare(right)); } catch { return []; } diff --git a/src/skills/loading/plugin-skills.ts b/src/skills/loading/plugin-skills.ts index dd9e74dd8e7e..24470c4e65f7 100644 --- a/src/skills/loading/plugin-skills.ts +++ b/src/skills/loading/plugin-skills.ts @@ -151,7 +151,9 @@ function collectSkillTargets(dir: string, targets: Map): void { }).entries; for (const entry of entries) { const childPath = entry.path; - if (!hasPublishableSkillFile({ skillDir: childPath, rootDir: dir })) continue; + if (!hasPublishableSkillFile({ skillDir: childPath, rootDir: dir })) { + continue; + } const basename = entry.name; const existing = targets.get(basename); if (existing) { diff --git a/src/skills/loading/workspace.ts b/src/skills/loading/workspace.ts index 21a26eaf2151..8d8af2d96378 100644 --- a/src/skills/loading/workspace.ts +++ b/src/skills/loading/workspace.ts @@ -65,12 +65,14 @@ function resolveCompactHomePrefixes(): string[] { const realHomes = resolvedHomes .map((home) => tryRealpath(home)) .filter((home): home is string => !!home); - return uniqueStrings([...resolvedHomes, ...realHomes]).sort((a, b) => b.length - a.length); + return uniqueStrings([...resolvedHomes, ...realHomes]).toSorted((a, b) => b.length - a.length); } function compactSkillPaths(skills: Skill[]): Skill[] { const homes = resolveCompactHomePrefixes(); - if (homes.length === 0) return skills; + if (homes.length === 0) { + return skills; + } return skills.map((s) => ({ ...s, filePath: compactHomePath(s.filePath, homes), @@ -106,12 +108,12 @@ function compactPathForConsoleMessage(filePath: string): string { function isSkillVisibleInAvailableSkillsPrompt(entry: SkillEntry): boolean { if (entry.exposure) { - return entry.exposure.includeInAvailableSkillsPrompt !== false; + return entry.exposure.includeInAvailableSkillsPrompt ?? true; } if (entry.invocation) { - return entry.invocation.disableModelInvocation !== true; + return !entry.invocation.disableModelInvocation; } - return entry.skill.disableModelInvocation !== true; + return !entry.skill.disableModelInvocation; } function filterSkillEntries( @@ -839,8 +841,7 @@ function loadGeneratedPluginSkillRecords(params: { if (loadedSkills.length > maxSkillsLoadedPerSource) { return loadedSkills - .slice() - .sort((a, b) => a.skill.name.localeCompare(b.skill.name, "en")) + .toSorted((a, b) => a.skill.name.localeCompare(b.skill.name, "en")) .slice(0, maxSkillsLoadedPerSource); } return loadedSkills; @@ -1107,8 +1108,7 @@ function loadSkillEntries( if (loadedSkills.length > maxSkillsLoadedPerSource) { return loadedSkills - .slice() - .sort((a, b) => a.skill.name.localeCompare(b.skill.name, "en")) + .toSorted((a, b) => a.skill.name.localeCompare(b.skill.name, "en")) .slice(0, maxSkillsLoadedPerSource); } @@ -1193,7 +1193,7 @@ function loadSkillEntries( } const skillEntries: SkillEntry[] = Array.from(merged.values()) - .sort((a, b) => a.skill.name.localeCompare(b.skill.name, "en")) + .toSorted((a, b) => a.skill.name.localeCompare(b.skill.name, "en")) .map((record) => { const skill = record.skill; const frontmatter = @@ -1205,22 +1205,27 @@ function loadSkillEntries( }) ?? ({} as ParsedSkillFrontmatter); const invocation = resolveSkillInvocationPolicy(frontmatter); - return { + const entry: SkillEntry = { skill, frontmatter, metadata: resolveSkillEntryMetadata({ frontmatter, skillDir: skill.baseDir }), invocation, - ...(record.syncSourceDir !== undefined ? { syncSourceDir: record.syncSourceDir } : {}), - ...(record.syncDirName !== undefined ? { syncDirName: record.syncDirName } : {}), exposure: { includeInRuntimeRegistry: true, // Freshly loaded entries preserve the documented disable-model-invocation // contract, while legacy entries without exposure metadata still use the // fallback in isSkillVisibleInAvailableSkillsPrompt(). - includeInAvailableSkillsPrompt: invocation.disableModelInvocation !== true, - userInvocable: invocation.userInvocable !== false, + includeInAvailableSkillsPrompt: !invocation.disableModelInvocation, + userInvocable: invocation.userInvocable ?? true, }, }; + if (record.syncSourceDir !== undefined) { + entry.syncSourceDir = record.syncSourceDir; + } + if (record.syncDirName !== undefined) { + entry.syncDirName = record.syncDirName; + } + return entry; }); return skillEntries; } @@ -1240,7 +1245,9 @@ function escapeXml(str: string): string { * preserving awareness of all skills before resorting to dropping. */ export function formatSkillsCompact(skills: Skill[]): string { - if (skills.length === 0) return ""; + if (skills.length === 0) { + return ""; + } const lines = [ "\n\nThe following skills provide specialized instructions for specific tasks.", "Use the read tool to load a skill's file when the task matches its name.", @@ -1392,9 +1399,9 @@ function resolveWorkspaceSkillPromptState( // Budget checks and final render both use this same representation so the // tier decision is based on the exact strings that end up in the prompt. // resolvedSkills keeps canonical paths for snapshot / runtime consumers. - const promptSkills = compactSkillPaths(resolvedSkills) - .slice() - .sort((a, b) => a.name.localeCompare(b.name, "en")); + const promptSkills = compactSkillPaths(resolvedSkills).toSorted((a, b) => + a.name.localeCompare(b.name, "en"), + ); const { skillsForPrompt, truncated, compact } = applySkillsPromptLimits({ skills: promptSkills, config: opts?.config,