Skip to content

Commit 15045fd

Browse files
committed
add plugin structure and tooling
1 parent f1466de commit 15045fd

6 files changed

Lines changed: 318 additions & 0 deletions

File tree

‎.claude-plugin/plugin.json‎

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,6 @@
1+
{
2+
"name": "databricks-skills",
3+
"description": "Databricks skills for CLI, Apps, Unity Catalog, Model Serving, Asset Bundles, and more.",
4+
"version": "0.1.0",
5+
"author": "Databricks"
6+
}
Lines changed: 25 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,25 @@
1+
name: Validate Manifest
2+
3+
on:
4+
pull_request:
5+
paths:
6+
- 'skills/**'
7+
- 'scripts/generate_manifest.py'
8+
- 'manifest.json'
9+
push:
10+
branches:
11+
- main
12+
13+
jobs:
14+
validate:
15+
runs-on: ubuntu-latest
16+
steps:
17+
- uses: actions/checkout@v4
18+
19+
- name: Set up Python
20+
uses: actions/setup-python@v5
21+
with:
22+
python-version: '3.11'
23+
24+
- name: Validate manifest is up to date
25+
run: python3 scripts/generate_manifest.py validate

‎CLAUDE.md‎

Lines changed: 54 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,54 @@
1+
# Databricks Agent Skills
2+
3+
Skills for AI coding assistants (Claude Code, etc.) that provide Databricks-specific guidance.
4+
5+
## Structure
6+
7+
```
8+
skills/
9+
├── databricks/ # core skill: CLI, auth, data exploration
10+
│ ├── SKILL.md
11+
│ └── *.md (references)
12+
└── databricks-apps/ # product skill: app development
13+
├── SKILL.md
14+
└── references/
15+
```
16+
17+
Hierarchy: `databricks` (core) → `databricks-apps` (product) → `databricks-apps-*` (niche)
18+
19+
## Development
20+
21+
### Adding Skills
22+
23+
Create subskills that reference parent:
24+
25+
```markdown
26+
---
27+
name: "databricks-apps-chatbots"
28+
parent: databricks-apps
29+
---
30+
31+
# Chatbot Apps
32+
33+
**FIRST**: Use the parent `databricks-apps` skill for app development basics.
34+
35+
Then apply these patterns:
36+
- Pattern 1
37+
- Pattern 2
38+
```
39+
40+
### Manifest
41+
42+
```bash
43+
python3 scripts/generate_manifest.py # generate
44+
python3 scripts/generate_manifest.py validate # check in CI
45+
```
46+
47+
## Security
48+
49+
When documenting examples, obfuscate sensitive info:
50+
51+
- Workspace IDs: use `1111111111111111` not real IDs
52+
- URLs: use `company-workspace.cloud.databricks.com`
53+
- Never include real tokens, passwords, credentials
54+
- Use placeholders for users, teams, resource IDs

‎README.md‎

Lines changed: 67 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,67 @@
1+
# Databricks Agent Skills
2+
3+
Skills for AI coding assistants (Claude Code, etc.) that provide Databricks-specific guidance.
4+
5+
## Installation
6+
7+
```bash
8+
databricks experimental aitools skills install
9+
```
10+
11+
This installs skills to `~/.claude/skills/` for use with Claude Code.
12+
13+
## Available Skills
14+
15+
- **databricks-apps** - Build full-stack TypeScript apps on Databricks using AppKit
16+
17+
## Structure
18+
19+
Each skill follows the [Agent Skills Specification](https://agentskills.io/specification):
20+
21+
```
22+
skill-name/
23+
├── SKILL.md # Main skill file with frontmatter + instructions
24+
└── references/ # Additional documentation loaded on demand
25+
```
26+
27+
## Development
28+
29+
### Adding New Skills
30+
31+
When experimenting with new skill variations, create a "subskill" that references the main skill and adds specific guidance:
32+
33+
```markdown
34+
---
35+
name: "ai-databricks-apps"
36+
description: "Databricks apps with AI features"
37+
---
38+
39+
# AI powered Databricks Apps
40+
41+
First, load the base databricks-apps skill for foundational guidance.
42+
43+
Then apply these additional patterns:
44+
- Custom pattern 1
45+
- Custom pattern 2
46+
```
47+
48+
This approach:
49+
- Keeps the main skill stable and focused
50+
- Allows experimentation without modifying core skills
51+
- Makes it easy to follow the changes in the main skill
52+
53+
### Manifest Management
54+
55+
Generate manifest after adding/updating skills:
56+
57+
```bash
58+
python3 scripts/generate_manifest.py
59+
```
60+
61+
Validate that manifest is up to date (for CI):
62+
63+
```bash
64+
python3 scripts/generate_manifest.py validate
65+
```
66+
67+
The manifest is used by the CLI to discover available skills.

‎manifest.json‎

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,14 @@
1+
{
2+
"version": "1",
3+
"updated_at": "2026-01-27T14:12:53Z",
4+
"skills": {
5+
"databricks": {
6+
"version": "0.1.0",
7+
"updated_at": "2026-01-27T13:49:12Z"
8+
},
9+
"databricks-apps": {
10+
"version": "0.1.0",
11+
"updated_at": "2026-01-27T13:49:12Z"
12+
}
13+
}
14+
}

‎scripts/generate_manifest.py‎

Lines changed: 152 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,152 @@
1+
#!/usr/bin/env python3
2+
"""Generate or validate manifest.json from skill directories."""
3+
4+
import argparse
5+
import json
6+
import re
7+
import sys
8+
from datetime import datetime, timezone
9+
from pathlib import Path
10+
11+
12+
def extract_version_from_skill(skill_path: Path) -> str:
13+
"""Extract version from SKILL.md frontmatter metadata."""
14+
skill_md = skill_path / "SKILL.md"
15+
if not skill_md.exists():
16+
raise ValueError(f"SKILL.md not found in {skill_path}")
17+
18+
content = skill_md.read_text()
19+
20+
# parse YAML frontmatter
21+
if not content.startswith("---"):
22+
raise ValueError(f"SKILL.md in {skill_path} missing frontmatter")
23+
24+
end_idx = content.find("---", 3)
25+
if end_idx == -1:
26+
raise ValueError(f"SKILL.md in {skill_path} has unclosed frontmatter")
27+
28+
frontmatter = content[3:end_idx]
29+
30+
# extract version from metadata block
31+
version_match = re.search(r'version:\s*["\']?([^"\'\n]+)["\']?', frontmatter)
32+
if version_match:
33+
return version_match.group(1).strip()
34+
35+
return "0.0.0"
36+
37+
38+
def get_skill_updated_at(skill_path: Path) -> str:
39+
"""Get the most recent modification time of any file in the skill directory."""
40+
latest_mtime = 0.0
41+
for file_path in skill_path.rglob("*"):
42+
if file_path.is_file():
43+
mtime = file_path.stat().st_mtime
44+
if mtime > latest_mtime:
45+
latest_mtime = mtime
46+
47+
if latest_mtime == 0.0:
48+
return datetime.now(timezone.utc).strftime("%Y-%m-%dT%H:%M:%SZ")
49+
50+
return datetime.fromtimestamp(latest_mtime, timezone.utc).strftime(
51+
"%Y-%m-%dT%H:%M:%SZ"
52+
)
53+
54+
55+
def generate_manifest(repo_root: Path) -> dict:
56+
"""Generate manifest from skill directories."""
57+
skills = {}
58+
59+
skills_dir = repo_root / "skills"
60+
61+
for item in sorted(skills_dir.iterdir()):
62+
if not item.is_dir():
63+
continue
64+
if item.name.startswith(".") or item.name == "scripts":
65+
continue
66+
67+
skill_md = item / "SKILL.md"
68+
if not skill_md.exists():
69+
continue
70+
71+
skills[item.name] = {
72+
"version": extract_version_from_skill(item),
73+
"updated_at": get_skill_updated_at(item),
74+
}
75+
76+
return {
77+
"version": "1",
78+
"updated_at": datetime.now(timezone.utc).strftime("%Y-%m-%dT%H:%M:%SZ"),
79+
"skills": skills,
80+
}
81+
82+
83+
def normalize_manifest(manifest: dict) -> dict:
84+
"""Normalize manifest for comparison by excluding updated_at timestamps."""
85+
normalized = manifest.copy()
86+
normalized.pop("updated_at", None)
87+
88+
skills = {}
89+
for name, skill in manifest.get("skills", {}).items():
90+
skill_copy = skill.copy()
91+
skill_copy.pop("updated_at", None)
92+
skills[name] = skill_copy
93+
94+
normalized["skills"] = skills
95+
return normalized
96+
97+
98+
def validate_manifest(repo_root: Path) -> bool:
99+
"""Validate that manifest.json is up to date. Returns True if valid."""
100+
manifest_path = repo_root / "manifest.json"
101+
102+
if not manifest_path.exists():
103+
print("ERROR: manifest.json does not exist", file=sys.stderr)
104+
return False
105+
106+
current_manifest = json.loads(manifest_path.read_text())
107+
expected_manifest = generate_manifest(repo_root)
108+
109+
# compare without timestamps
110+
current_normalized = normalize_manifest(current_manifest)
111+
expected_normalized = normalize_manifest(expected_manifest)
112+
113+
if current_normalized != expected_normalized:
114+
print("ERROR: manifest.json is out of date", file=sys.stderr)
115+
print("\nExpected:", file=sys.stderr)
116+
print(json.dumps(expected_normalized, indent=2), file=sys.stderr)
117+
print("\nActual:", file=sys.stderr)
118+
print(json.dumps(current_normalized, indent=2), file=sys.stderr)
119+
return False
120+
121+
print("manifest.json is up to date")
122+
return True
123+
124+
125+
def main() -> None:
126+
parser = argparse.ArgumentParser(description="Generate or validate manifest.json")
127+
parser.add_argument(
128+
"mode",
129+
nargs="?",
130+
default="generate",
131+
choices=["generate", "validate"],
132+
help="Mode: generate (creates manifest.json, default) or validate (checks if up to date)",
133+
)
134+
135+
args = parser.parse_args()
136+
repo_root = Path(__file__).parent.parent
137+
138+
match args.mode:
139+
case "generate":
140+
manifest = generate_manifest(repo_root)
141+
manifest_path = repo_root / "manifest.json"
142+
manifest_path.write_text(json.dumps(manifest, indent=2) + "\n")
143+
print(f"Generated {manifest_path}")
144+
print(f"Found {len(manifest['skills'])} skill(s): {', '.join(manifest['skills'].keys())}")
145+
146+
case "validate":
147+
is_valid = validate_manifest(repo_root)
148+
sys.exit(0 if is_valid else 1)
149+
150+
151+
if __name__ == "__main__":
152+
main()

0 commit comments

Comments
 (0)