Files
bare-operating-system/packages/bare-os-coreutils/lib/agent/agent-skills.js
T
2026-08-18 18:11:28 -04:00

190 lines
7.0 KiB
JavaScript

/**
* Agent skills: discover SKILL.md under workspace/skills/ (highest precedence) then ~/.agent/skills/.
* Depends on bareAgentWorkspaceDecode from agent-workspace.js (same preamble order).
*/
/**
* @param {string} text
* @returns {{ front: Record<string, string>, body: string }}
*/
function bareAgentParseSkillFrontmatter(text) {
const t = String(text || '')
if (!t.startsWith('---')) return { front: {}, body: t.trim() }
const nl = t.indexOf('\n')
const afterFirst = nl === -1 ? '' : t.slice(nl + 1)
const end = afterFirst.search(/\n---\s*(?:\n|$)/)
if (end === -1) return { front: {}, body: t.trim() }
const yamlBlock = afterFirst.slice(0, end)
const body = afterFirst.slice(end + 1).replace(/^---\s*/, '').replace(/^\r?\n/, '')
/** @type {Record<string, string>} */
const front = {}
for (const line of yamlBlock.split(/\r?\n/)) {
const m = line.match(/^([A-Za-z0-9_-]+):\s*(.*)$/)
if (m) front[m[1]] = m[2].trim()
}
return { front, body: body.trim() }
}
/**
* @param {string} full SKILL.md text
* @param {string} folderName directory basename
*/
function bareAgentSkillMetaFromMarkdown(full, folderName) {
const { front } = bareAgentParseSkillFrontmatter(full)
const name = (front.name || folderName || 'unnamed').trim() || folderName
const descFromFront =
front.description && String(front.description).trim()
? String(front.description).trim()
: ''
const descFromBody =
full
.split(/\r?\n/)
.find((l) => {
const x = l.trim()
return x && !x.startsWith('---') && !x.startsWith('#')
})
?.trim() || ''
const description = (descFromFront || descFromBody || 'Skill').slice(0, 400)
return { name, description }
}
/**
* @param {Record<string, unknown>} ctx
* @param {{ workspaceSkills: string, skillsGlobal: string }} paths
* @returns {Promise<{ id: string, name: string, description: string, path: string, source: string }[]>}
*/
async function bareAgentDiscoverSkills(ctx, paths) {
const vfs = ctx.vfs
if (!vfs || typeof vfs.readdir !== 'function' || typeof vfs.readFile !== 'function')
return []
/** @type {{ id: string, name: string, description: string, path: string, source: string }[]} */
const out = []
const seen = new Set()
/**
* @param {string} root
* @param {string} source
*/
async function scanRoot(root, source) {
let names = []
try {
names = await vfs.readdir(root)
} catch {
return
}
if (!Array.isArray(names)) return
for (const raw of names) {
const entry = String(raw)
if (!entry || entry.startsWith('.')) continue
const skillMd = root.replace(/\/+$/, '') + '/' + entry + '/SKILL.md'
try {
const buf = await vfs.readFile(skillMd)
if (!buf || !buf.length) continue
const full = bareAgentWorkspaceDecode(ctx, buf)
const meta = bareAgentSkillMetaFromMarkdown(full, entry)
const keys = [entry.toLowerCase(), meta.name.toLowerCase()]
let dup = false
for (const k of keys) {
if (seen.has(k)) dup = true
}
if (dup) continue
for (const k of keys) seen.add(k)
out.push({
id: entry,
name: meta.name,
description: meta.description,
path: skillMd,
source
})
} catch {
/* not a skill dir */
}
}
}
await scanRoot(paths.workspaceSkills, 'workspace')
await scanRoot(paths.skillsGlobal, 'global')
let extras = Array.isArray(paths.extraSkillRoots) ? paths.extraSkillRoots : []
if (!extras.length && typeof bareAgentDiscoverProjectSkillRoots === 'function') {
const env =
ctx.env && typeof ctx.env === 'object'
? /** @type {Record<string, string>} */ (ctx.env)
: {}
const start = String(env.PWD || env.CWD || env.HOME || '').trim()
if (start) extras = await bareAgentDiscoverProjectSkillRoots(ctx, start)
}
for (let i = 0; i < extras.length; i++) {
const item = extras[i]
const root = typeof item === 'string' ? item : String((item && item.path) || '')
const source =
typeof item === 'object' && item && item.source ? String(item.source) : 'project'
if (root) await scanRoot(root, source)
}
return out
}
/**
* Compact Markdown block for system prompt (names + short descriptions only).
* @param {Record<string, unknown>} ctx
* @param {{ workspaceSkills: string, skillsGlobal: string }} paths
* @param {number} [maxChars]
*/
async function bareAgentSkillsCompactPrompt(ctx, paths, maxChars) {
const cap = Math.min(Math.max(Number(maxChars) || 4000, 500), 12000)
const skills = await bareAgentDiscoverSkills(ctx, paths)
let block =
'## Available skills (compact index)\n' +
'Each skill is a directory with **SKILL.md** (optional YAML frontmatter: `name`, `description`, …).\n' +
'**Workspace skills** (`~/.agent/workspace/skills/`) override **global** (`~/.agent/skills/`) and walk-up `.grok/skills` / `.agents/skills` when names match.\n' +
'To run one: call the **read_skill** tool with the skill id or frontmatter `name` before following its instructions.\n\n'
if (!skills.length) {
block += '(No skills discovered yet — add folders under `workspace/skills/<id>/SKILL.md`.)\n'
return block.length > cap ? block.slice(0, cap) + '\n…\n' : block
}
block += '| id | name | source | description |\n| --- | --- | --- | --- |\n'
for (const s of skills) {
const desc = s.description.replace(/\|/g, '/').replace(/\r?\n/g, ' ').slice(0, 160)
block +=
'| `' +
s.id.replace(/`/g, "'") +
'` | ' +
s.name.replace(/\|/g, '/').replace(/\r?\n/g, ' ') +
' | ' +
s.source +
' | ' +
desc +
' |\n'
}
if (block.length > cap) block = block.slice(0, cap) + '\n… truncated\n'
return block
}
/**
* Load full SKILL.md for a skill matched by folder id or frontmatter name (case-insensitive).
* @param {Record<string, unknown>} ctx
* @param {{ workspaceSkills: string, skillsGlobal: string }} paths
* @param {string} skillQuery
*/
async function bareAgentLoadSkillMarkdown(ctx, paths, skillQuery) {
const q = String(skillQuery || '')
.trim()
.toLowerCase()
if (!q) return { ok: false, error: 'empty_skill', content: '', path: '' }
const vfs = ctx.vfs
if (!vfs || typeof vfs.readFile !== 'function')
return { ok: false, error: 'vfs unavailable', content: '', path: '' }
const skills = await bareAgentDiscoverSkills(ctx, paths)
const hit =
skills.find((s) => s.id.toLowerCase() === q) ||
skills.find((s) => s.name.toLowerCase() === q)
if (!hit) return { ok: false, error: 'skill_not_found', content: '', path: '' }
try {
const buf = await vfs.readFile(hit.path)
if (!buf || !buf.length)
return { ok: false, error: 'empty_file', content: '', path: hit.path }
const t = bareAgentWorkspaceDecode(ctx, buf)
return { ok: true, skill: hit.name, id: hit.id, path: hit.path, source: hit.source, content: t }
} catch (e) {
const msg = e && typeof e === 'object' && 'message' in e ? String(e.message) : String(e)
return { ok: false, error: msg, content: '', path: hit.path }
}
}