Skip to content

Commit e1dd7a2

Browse files
authored
New Plugin: Agent Skills Editing (#230)
1 parent 3bf67a0 commit e1dd7a2

24 files changed

Lines changed: 1610 additions & 0 deletions
Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,14 @@
1+
# Agent Skills Editing Changelog
2+
3+
<!-- Keep a changelog entry for the current unreleased version -->
4+
5+
## Unreleased
6+
7+
### Added
8+
9+
- Inline rename for skill names (Shift+F6 on `name` field)
10+
- Auto-fix for name/directory mismatch (`Set name to...`, `Rename directory to...`, `Rename both to...`)
11+
- Manual rename quickfix that triggers inline rename
12+
- Validation of consecutive hyphens in skill names
13+
- Go to Declaration from `name` to parent skill directory
14+
- Update from skill directory to `SKILL.md` on rename
Lines changed: 31 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,31 @@
1+
# Agent Skills Editing
2+
3+
<!-- Plugin description -->
4+
Provides editing support for Codex SKILL.md files:
5+
6+
- **JSON Schema injection** — Autocomplete and validation for SKILL.md YAML front-matter
7+
- **Metadata inspection** — Validates required fields (`name`, `description`)
8+
- **Naming convention checks** — Enforces kebab-case for skill names
9+
- **Name consistency** — Ensures skill name matches the parent directory name
10+
- **QuickFixes** — One-click fixes for naming issues
11+
<!-- Plugin description end -->
12+
13+
## Features
14+
15+
| Feature | Description |
16+
|-------------|---------------------------------------------------------------------------|
17+
| JSON Schema | Provides schema for `name` (kebab-case pattern) and `description` |
18+
| Inspection | Validates front-matter structure, required fields, and naming conventions |
19+
| QuickFix | Auto-convert names to kebab-case or match parent directory |
20+
21+
## SKILL.md Format
22+
23+
```yaml
24+
---
25+
name: my-skill-name
26+
description: A brief description of the skill's purpose
27+
---
28+
29+
# Skill Title
30+
... documentation body ...
31+
```

plugins/AgentSkillsEditing/TODO.md

Lines changed: 35 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,35 @@
1+
# Agent Skills Editing — TODO
2+
3+
## v0.1.0 (initial)
4+
5+
- [x] Plugin skeleton with `build.gradle.kts` and `plugin.xml`
6+
- [x] JSON Schema for SKILL.md front-matter (`skill-md-schema.json`)
7+
- [x] `JsonSchemaProviderFactory` registration via `JavaScript.JsonSchema.ProviderFactory`
8+
- [x] `SkillMdInspection` — validates YAML front-matter, required fields, kebab-case, name consistency
9+
- [x] `FixSkillNameQuickFix` — converts arbitrary names to kebab-case
10+
- [x] `MatchDirectoryNameQuickFix` — sets name to parent directory name
11+
12+
## v0.2.0
13+
14+
- [x] Add `docs/` folder with detailed design notes after stabilization
15+
- [x] Test fixtures with real `SKILL.md` samples
16+
- [x] Integration test via `BasePlatformTestCase`
17+
18+
## v1.0.0 — Inline Rename + Unified Inspection
19+
20+
- [x] `SkillNameInspection` unified fix decision system (one state calc → three problem types → filtered fix candidates)
21+
- [x] QuickFix priority via `PriorityAction` (AutoSetName=TOP, AutoRenameDir=HIGH, AutoRenameBoth=NORMAL, ManualRename=LOW)
22+
- [x] `SkillNameInlineElement` PsiNamedElement delegate for inline rename
23+
- [x] `SkillNameInlineRenamer` custom VariableInplaceRenamer subclass
24+
- [x] Shared `performSkillNameInlineRename()` utility (handler + quickfix)
25+
- [x] `SkillNameRenameHandler` uses inline rename instead of dialog
26+
- [x] `NameQuality` / `NamePart` / `analyzeSkillName()` name analysis model
27+
- [x] `normalizeSkillNameOrNull()` safe normalization
28+
- [x] `SKILL_NAME_REGEX` disallows consecutive hyphens
29+
- [x] `GotoDeclarationHandler` for scalar → directory navigation
30+
- [x] `SkillDirRenameSearchExecutor` + `SkillDirectoryNameReference` for directory→YAML sync
31+
- [x] `SkillNameRenamePsiElementProcessor` for scalar→directory rename forwarding
32+
- [x] `SkillMdPsiUtil` extraction of shared PSI helpers
33+
- [x] Real fixture tests with `mcp-sdk` skill samples
34+
- [x] Remove old `psi.referenceContributor` (forward PsiReference) from plugin.xml
35+
- [x] Bundle-localized quickfix names with `quickfix.auto.*` / `quickfix.manual.*` keys

plugins/AgentSkillsEditing/api/AgentSkillsEditing.api

Whitespace-only changes.
Lines changed: 33 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,33 @@
1+
/*
2+
* Copyright (c) 2026 ghostflyby
3+
* SPDX-FileCopyrightText: 2026 ghostflyby
4+
* SPDX-License-Identifier: LGPL-3.0-or-later
5+
*
6+
* This file is part of IntelliJ-Plugins by ghostflyby
7+
*
8+
* IntelliJ-Plugins by ghostflyby is free software; you can redistribute it and/or
9+
* modify it under the terms of the GNU Lesser General Public
10+
* License as published by the Free Software Foundation; either
11+
* version 3.0 of the License, or (at your option) any later version.
12+
*
13+
* This program is distributed in the hope that it will be useful,
14+
* but WITHOUT ANY WARRANTY; without even the implied warranty of
15+
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
16+
* Lesser General Public License for more details.
17+
*
18+
* You should have received a copy of the GNU Lesser General Public
19+
* License along with this library; if not, see
20+
* <https://www.gnu.org/licenses/>.
21+
*/
22+
23+
plugins {
24+
id("repo.intellij-plugin")
25+
}
26+
27+
version = "1.0.0"
28+
29+
dependencies.intellijPlatform {
30+
bundledPlugin("com.intellij.modules.json")
31+
bundledPlugin("org.intellij.plugins.markdown")
32+
bundledPlugin("org.jetbrains.plugins.yaml")
33+
}
Lines changed: 39 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,39 @@
1+
# Skill Name Inline Rename
2+
3+
## Architecture
4+
5+
The skill name rename system has three entry points, all converging on the same inline rename:
6+
7+
1. **Rename shortcut** (Shift+F6) -> `SkillNameRenameHandler` -> `performSkillNameInlineRename()`
8+
2. **Inspection manual fix** -> `ManualRenameQuickFix` -> `performSkillNameInlineRename()`
9+
3. **Directory rename** -> `SkillDirRenameSearchExecutor` + `SkillDirectoryNameReference` -> updates YAML name
10+
11+
### Key components
12+
13+
- `SkillNameInlineElement` - `PsiNamedElement` wrapper for a `YAMLScalar`, provides host-file-relative text range
14+
- `SkillNameInlineReference` - precise `PsiReference` covering only the scalar value range
15+
- `SkillNameInlineRenamer` - extends `VariableInplaceRenamer`, handles template building and directory sync
16+
- `performSkillNameInlineRename()` - shared utility that positions caret and starts inline rename
17+
- `SkillNameRenamePsiElementProcessor` - converts scalar rename into directory rename
18+
19+
## Inspection Fix Decision System
20+
21+
`SkillNameInspection` is a single `LocalInspectionTool` that:
22+
1. Analyzes both name and directory once into `NamePart` (VALID / NORMALIZABLE / INVALID)
23+
2. Computes `SkillNameState`
24+
3. Registers up to 3 problem types: `INVALID_NAME`, `INVALID_DIRECTORY`, `MISMATCH`
25+
4. Each problem provides filtered, deduplicated fix candidates sorted by `PriorityAction`
26+
27+
QuickFix priorities:
28+
- `TOP` - AutoSetName (change YAML value)
29+
- `HIGH` - AutoRenameDir (VFS rename directory)
30+
- `NORMAL` - AutoRenameBoth (change both)
31+
- `LOW` - ManualRename (inline rename)
32+
33+
## References
34+
35+
- `SkillNameRenameHandler.kt` - rename handler entry point
36+
- `SkillNameInlineRename.kt` - inline rename infrastructure
37+
- `SkillNameInspection.kt` - unified inspection with fix decision system
38+
- `SkillMdPsiUtil.kt` - shared PSI utilities and name analysis
39+
- `plugin.xml` - extension registrations
Lines changed: 69 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,69 @@
1+
/*
2+
* Copyright (c) 2026 ghostflyby
3+
* SPDX-FileCopyrightText: 2026 ghostflyby
4+
* SPDX-License-Identifier: LGPL-3.0-or-later
5+
*
6+
* This file is part of IntelliJ-Plugins by ghostflyby
7+
*
8+
* IntelliJ-Plugins by ghostflyby is free software; you can redistribute it and/or
9+
* modify it under the terms of the GNU Lesser General Public
10+
* License as published by the Free Software Foundation; either
11+
* version 3.0 of the License, or (at your option) any later version.
12+
*
13+
* This program is distributed in the hope that it will be useful,
14+
* but WITHOUT ANY WARRANTY; without even the implied warranty of
15+
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
16+
* Lesser General Public License for more details.
17+
*
18+
* You should have received a copy of the GNU Lesser General Public
19+
* License along with this library; if not, see
20+
* <https://www.gnu.org/licenses/>.
21+
*/
22+
23+
package dev.ghostflyby.skills
24+
25+
import com.intellij.openapi.application.QueryExecutorBase
26+
import com.intellij.psi.PsiDirectory
27+
import com.intellij.psi.PsiElement
28+
import com.intellij.psi.PsiReference
29+
import com.intellij.psi.PsiReferenceBase
30+
import com.intellij.psi.search.searches.ReferencesSearch
31+
import com.intellij.psi.util.parentOfType
32+
import com.intellij.util.Processor
33+
import org.jetbrains.yaml.YAMLElementGenerator
34+
import org.jetbrains.yaml.psi.YAMLKeyValue
35+
import org.jetbrains.yaml.psi.YAMLScalar
36+
37+
internal class SkillDirRenameSearchExecutor : QueryExecutorBase<PsiReference, ReferencesSearch.SearchParameters>() {
38+
39+
override fun processQuery(
40+
queryParameters: ReferencesSearch.SearchParameters,
41+
consumer: Processor<in PsiReference>,
42+
) {
43+
val element = queryParameters.elementToSearch
44+
val dir = element as? PsiDirectory ?: return
45+
val skillFile = dir.skillMarkdownFile ?: return
46+
val scalar = skillFile.skillNameScalar() ?: return
47+
val reference = SkillDirectoryNameReference(scalar, dir)
48+
if (reference.isReferenceTo(dir)) {
49+
consumer.process(reference)
50+
}
51+
}
52+
}
53+
54+
internal class SkillDirectoryNameReference(
55+
element: YAMLScalar,
56+
private val directory: PsiDirectory,
57+
) : PsiReferenceBase<YAMLScalar>(element, false) {
58+
override fun resolve(): PsiElement = directory
59+
60+
override fun handleElementRename(newElementName: String): PsiElement? {
61+
val scalar = directory.skillMarkdownFile?.skillNameScalar() ?: return null
62+
val gen = YAMLElementGenerator.getInstance(directory.project)
63+
val kv = scalar.parentOfType<YAMLKeyValue>() ?: return null
64+
val newKv = gen.createYamlKeyValue(kv.keyText, newElementName)
65+
val newValue = newKv.value ?: return null
66+
kv.setValue(newValue)
67+
return kv.value
68+
}
69+
}
Lines changed: 39 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,39 @@
1+
/*
2+
* Copyright (c) 2026 ghostflyby
3+
* SPDX-FileCopyrightText: 2026 ghostflyby
4+
* SPDX-License-Identifier: LGPL-3.0-or-later
5+
*
6+
* This file is part of IntelliJ-Plugins by ghostflyby
7+
*
8+
* IntelliJ-Plugins by ghostflyby is free software; you can redistribute it and/or
9+
* modify it under the terms of the GNU Lesser General Public
10+
* License as published by the Free Software Foundation; either
11+
* version 3.0 of the License, or (at your option) any later version.
12+
*
13+
* This program is distributed in the hope that it will be useful,
14+
* but WITHOUT ANY WARRANTY; without even the implied warranty of
15+
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
16+
* Lesser General Public License for more details.
17+
*
18+
* You should have received a copy of the GNU Lesser General Public
19+
* License along with this library; if not, see
20+
* <https://www.gnu.org/licenses/>.
21+
*/
22+
23+
package dev.ghostflyby.skills
24+
25+
import com.intellij.DynamicBundle
26+
import org.jetbrains.annotations.Nls
27+
import org.jetbrains.annotations.NonNls
28+
import org.jetbrains.annotations.PropertyKey
29+
30+
@NonNls
31+
private const val BUNDLE = "messages.SkillMdBundle"
32+
33+
internal object SkillMdBundle {
34+
private val bundle = DynamicBundle(SkillMdBundle::class.java, BUNDLE)
35+
36+
@Nls
37+
fun message(@PropertyKey(resourceBundle = BUNDLE) key: String, vararg params: Any): String =
38+
bundle.getMessage(key, *params)
39+
}
Lines changed: 72 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,72 @@
1+
/*
2+
* Copyright (c) 2026 ghostflyby
3+
* SPDX-FileCopyrightText: 2026 ghostflyby
4+
* SPDX-License-Identifier: LGPL-3.0-or-later
5+
*
6+
* This file is part of IntelliJ-Plugins by ghostflyby
7+
*
8+
* IntelliJ-Plugins by ghostflyby is free software; you can redistribute it and/or
9+
* modify it under the terms of the GNU Lesser General Public
10+
* License as published by the Free Software Foundation; either
11+
* version 3.0 of the License, or (at your option) any later version.
12+
*
13+
* This program is distributed in the hope that it will be useful,
14+
* but WITHOUT ANY WARRANTY; without even the implied warranty of
15+
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
16+
* Lesser General Public License for more details.
17+
*
18+
* You should have received a copy of the GNU Lesser General Public
19+
* License along with this library; if not, see
20+
* <https://www.gnu.org/licenses/>.
21+
*/
22+
23+
package dev.ghostflyby.skills
24+
25+
import com.intellij.codeInspection.LocalInspectionTool
26+
import com.intellij.codeInspection.LocalQuickFix
27+
import com.intellij.codeInspection.ProblemDescriptor
28+
import com.intellij.codeInspection.ProblemsHolder
29+
import com.intellij.openapi.project.Project
30+
import com.intellij.psi.PsiElementVisitor
31+
import com.intellij.psi.PsiFile
32+
33+
/**
34+
* Checks that SKILL.md has a YAML frontmatter block (--- delimiters).
35+
* Schema can't express "must have delimiters", so this is a pure inspection.
36+
*/
37+
internal class SkillMdFrontmatterInspection : LocalInspectionTool() {
38+
39+
override fun buildVisitor(holder: ProblemsHolder, isOnTheFly: Boolean): PsiElementVisitor {
40+
return object : PsiElementVisitor() {
41+
override fun visitFile(file: PsiFile) {
42+
if (!file.isSkillMarkdownFile) return
43+
if (!file.hasSkillMdFrontmatter) {
44+
holder.registerProblem(
45+
file, file.textRange,
46+
SkillMdBundle.message("frontmatter.missing"),
47+
AddFrontmatterQuickFix(),
48+
)
49+
}
50+
}
51+
}
52+
}
53+
}
54+
55+
internal class AddFrontmatterQuickFix : LocalQuickFix {
56+
override fun getName(): String = SkillMdBundle.message("quickfix.add.frontmatter")
57+
override fun getFamilyName(): String = SkillMdBundle.message("quickfix.family.add.frontmatter")
58+
override fun applyFix(
59+
project: Project,
60+
descriptor: ProblemDescriptor,
61+
) {
62+
val psiFile = descriptor.psiElement.containingFile ?: return
63+
val document = psiFile.viewProvider.document ?: return
64+
val frontmatter = """
65+
|---
66+
|
67+
|---
68+
|
69+
|""".trimMargin()
70+
document.insertString(0, frontmatter)
71+
}
72+
}

0 commit comments

Comments
 (0)