mirror of
https://github.com/coder/coder.git
synced 2026-06-02 20:48:20 +00:00
356bccddc2
> Mux updated this PR on behalf of Mike. ## Summary - Add experimental personal skills API helpers and an Agents settings UI for listing, creating, editing, deleting, and importing SKILL.md content. - Add docs, Storybook coverage, and unit tests for backend-compatible SKILL.md parsing. - Address review feedback by simplifying frontmatter scalar parsing, clarifying the UI parser scope, defaulting personal skill queries to `me`, and patching React Query caches after create, update, and delete. - Merge latest `main` and resolve the Agents sidebar refactor conflicts. ## Validation - pre-commit hook - `go test ./codersdk/workspacesdk -run TestParseSkillFrontmatter -count=1` - `go test ./coderd/x/chatd/chattool -run 'Test' -count=1` - `cd site && pnpm test -- src/pages/AgentsPage/utils/personalSkills.test.ts src/api/queries/userSkills.test.ts src/utils/fileSize.test.ts --runInBand` - `cd site && pnpm lint:types` - `cd site && pnpm lint:check`
108 lines
2.8 KiB
Go
108 lines
2.8 KiB
Go
package workspacesdk
|
|
|
|
import (
|
|
"regexp"
|
|
"strings"
|
|
|
|
"golang.org/x/xerrors"
|
|
)
|
|
|
|
// SkillNameRegex is the regular expression used to validate kebab-case skill names.
|
|
const SkillNameRegex = "^[a-z0-9]+(-[a-z0-9]+)*$"
|
|
|
|
// MaxSkillMetaBytes is the maximum raw Markdown size accepted for a skill meta file.
|
|
const MaxSkillMetaBytes = 64 * 1024
|
|
|
|
// SkillNamePattern is the compiled pattern used to validate kebab-case skill names.
|
|
var SkillNamePattern = regexp.MustCompile(SkillNameRegex)
|
|
|
|
// markdownCommentRe strips HTML comments from skill file bodies so
|
|
// they don't leak into the LLM prompt.
|
|
var markdownCommentRe = regexp.MustCompile(`<!--[\s\S]*?-->`)
|
|
|
|
// ErrFrontmatterNameRequired is returned by ParseSkillFrontmatter when
|
|
// the frontmatter is missing a required name field.
|
|
var ErrFrontmatterNameRequired = xerrors.New("frontmatter missing required 'name' field")
|
|
|
|
func unquoteFrontmatterScalar(value string) string {
|
|
if len(value) < 2 {
|
|
return value
|
|
}
|
|
|
|
quote := value[0]
|
|
if quote != value[len(value)-1] {
|
|
return value
|
|
}
|
|
|
|
inner := value[1 : len(value)-1]
|
|
switch quote {
|
|
case '"':
|
|
// This parser supports a small SKILL.md scalar subset, not full
|
|
// YAML. Double quotes only unescape quoted text and Windows paths.
|
|
return strings.NewReplacer(`\"`, `"`, `\\`, `\`).Replace(inner)
|
|
case '\'':
|
|
return inner
|
|
default:
|
|
return value
|
|
}
|
|
}
|
|
|
|
// ParseSkillFrontmatter extracts name, description, and the
|
|
// remaining body from a skill meta file. The expected format is
|
|
// YAML-ish frontmatter delimited by "---" lines:
|
|
//
|
|
// ---
|
|
// name: my-skill
|
|
// description: Does a thing
|
|
// ---
|
|
// Body text here...
|
|
func ParseSkillFrontmatter(content string) (name, description, body string, err error) {
|
|
content = strings.TrimPrefix(content, "\xef\xbb\xbf")
|
|
lines := strings.Split(content, "\n")
|
|
if len(lines) == 0 || strings.TrimSpace(lines[0]) != "---" {
|
|
return "", "", "", xerrors.New(
|
|
"missing opening frontmatter delimiter",
|
|
)
|
|
}
|
|
|
|
closingIdx := -1
|
|
for i := 1; i < len(lines); i++ {
|
|
if strings.TrimSpace(lines[i]) == "---" {
|
|
closingIdx = i
|
|
break
|
|
}
|
|
}
|
|
if closingIdx < 0 {
|
|
return "", "", "", xerrors.New(
|
|
"missing closing frontmatter delimiter",
|
|
)
|
|
}
|
|
|
|
for _, line := range lines[1:closingIdx] {
|
|
key, value, ok := strings.Cut(line, ":")
|
|
if !ok {
|
|
continue
|
|
}
|
|
key = strings.TrimSpace(key)
|
|
value = strings.TrimSpace(value)
|
|
value = unquoteFrontmatterScalar(value)
|
|
switch strings.ToLower(key) {
|
|
case "name":
|
|
name = value
|
|
case "description":
|
|
description = value
|
|
}
|
|
}
|
|
|
|
if name == "" {
|
|
return "", "", "", xerrors.Errorf("%w", ErrFrontmatterNameRequired)
|
|
}
|
|
|
|
// Everything after the closing delimiter is the body.
|
|
body = strings.Join(lines[closingIdx+1:], "\n")
|
|
body = markdownCommentRe.ReplaceAllString(body, "")
|
|
body = strings.TrimSpace(body)
|
|
|
|
return name, description, body, nil
|
|
}
|