Orginize
This commit is contained in:
@@ -0,0 +1,189 @@
|
||||
/**
|
||||
* 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 }
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user