feat: discover and display per-agent workspace skill roots (#426)
Dynamically scan workspace-* directories under the openclaw state dir to discover per-agent skill roots. Display them in the Skills Hub with agent-specific labels and violet badge styling. Closes #412 Supersedes #413
This commit is contained in:
parent
4671946e97
commit
2f2531f3d1
|
|
@ -94,6 +94,22 @@ function getSkillRoots(): SkillRoot[] {
|
||||||
const workspaceSkills = resolveSkillRoot('MC_SKILLS_WORKSPACE_DIR', join(workspaceDir, 'skills'))
|
const workspaceSkills = resolveSkillRoot('MC_SKILLS_WORKSPACE_DIR', join(workspaceDir, 'skills'))
|
||||||
roots.push({ source: 'workspace', path: workspaceSkills })
|
roots.push({ source: 'workspace', path: workspaceSkills })
|
||||||
|
|
||||||
|
// Dynamic: scan for workspace-<agent> directories
|
||||||
|
try {
|
||||||
|
const { readdirSync, existsSync } = require('node:fs') as typeof import('node:fs')
|
||||||
|
const entries = readdirSync(openclawState) as string[]
|
||||||
|
for (const entry of entries) {
|
||||||
|
if (!entry.startsWith('workspace-')) continue
|
||||||
|
const skillsDir = join(openclawState, entry, 'skills')
|
||||||
|
if (existsSync(skillsDir)) {
|
||||||
|
const agentName = entry.replace('workspace-', '')
|
||||||
|
roots.push({ source: `workspace-${agentName}`, path: skillsDir })
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} catch {
|
||||||
|
// openclawBase may not exist
|
||||||
|
}
|
||||||
|
|
||||||
return roots
|
return roots
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -259,6 +275,10 @@ export async function GET(request: NextRequest) {
|
||||||
groupMap.set(root.source, { source: root.source, path: root.path, skills: [] })
|
groupMap.set(root.source, { source: root.source, path: root.path, skills: [] })
|
||||||
}
|
}
|
||||||
for (const skill of dbSkills) {
|
for (const skill of dbSkills) {
|
||||||
|
// Dynamically add workspace-* groups not already in roots
|
||||||
|
if (!groupMap.has(skill.source) && skill.source.startsWith('workspace-')) {
|
||||||
|
groupMap.set(skill.source, { source: skill.source, path: '', skills: [] })
|
||||||
|
}
|
||||||
const group = groupMap.get(skill.source)
|
const group = groupMap.get(skill.source)
|
||||||
if (group) group.skills.push(skill)
|
if (group) group.skills.push(skill)
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -59,6 +59,15 @@ const SOURCE_LABELS: Record<string, string> = {
|
||||||
'workspace': '~/.openclaw/workspace/skills',
|
'workspace': '~/.openclaw/workspace/skills',
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function getSourceLabel(source: string): string {
|
||||||
|
if (SOURCE_LABELS[source]) return SOURCE_LABELS[source]
|
||||||
|
if (source.startsWith('workspace-')) {
|
||||||
|
const agentName = source.replace('workspace-', '')
|
||||||
|
return `${agentName} workspace`
|
||||||
|
}
|
||||||
|
return source
|
||||||
|
}
|
||||||
|
|
||||||
export function SkillsPanel() {
|
export function SkillsPanel() {
|
||||||
const t = useTranslations('skills')
|
const t = useTranslations('skills')
|
||||||
const { dashboardMode, skillsList, skillGroups, skillsTotal, setSkillsData } = useMissionControl()
|
const { dashboardMode, skillsList, skillGroups, skillsTotal, setSkillsData } = useMissionControl()
|
||||||
|
|
@ -552,17 +561,19 @@ export function SkillsPanel() {
|
||||||
{t('showAllRoots')}
|
{t('showAllRoots')}
|
||||||
</button>
|
</button>
|
||||||
)}
|
)}
|
||||||
{(skillGroups || []).filter(g => g.skills.length > 0 || ['user-agents', 'user-codex', 'openclaw', 'workspace'].includes(g.source)).map((group) => (
|
{(skillGroups || []).filter(g => g.skills.length > 0 || ['user-agents', 'user-codex', 'openclaw', 'workspace'].includes(g.source) || g.source.startsWith('workspace-')).map((group) => (
|
||||||
<button
|
<button
|
||||||
key={group.source}
|
key={group.source}
|
||||||
onClick={() => setActiveRoot(activeRoot === group.source ? null : group.source)}
|
onClick={() => setActiveRoot(activeRoot === group.source ? null : group.source)}
|
||||||
className={`rounded-lg border bg-card p-3 text-left transition-colors ${
|
className={`rounded-lg border bg-card p-3 text-left transition-colors ${
|
||||||
activeRoot === group.source
|
activeRoot === group.source
|
||||||
? 'border-primary ring-1 ring-primary/30'
|
? 'border-primary ring-1 ring-primary/30'
|
||||||
: group.source === 'openclaw' ? 'border-cyan-500/30 hover:border-cyan-500/50' : 'border-border hover:border-border/80'
|
: group.source === 'openclaw' ? 'border-cyan-500/30 hover:border-cyan-500/50'
|
||||||
|
: group.source.startsWith('workspace-') ? 'border-violet-500/30 hover:border-violet-500/50'
|
||||||
|
: 'border-border hover:border-border/80'
|
||||||
}`}
|
}`}
|
||||||
>
|
>
|
||||||
<div className="text-xs font-medium text-muted-foreground">{SOURCE_LABELS[group.source] || group.source}</div>
|
<div className="text-xs font-medium text-muted-foreground">{getSourceLabel(group.source)}</div>
|
||||||
<div className="mt-1 text-lg font-semibold text-foreground">{group.skills.length}</div>
|
<div className="mt-1 text-lg font-semibold text-foreground">{group.skills.length}</div>
|
||||||
<div className="mt-1 text-2xs text-muted-foreground truncate">{group.path}</div>
|
<div className="mt-1 text-2xs text-muted-foreground truncate">{group.path}</div>
|
||||||
</button>
|
</button>
|
||||||
|
|
@ -593,11 +604,13 @@ export function SkillsPanel() {
|
||||||
<span className={`text-2xs rounded-full border px-2 py-0.5 ${
|
<span className={`text-2xs rounded-full border px-2 py-0.5 ${
|
||||||
skill.source === 'openclaw'
|
skill.source === 'openclaw'
|
||||||
? 'bg-cyan-500/10 text-cyan-400 border-cyan-500/30'
|
? 'bg-cyan-500/10 text-cyan-400 border-cyan-500/30'
|
||||||
: skill.source.startsWith('project-')
|
: skill.source.startsWith('workspace-')
|
||||||
? 'bg-amber-500/10 text-amber-400 border-amber-500/30'
|
? 'bg-violet-500/10 text-violet-400 border-violet-500/30'
|
||||||
: 'border-border text-muted-foreground'
|
: skill.source.startsWith('project-')
|
||||||
|
? 'bg-amber-500/10 text-amber-400 border-amber-500/30'
|
||||||
|
: 'border-border text-muted-foreground'
|
||||||
}`}>
|
}`}>
|
||||||
{SOURCE_LABELS[skill.source] || skill.source}
|
{getSourceLabel(skill.source)}
|
||||||
</span>
|
</span>
|
||||||
<Button variant="outline" size="xs" onClick={() => checkSecurity(skill)}>
|
<Button variant="outline" size="xs" onClick={() => checkSecurity(skill)}>
|
||||||
{t('scan')}
|
{t('scan')}
|
||||||
|
|
|
||||||
|
|
@ -60,7 +60,7 @@ function getSkillRoots(): Array<{ source: string; path: string }> {
|
||||||
const home = homedir()
|
const home = homedir()
|
||||||
const cwd = process.cwd()
|
const cwd = process.cwd()
|
||||||
const openclawState = process.env.OPENCLAW_STATE_DIR || process.env.OPENCLAW_HOME || join(home, '.openclaw')
|
const openclawState = process.env.OPENCLAW_STATE_DIR || process.env.OPENCLAW_HOME || join(home, '.openclaw')
|
||||||
return [
|
const roots: Array<{ source: string; path: string }> = [
|
||||||
{ source: 'user-agents', path: process.env.MC_SKILLS_USER_AGENTS_DIR || join(home, '.agents', 'skills') },
|
{ source: 'user-agents', path: process.env.MC_SKILLS_USER_AGENTS_DIR || join(home, '.agents', 'skills') },
|
||||||
{ source: 'user-codex', path: process.env.MC_SKILLS_USER_CODEX_DIR || join(home, '.codex', 'skills') },
|
{ source: 'user-codex', path: process.env.MC_SKILLS_USER_CODEX_DIR || join(home, '.codex', 'skills') },
|
||||||
{ source: 'project-agents', path: process.env.MC_SKILLS_PROJECT_AGENTS_DIR || join(cwd, '.agents', 'skills') },
|
{ source: 'project-agents', path: process.env.MC_SKILLS_PROJECT_AGENTS_DIR || join(cwd, '.agents', 'skills') },
|
||||||
|
|
@ -68,6 +68,23 @@ function getSkillRoots(): Array<{ source: string; path: string }> {
|
||||||
{ source: 'openclaw', path: process.env.MC_SKILLS_OPENCLAW_DIR || join(openclawState, 'skills') },
|
{ source: 'openclaw', path: process.env.MC_SKILLS_OPENCLAW_DIR || join(openclawState, 'skills') },
|
||||||
{ source: 'workspace', path: process.env.MC_SKILLS_WORKSPACE_DIR || join(process.env.OPENCLAW_WORKSPACE_DIR || process.env.MISSION_CONTROL_WORKSPACE_DIR || join(openclawState, 'workspace'), 'skills') },
|
{ source: 'workspace', path: process.env.MC_SKILLS_WORKSPACE_DIR || join(process.env.OPENCLAW_WORKSPACE_DIR || process.env.MISSION_CONTROL_WORKSPACE_DIR || join(openclawState, 'workspace'), 'skills') },
|
||||||
]
|
]
|
||||||
|
|
||||||
|
// Dynamic: scan for workspace-<agent> directories
|
||||||
|
try {
|
||||||
|
const entries = readdirSync(openclawState)
|
||||||
|
for (const entry of entries) {
|
||||||
|
if (!entry.startsWith('workspace-')) continue
|
||||||
|
const skillsDir = join(openclawState, entry, 'skills')
|
||||||
|
if (existsSync(skillsDir)) {
|
||||||
|
const agentName = entry.replace('workspace-', '')
|
||||||
|
roots.push({ source: `workspace-${agentName}`, path: skillsDir })
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} catch {
|
||||||
|
// openclawBase may not exist
|
||||||
|
}
|
||||||
|
|
||||||
|
return roots
|
||||||
}
|
}
|
||||||
|
|
||||||
// ---------------------------------------------------------------------------
|
// ---------------------------------------------------------------------------
|
||||||
|
|
@ -128,6 +145,12 @@ export async function syncSkillsFromDisk(): Promise<{ ok: boolean; message: stri
|
||||||
|
|
||||||
// Fetch current DB rows (only local sources, not registry-installed via slug)
|
// Fetch current DB rows (only local sources, not registry-installed via slug)
|
||||||
const localSources = ['user-agents', 'user-codex', 'project-agents', 'project-codex', 'openclaw', 'workspace']
|
const localSources = ['user-agents', 'user-codex', 'project-agents', 'project-codex', 'openclaw', 'workspace']
|
||||||
|
// Also include any dynamic workspace-* sources from disk
|
||||||
|
for (const s of diskSkills) {
|
||||||
|
if (s.source.startsWith('workspace-') && !localSources.includes(s.source)) {
|
||||||
|
localSources.push(s.source)
|
||||||
|
}
|
||||||
|
}
|
||||||
const dbRows = db.prepare(
|
const dbRows = db.prepare(
|
||||||
`SELECT * FROM skills WHERE source IN (${localSources.map(() => '?').join(',')})`
|
`SELECT * FROM skills WHERE source IN (${localSources.map(() => '?').join(',')})`
|
||||||
).all(...localSources) as SkillRow[]
|
).all(...localSources) as SkillRow[]
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue