import z from "zod"import path from "path"import os from "os"import fs from "fs/promises"import { Config } from "../config/config"import { Instance } from "../project/instance"import { State } from "../project/state"import { runtimeRegexPass, classifierInjectionRegexPass } from "./install/review"import { NamedError } from "@synsci/util/error"import { ConfigMarkdown } from "../config/markdown"import { Log } from "../util/log"import { Global } from "@/global"import { Filesystem } from "@/util/filesystem"import { Flag } from "@/flag/flag"import { Bus } from "@/bus"import { BusEvent } from "@/bus/bus-event"import { Session } from "@/session"import { OpenScience } from "@/openscience"import { Installation } from "@/installation" export namespace Skill { const log = Log.create({ service: "skill" }) export const Info = z.object({ name: z.string(), description: z.string(), location: z.string(), category: z.string().optional(), tags: z.array(z.string()).optional(), /** Whether the skill is user-facing (shows in / autocomplete) or an * internal helper used transitively by other skills. Defaults to true. * Driven by `openscience-skills.json` `entries[]` for URL-installed skills; * bundled / learned skills omit this and are always entries. */ entry: z.boolean().optional(), }) export type Info = z.infer<typeof Info> export const Event = { Updated: BusEvent.define("skill.updated", z.object({})), } export const InvalidError = NamedError.create( "SkillInvalidError", z.object({ path: z.string(), message: z.string().optional(), issues: z.custom<z.core.$ZodIssue[]>().optional(), }), ) export const NameMismatchError = NamedError.create( "SkillNameMismatchError", z.object({ path: z.string(), expected: z.string(), actual: z.string(), }), ) const OPENSCIENCE_SKILL_GLOB = new Bun.Glob("{skill,skills}/**/SKILL.md") const CLAUDE_SKILL_GLOB = new Bun.Glob("skills/**/SKILL.md") const SKILL_GLOB = new Bun.Glob("**/SKILL.md") const USER_SKILL_DIR = path.join(Global.Path.data, "user-skills") const UserSkillName = z.string().regex(/^[a-zA-Z0-9][a-zA-Z0-9_-]{0,63}$/) async function compute() { const skills: Record<string, Info> = {} const addSkill = async (match: string) => { const md = await ConfigMarkdown.parse(match).catch((err) => { const message = ConfigMarkdown.FrontmatterError.isInstance(err) ? err.data.message : `Failed to parse skill ${match}` Bus.publish(Session.Event.Error, { error: new NamedError.Unknown({ message }).toObject() }) log.error("failed to load skill", { skill: match, err }) return undefined }) if (!md) return const parsed = Info.pick({ name: true, description: true, category: true, tags: true, entry: true }).safeParse( md.data, ) if (!parsed.success) return // Block skills with injection-like descriptions const desc = (parsed.data.description ?? "").toLowerCase() if (desc.includes("always run this skill") || desc.includes("must always run")) { log.warn("blocked skill with injection pattern", { name: parsed.data.name, reason: "description contains injection directive", }) return } // Warn on duplicate skill names if (skills[parsed.data.name]) { log.warn("duplicate skill name", { name: parsed.data.name, existing: skills[parsed.data.name].location, duplicate: match, }) } skills[parsed.data.name] = { name: parsed.data.name, description: parsed.data.description, location: match, category: parsed.data.category, tags: parsed.data.tags, entry: parsed.data.entry, } } // Scan .claude/skills/ directories (project-level) const claudeDirs = await Array.fromAsync( Filesystem.up({ targets: [".claude"], start: Instance.directory, stop: Instance.worktree, }), ) // Also include global ~/.claude/skills/ const globalClaude = `${Global.Path.home}/.claude` if (await Filesystem.isDir(globalClaude)) { claudeDirs.push(globalClaude) } if (!Flag.OPENSCIENCE_DISABLE_CLAUDE_CODE_SKILLS) { for (const dir of claudeDirs) { const matches = await Array.fromAsync( CLAUDE_SKILL_GLOB.scan({ cwd: dir, absolute: true, onlyFiles: true, followSymlinks: true, dot: true, }), ).catch((error) => { log.error("failed .claude directory scan for skills", { dir, error }) return [] }) for (const match of matches) { await addSkill(match) } } } // Scan .openscience/skill/ directories for (const dir of await Config.directories()) { for await (const match of OPENSCIENCE_SKILL_GLOB.scan({ cwd: dir, absolute: true, onlyFiles: true, followSymlinks: true, })) { await addSkill(match) } } // === Server-side Skills: fetched from API, cached locally === if (!Flag.OPENSCIENCE_DISABLE_BUNDLED_SKILLS) { const cacheDir = Global.Path.cache // Try fetching skill index from dashboard API const index = await OpenScience.fetchSkillIndex() if (index) { for (const skill of index) { if (!skills[skill.name]) { // Block skills with injection patterns (same check as addSkill) const desc = (skill.description ?? "").toLowerCase() if (desc.includes("always run this skill") || desc.includes("must always run")) { log.warn("blocked API skill with injection pattern", { name: skill.name, reason: "description contains injection directive", }) continue } const cachePath = path.join(cacheDir, "skills", skill.name, "SKILL.md") skills[skill.name] = { name: skill.name, description: skill.description, location: cachePath, // may not exist yet — fetched lazily when invoked category: skill.category, tags: skill.tags, } } } log.info("Loaded skill index from API", { count: index.length }) } else { // Offline fallback: scan cached skills directory const cachedSkillsDir = path.join(cacheDir, "skills") if (await Filesystem.isDir(cachedSkillsDir)) { let count = 0 for await (const match of SKILL_GLOB.scan({ cwd: cachedSkillsDir, absolute: true, onlyFiles: true, followSymlinks: true, })) { await addSkill(match) count++ } log.info("Loaded cached skills (offline)", { count }) } } // Development fallback: only when running from source (not compiled binary) const devSkillsPath = path.join(import.meta.dir, "../../skills") if (Installation.VERSION === "local" && (await Filesystem.isDir(devSkillsPath))) { let count = 0 for await (const match of SKILL_GLOB.scan({ cwd: devSkillsPath, absolute: true, onlyFiles: true, followSymlinks: true, })) { await addSkill(match) count++ } log.info("Loaded dev skills", { path: devSkillsPath, count }) } } // === Learned Skills: from RSI distillation, cloud-synced + local === const learnedDir = path.join(Global.Path.data, "learned-skills") // Sync from cloud: fetch learned skills index and write any missing to disk const cloudLearned = await OpenScience.fetchLearnedSkills().catch(() => null) if (cloudLearned) { for (const entry of cloudLearned) { const skillDir = path.join(learnedDir, entry.name) const skillPath = path.join(skillDir, "SKILL.md") const exists = await Bun.file(skillPath).exists() if (!exists) { const content = await OpenScience.fetchLearnedSkillContent(entry.name).catch(() => null) if (content) { await fs.mkdir(skillDir, { recursive: true }) await Bun.write(skillPath, content) log.info("synced learned skill from cloud", { name: entry.name }) } } } } // Scan local learned-skills directory (includes both local and just-synced cloud skills) if (await Filesystem.isDir(learnedDir)) { let learnedCount = 0 for await (const match of SKILL_GLOB.scan({ cwd: learnedDir, absolute: true, onlyFiles: true, followSymlinks: true, })) { await addSkill(match) learnedCount++ } if (learnedCount > 0) { log.info("Loaded learned skills", { count: learnedCount }) } } // === User Skills: authored locally via openscience/web, private by default === if (await Filesystem.isDir(USER_SKILL_DIR)) { let userCount = 0 for await (const match of SKILL_GLOB.scan({ cwd: USER_SKILL_DIR, absolute: true, onlyFiles: true, followSymlinks: true, })) { await addSkill(match) userCount++ } if (userCount > 0) { log.info("Loaded user skills", { count: userCount }) } } // === Installed Skills: URL-installed third-party skills === // Local-first store at: // ~/.openscience/installed-skills/<ns>/skills/<name>/SKILL.md // mirroring the upstream plugin convention. Cloud sync via // /api/cli/installed-skills returns install pointers (repo_url + sha); // each machine re-fetches from git on first sync. The DB row is just // the install ledger / kill-switch hook. const installedDir = path.join(Global.Path.data, "installed-skills") // One-time on-disk migration from the legacy flat layout // (<ns>/<name>/SKILL.md) → plugin layout (<ns>/skills/<name>/SKILL.md). // Idempotent: skips namespaces that already have the skills/ subdir. if (await Filesystem.isDir(installedDir)) { try { const nsDirs = await fs.readdir(installedDir, { withFileTypes: true }) for (const ns of nsDirs) { if (!ns.isDirectory()) continue const nsPath = path.join(installedDir, ns.name) const skillsSubdir = path.join(nsPath, "skills") if (await Filesystem.isDir(skillsSubdir)) continue // No skills/ subdir — sniff for legacy layout (children with SKILL.md). const children = await fs.readdir(nsPath, { withFileTypes: true }) await fs.mkdir(skillsSubdir, { recursive: true }) let migrated = 0 for (const c of children) { if (!c.isDirectory()) continue const src = path.join(nsPath, c.name) const hasSkill = await Bun.file(path.join(src, "SKILL.md")).exists() if (!hasSkill) continue await fs.rename(src, path.join(skillsSubdir, c.name)) migrated++ } if (migrated > 0) { log.info("migrated installed skills to plugin layout", { namespace: ns.name, migrated }) } else { await fs.rmdir(skillsSubdir).catch(() => {}) } } } catch { /* migration best-effort */ } } const cloudInstalled = await OpenScience.fetchInstalledSkills().catch(() => null) if (cloudInstalled) { for (const entry of cloudInstalled) { const skillDir = path.join(installedDir, entry.namespace, "skills", entry.name) const skillPath = path.join(skillDir, "SKILL.md") const exists = await Bun.file(skillPath).exists() if (!exists) { // Pointer-only DB: re-fetch from git using stored repo_url + sha. // Skips Layer 3 — the first install's classifier verdict is // authoritative; the SHA is content-addressed so the content is // provably identical. Layers 1 + 2 still run as paranoid defense. try { const { gitFetchPinned } = await import("./install/git-fetch") await gitFetchPinned({ repo_url: entry.repo_url, pinned_sha: entry.pinned_sha, namespace: entry.namespace, skillName: entry.name, destDir: skillDir, }) log.info("synced installed skill from git", { namespace: entry.namespace, name: entry.name, sha: entry.pinned_sha.slice(0, 7), }) } catch (e) { log.warn("failed to sync installed skill from git", { namespace: entry.namespace, name: entry.name, error: e instanceof Error ? e.message : String(e), }) } } } } if (await Filesystem.isDir(installedDir)) { let installedCount = 0 const entriesByNs = new Map<string, Set<string> | null>() try { const nsDirs = await fs.readdir(installedDir, { withFileTypes: true }) for (const ns of nsDirs) { if (!ns.isDirectory()) continue const manifestPath = path.join(installedDir, ns.name, "openscience-skills.json") try { const raw = await Bun.file(manifestPath).text() const parsed = JSON.parse(raw) as { entries?: unknown } if (Array.isArray(parsed.entries)) { entriesByNs.set(ns.name, new Set(parsed.entries.filter((e): e is string => typeof e === "string"))) } else { entriesByNs.set(ns.name, null) } } catch { entriesByNs.set(ns.name, null) } } } catch { /* installedDir read failed — skip */ } for await (const match of SKILL_GLOB.scan({ cwd: installedDir, absolute: true, onlyFiles: true, followSymlinks: true, })) { await addSkill(match) installedCount++ // SKILL_GLOB matches <installedDir>/<ns>/skills/<name>/SKILL.md. const rel = match.slice(installedDir.length + 1) const segments = rel.split("/") const ns = segments[0] const skillName = segments[2] const entrySet = entriesByNs.get(ns) if (entrySet) { const skill = Object.values(skills).find((s) => s.location === match) if (skill) { skill.entry = entrySet.has(skillName) || entrySet.has(skill.name) } } } if (installedCount > 0) { log.info("Loaded installed skills", { count: installedCount }) } } // Scan additional skill paths from config const config = await Config.get() for (const skillPath of config.skills?.paths ?? []) { const expanded = skillPath.startsWith("~/") ? path.join(os.homedir(), skillPath.slice(2)) : skillPath const resolved = path.isAbsolute(expanded) ? expanded : path.join(Instance.directory, expanded) if (!(await Filesystem.isDir(resolved))) { log.warn("skill path not found", { path: resolved }) continue } for await (const match of SKILL_GLOB.scan({ cwd: resolved, absolute: true, onlyFiles: true, followSymlinks: true, })) { await addSkill(match) } } return skills } export const state = Instance.state(compute) export async function invalidate() { State.clear(Instance.directory, compute) await Bus.publish(Event.Updated, {}) } export async function writeUser(input: { name: string; content: string }) { const name = UserSkillName.parse(input.name) const dir = path.join(USER_SKILL_DIR, name) const file = path.join(dir, "SKILL.md") const tmp = path.join(USER_SKILL_DIR, `${name}.${Date.now()}.tmp.md`) await fs.mkdir(USER_SKILL_DIR, { recursive: true }) await Bun.write(tmp, input.content, { mode: 0o600 }) try { const md = await ConfigMarkdown.parse(tmp) const parsed = Info.pick({ name: true, description: true, category: true, tags: true, entry: true }).safeParse( md.data, ) if (!parsed.success) { throw new InvalidError({ path: file, message: "Skill frontmatter must include name and description.", issues: parsed.error.issues, }) } if (parsed.data.name !== name) { throw new NameMismatchError({ path: file, expected: name, actual: parsed.data.name, }) } // Server-side moderation: block injection / catastrophic patterns the // same way URL-installed skills are screened (Layers 1 + 2). Warnings // (Layer 4) are advisory and don't block local authoring. const entry = { namespace: "user", name: parsed.data.name, description: parsed.data.description ?? "", content: input.content, scripts: [], references: [], } const rejected = [...runtimeRegexPass([entry]).rejected, ...classifierInjectionRegexPass([entry]).rejected] if (rejected.length > 0) { throw new InvalidError({ path: file, message: `Skill rejected by safety review: ${rejected.map((r) => r.reason).join("; ")}`, }) } await fs.mkdir(dir, { recursive: true }) await Bun.write(file, input.content, { mode: 0o600 }) await invalidate() return { ...parsed.data, location: file, } satisfies Info } finally { await fs.rm(tmp, { force: true }).catch(() => {}) } } export async function deleteUser(name: string) { const safe = UserSkillName.parse(name) const dir = path.join(USER_SKILL_DIR, safe) await fs.rm(dir, { recursive: true, force: true }) await invalidate() return true }Lines 1–500 of 509. Load more
import z from "zod" import path from "path" import os from "os" import fs from "fs/promises" import { Config } from "../config/config" import { Instance } from "../project/instance" import { State } from "../project/state" import { runtimeRegexPass, classifierInjectionRegexPass } from "./install/review" import { NamedError } from "@synsci/util/error" import { ConfigMarkdown } from "../config/markdown" import { Log } from "../util/log" import { Global } from "@/global" import { Filesystem } from "@/util/filesystem" import { Flag } from "@/flag/flag" import { Bus } from "@/bus" import { BusEvent } from "@/bus/bus-event" import { Session } from "@/session" import { OpenScience } from "@/openscience" import { Installation } from "@/installation" export namespace Skill { const log = Log.create({ service: "skill" }) export const Info = z.object({ name: z.string(), description: z.string(), location: z.string(), category: z.string().optional(), tags: z.array(z.string()).optional(), /** Whether the skill is user-facing (shows in / autocomplete) or an * internal helper used transitively by other skills. Defaults to true. * Driven by `openscience-skills.json` `entries[]` for URL-installed skills; * bundled / learned skills omit this and are always entries. */ entry: z.boolean().optional(), }) export type Info = z.infer<typeof Info> export const Event = { Updated: BusEvent.define("skill.updated", z.object({})), } export const InvalidError = NamedError.create( "SkillInvalidError", z.object({ path: z.string(), message: z.string().optional(), issues: z.custom<z.core.$ZodIssue[]>().optional(), }), ) export const NameMismatchError = NamedError.create( "SkillNameMismatchError", z.object({ path: z.string(), expected: z.string(), actual: z.string(), }), ) const OPENSCIENCE_SKILL_GLOB = new Bun.Glob("{skill,skills}/**/SKILL.md") const CLAUDE_SKILL_GLOB = new Bun.Glob("skills/**/SKILL.md") const SKILL_GLOB = new Bun.Glob("**/SKILL.md") const USER_SKILL_DIR = path.join(Global.Path.data, "user-skills") const UserSkillName = z.string().regex(/^[a-zA-Z0-9][a-zA-Z0-9_-]{0,63}$/) async function compute() { const skills: Record<string, Info> = {} const addSkill = async (match: string) => { const md = await ConfigMarkdown.parse(match).catch((err) => { const message = ConfigMarkdown.FrontmatterError.isInstance(err) ? err.data.message : `Failed to parse skill ${match}` Bus.publish(Session.Event.Error, { error: new NamedError.Unknown({ message }).toObject() }) log.error("failed to load skill", { skill: match, err }) return undefined }) if (!md) return const parsed = Info.pick({ name: true, description: true, category: true, tags: true, entry: true }).safeParse( md.data, ) if (!parsed.success) return // Block skills with injection-like descriptions const desc = (parsed.data.description ?? "").toLowerCase() if (desc.includes("always run this skill") || desc.includes("must always run")) { log.warn("blocked skill with injection pattern", { name: parsed.data.name, reason: "description contains injection directive", }) return } // Warn on duplicate skill names if (skills[parsed.data.name]) { log.warn("duplicate skill name", { name: parsed.data.name, existing: skills[parsed.data.name].location, duplicate: match, }) } skills[parsed.data.name] = { name: parsed.data.name, description: parsed.data.description, location: match, category: parsed.data.category, tags: parsed.data.tags, entry: parsed.data.entry, } } // Scan .claude/skills/ directories (project-level) const claudeDirs = await Array.fromAsync( Filesystem.up({ targets: [".claude"], start: Instance.directory, stop: Instance.worktree, }), ) // Also include global ~/.claude/skills/ const globalClaude = `${Global.Path.home}/.claude` if (await Filesystem.isDir(globalClaude)) { claudeDirs.push(globalClaude) } if (!Flag.OPENSCIENCE_DISABLE_CLAUDE_CODE_SKILLS) { for (const dir of claudeDirs) { const matches = await Array.fromAsync( CLAUDE_SKILL_GLOB.scan({ cwd: dir, absolute: true, onlyFiles: true, followSymlinks: true, dot: true, }), ).catch((error) => { log.error("failed .claude directory scan for skills", { dir, error }) return [] }) for (const match of matches) { await addSkill(match) } } } // Scan .openscience/skill/ directories for (const dir of await Config.directories()) { for await (const match of OPENSCIENCE_SKILL_GLOB.scan({ cwd: dir, absolute: true, onlyFiles: true, followSymlinks: true, })) { await addSkill(match) } } // === Server-side Skills: fetched from API, cached locally === if (!Flag.OPENSCIENCE_DISABLE_BUNDLED_SKILLS) { const cacheDir = Global.Path.cache // Try fetching skill index from dashboard API const index = await OpenScience.fetchSkillIndex() if (index) { for (const skill of index) { if (!skills[skill.name]) { // Block skills with injection patterns (same check as addSkill) const desc = (skill.description ?? "").toLowerCase() if (desc.includes("always run this skill") || desc.includes("must always run")) { log.warn("blocked API skill with injection pattern", { name: skill.name, reason: "description contains injection directive", }) continue } const cachePath = path.join(cacheDir, "skills", skill.name, "SKILL.md") skills[skill.name] = { name: skill.name, description: skill.description, location: cachePath, // may not exist yet — fetched lazily when invoked category: skill.category, tags: skill.tags, } } } log.info("Loaded skill index from API", { count: index.length }) } else { // Offline fallback: scan cached skills directory const cachedSkillsDir = path.join(cacheDir, "skills") if (await Filesystem.isDir(cachedSkillsDir)) { let count = 0 for await (const match of SKILL_GLOB.scan({ cwd: cachedSkillsDir, absolute: true, onlyFiles: true, followSymlinks: true, })) { await addSkill(match) count++ } log.info("Loaded cached skills (offline)", { count }) } } // Development fallback: only when running from source (not compiled binary) const devSkillsPath = path.join(import.meta.dir, "../../skills") if (Installation.VERSION === "local" && (await Filesystem.isDir(devSkillsPath))) { let count = 0 for await (const match of SKILL_GLOB.scan({ cwd: devSkillsPath, absolute: true, onlyFiles: true, followSymlinks: true, })) { await addSkill(match) count++ } log.info("Loaded dev skills", { path: devSkillsPath, count }) } } // === Learned Skills: from RSI distillation, cloud-synced + local === const learnedDir = path.join(Global.Path.data, "learned-skills") // Sync from cloud: fetch learned skills index and write any missing to disk const cloudLearned = await OpenScience.fetchLearnedSkills().catch(() => null) if (cloudLearned) { for (const entry of cloudLearned) { const skillDir = path.join(learnedDir, entry.name) const skillPath = path.join(skillDir, "SKILL.md") const exists = await Bun.file(skillPath).exists() if (!exists) { const content = await OpenScience.fetchLearnedSkillContent(entry.name).catch(() => null) if (content) { await fs.mkdir(skillDir, { recursive: true }) await Bun.write(skillPath, content) log.info("synced learned skill from cloud", { name: entry.name }) } } } } // Scan local learned-skills directory (includes both local and just-synced cloud skills) if (await Filesystem.isDir(learnedDir)) { let learnedCount = 0 for await (const match of SKILL_GLOB.scan({ cwd: learnedDir, absolute: true, onlyFiles: true, followSymlinks: true, })) { await addSkill(match) learnedCount++ } if (learnedCount > 0) { log.info("Loaded learned skills", { count: learnedCount }) } } // === User Skills: authored locally via openscience/web, private by default === if (await Filesystem.isDir(USER_SKILL_DIR)) { let userCount = 0 for await (const match of SKILL_GLOB.scan({ cwd: USER_SKILL_DIR, absolute: true, onlyFiles: true, followSymlinks: true, })) { await addSkill(match) userCount++ } if (userCount > 0) { log.info("Loaded user skills", { count: userCount }) } } // === Installed Skills: URL-installed third-party skills === // Local-first store at: // ~/.openscience/installed-skills/<ns>/skills/<name>/SKILL.md // mirroring the upstream plugin convention. Cloud sync via // /api/cli/installed-skills returns install pointers (repo_url + sha); // each machine re-fetches from git on first sync. The DB row is just // the install ledger / kill-switch hook. const installedDir = path.join(Global.Path.data, "installed-skills") // One-time on-disk migration from the legacy flat layout // (<ns>/<name>/SKILL.md) → plugin layout (<ns>/skills/<name>/SKILL.md). // Idempotent: skips namespaces that already have the skills/ subdir. if (await Filesystem.isDir(installedDir)) { try { const nsDirs = await fs.readdir(installedDir, { withFileTypes: true }) for (const ns of nsDirs) { if (!ns.isDirectory()) continue const nsPath = path.join(installedDir, ns.name) const skillsSubdir = path.join(nsPath, "skills") if (await Filesystem.isDir(skillsSubdir)) continue // No skills/ subdir — sniff for legacy layout (children with SKILL.md). const children = await fs.readdir(nsPath, { withFileTypes: true }) await fs.mkdir(skillsSubdir, { recursive: true }) let migrated = 0 for (const c of children) { if (!c.isDirectory()) continue const src = path.join(nsPath, c.name) const hasSkill = await Bun.file(path.join(src, "SKILL.md")).exists() if (!hasSkill) continue await fs.rename(src, path.join(skillsSubdir, c.name)) migrated++ } if (migrated > 0) { log.info("migrated installed skills to plugin layout", { namespace: ns.name, migrated }) } else { await fs.rmdir(skillsSubdir).catch(() => {}) } } } catch { /* migration best-effort */ } } const cloudInstalled = await OpenScience.fetchInstalledSkills().catch(() => null) if (cloudInstalled) { for (const entry of cloudInstalled) { const skillDir = path.join(installedDir, entry.namespace, "skills", entry.name) const skillPath = path.join(skillDir, "SKILL.md") const exists = await Bun.file(skillPath).exists() if (!exists) { // Pointer-only DB: re-fetch from git using stored repo_url + sha. // Skips Layer 3 — the first install's classifier verdict is // authoritative; the SHA is content-addressed so the content is // provably identical. Layers 1 + 2 still run as paranoid defense. try { const { gitFetchPinned } = await import("./install/git-fetch") await gitFetchPinned({ repo_url: entry.repo_url, pinned_sha: entry.pinned_sha, namespace: entry.namespace, skillName: entry.name, destDir: skillDir, }) log.info("synced installed skill from git", { namespace: entry.namespace, name: entry.name, sha: entry.pinned_sha.slice(0, 7), }) } catch (e) { log.warn("failed to sync installed skill from git", { namespace: entry.namespace, name: entry.name, error: e instanceof Error ? e.message : String(e), }) } } } } if (await Filesystem.isDir(installedDir)) { let installedCount = 0 const entriesByNs = new Map<string, Set<string> | null>() try { const nsDirs = await fs.readdir(installedDir, { withFileTypes: true }) for (const ns of nsDirs) { if (!ns.isDirectory()) continue const manifestPath = path.join(installedDir, ns.name, "openscience-skills.json") try { const raw = await Bun.file(manifestPath).text() const parsed = JSON.parse(raw) as { entries?: unknown } if (Array.isArray(parsed.entries)) { entriesByNs.set(ns.name, new Set(parsed.entries.filter((e): e is string => typeof e === "string"))) } else { entriesByNs.set(ns.name, null) } } catch { entriesByNs.set(ns.name, null) } } } catch { /* installedDir read failed — skip */ } for await (const match of SKILL_GLOB.scan({ cwd: installedDir, absolute: true, onlyFiles: true, followSymlinks: true, })) { await addSkill(match) installedCount++ // SKILL_GLOB matches <installedDir>/<ns>/skills/<name>/SKILL.md. const rel = match.slice(installedDir.length + 1) const segments = rel.split("/") const ns = segments[0] const skillName = segments[2] const entrySet = entriesByNs.get(ns) if (entrySet) { const skill = Object.values(skills).find((s) => s.location === match) if (skill) { skill.entry = entrySet.has(skillName) || entrySet.has(skill.name) } } } if (installedCount > 0) { log.info("Loaded installed skills", { count: installedCount }) } } // Scan additional skill paths from config const config = await Config.get() for (const skillPath of config.skills?.paths ?? []) { const expanded = skillPath.startsWith("~/") ? path.join(os.homedir(), skillPath.slice(2)) : skillPath const resolved = path.isAbsolute(expanded) ? expanded : path.join(Instance.directory, expanded) if (!(await Filesystem.isDir(resolved))) { log.warn("skill path not found", { path: resolved }) continue } for await (const match of SKILL_GLOB.scan({ cwd: resolved, absolute: true, onlyFiles: true, followSymlinks: true, })) { await addSkill(match) } } return skills } export const state = Instance.state(compute) export async function invalidate() { State.clear(Instance.directory, compute) await Bus.publish(Event.Updated, {}) } export async function writeUser(input: { name: string; content: string }) { const name = UserSkillName.parse(input.name) const dir = path.join(USER_SKILL_DIR, name) const file = path.join(dir, "SKILL.md") const tmp = path.join(USER_SKILL_DIR, `${name}.${Date.now()}.tmp.md`) await fs.mkdir(USER_SKILL_DIR, { recursive: true }) await Bun.write(tmp, input.content, { mode: 0o600 }) try { const md = await ConfigMarkdown.parse(tmp) const parsed = Info.pick({ name: true, description: true, category: true, tags: true, entry: true }).safeParse( md.data, ) if (!parsed.success) { throw new InvalidError({ path: file, message: "Skill frontmatter must include name and description.", issues: parsed.error.issues, }) } if (parsed.data.name !== name) { throw new NameMismatchError({ path: file, expected: name, actual: parsed.data.name, }) } // Server-side moderation: block injection / catastrophic patterns the // same way URL-installed skills are screened (Layers 1 + 2). Warnings // (Layer 4) are advisory and don't block local authoring. const entry = { namespace: "user", name: parsed.data.name, description: parsed.data.description ?? "", content: input.content, scripts: [], references: [], } const rejected = [...runtimeRegexPass([entry]).rejected, ...classifierInjectionRegexPass([entry]).rejected] if (rejected.length > 0) { throw new InvalidError({ path: file, message: `Skill rejected by safety review: ${rejected.map((r) => r.reason).join("; ")}`, }) } await fs.mkdir(dir, { recursive: true }) await Bun.write(file, input.content, { mode: 0o600 }) await invalidate() return { ...parsed.data, location: file, } satisfies Info } finally { await fs.rm(tmp, { force: true }).catch(() => {}) } } export async function deleteUser(name: string) { const safe = UserSkillName.parse(name) const dir = path.join(USER_SKILL_DIR, safe) await fs.rm(dir, { recursive: true, force: true }) await invalidate() return true }