Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
211 changes: 211 additions & 0 deletions .github/workflows/auto-update-readme.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,211 @@
name: Auto Update README

on:
push:
branches: [main]
paths:
- 'skills/**/rules/*.md'
- 'skills/**/SKILL.md'

jobs:
update-readme:
runs-on: ubuntu-latest

Comment on lines +10 to +13

Copilot AI Jan 29, 2026

Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The workflow commits back to main, but it doesn’t declare permissions: contents: write. If the repo/org default token permissions are read-only, the auto-commit step will fail. Add an explicit permissions block (workflow- or job-level) granting contents write (and only what’s needed).

Copilot uses AI. Check for mistakes.
steps:
- name: Checkout code
uses: actions/checkout@v6
with:
fetch-depth: 0

- name: Setup Python
uses: actions/setup-python@v5
with:
python-version: '3.11'

- name: Generate and update README
run: |
python3 << 'PYTHON_SCRIPT'
import os
import re
from pathlib import Path
from typing import Dict, List, Tuple

def parse_yaml_frontmatter(file_path: Path) -> Dict[str, str]:
"""Parse YAML frontmatter from a markdown file."""
with open(file_path, 'r', encoding='utf-8') as f:
content = f.read()

# Extract frontmatter between --- markers
match = re.match(r'^---\s*\n(.*?)\n---\s*\n', content, re.DOTALL)
if not match:
return {}

frontmatter = {}
for line in match.group(1).split('\n'):
if ':' in line:
key, value = line.split(':', 1)
frontmatter[key.strip()] = value.strip()

return frontmatter

def get_skill_info(skill_dir: Path) -> Tuple[str, str, List[Dict]]:
"""Get skill name, description and rules from a skill directory."""
skill_md = skill_dir / 'SKILL.md'
if not skill_md.exists():
return '', '', []

with open(skill_md, 'r', encoding='utf-8') as f:
skill_content = f.read()

# Parse skill frontmatter
Comment on lines +57 to +60

Copilot AI Jan 29, 2026

Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

skill_content is read from SKILL.md but never used (same for skill_desc). This extra read makes the script harder to follow; either remove it or use it to drive README content (e.g., description/order from SKILL.md).

Suggested change
with open(skill_md, 'r', encoding='utf-8') as f:
skill_content = f.read()
# Parse skill frontmatter
# Parse skill frontmatter directly from SKILL.md

Copilot uses AI. Check for mistakes.
skill_meta = parse_yaml_frontmatter(skill_md)
skill_name = skill_meta.get('name', skill_dir.name)
skill_desc = skill_meta.get('description', '')

# Get all rule files
rules_dir = skill_dir / 'rules'
if not rules_dir.exists():
return skill_name, skill_desc, []

rules = []
for rule_file in sorted(rules_dir.glob('*.md')):
rule_meta = parse_yaml_frontmatter(rule_file)
if rule_meta:
rules.append({
'file': rule_file.name,
'title': rule_meta.get('title', rule_file.stem),
'type': rule_meta.get('type', 'capability'),
'impact': rule_meta.get('impact', 'MEDIUM'),
'tags': rule_meta.get('tags', ''),
'path': str(rule_file.relative_to(Path.cwd()))
})

Comment on lines +71 to +82

Copilot AI Jan 29, 2026

Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Rules are gathered via sorted(rules_dir.glob('*.md')), so the “Examples” list is effectively based on filename sort order. This will cause unstable/less-meaningful example ordering (and README churn) compared to the curated ordering in each SKILL.md. Prefer deriving ordering (and maybe examples) from the SKILL.md rule tables, or an explicit ordering field in frontmatter.

Suggested change
for rule_file in sorted(rules_dir.glob('*.md')):
rule_meta = parse_yaml_frontmatter(rule_file)
if rule_meta:
rules.append({
'file': rule_file.name,
'title': rule_meta.get('title', rule_file.stem),
'type': rule_meta.get('type', 'capability'),
'impact': rule_meta.get('impact', 'MEDIUM'),
'tags': rule_meta.get('tags', ''),
'path': str(rule_file.relative_to(Path.cwd()))
})
for rule_file in rules_dir.glob('*.md'):
rule_meta = parse_yaml_frontmatter(rule_file)
if rule_meta:
# Optional explicit ordering from rule frontmatter (e.g., "order: 1")
raw_order = rule_meta.get('order')
order_value = None
if raw_order is not None:
try:
order_value = int(str(raw_order).strip())
except (TypeError, ValueError):
order_value = None
rules.append({
'file': rule_file.name,
'title': rule_meta.get('title', rule_file.stem),
'type': rule_meta.get('type', 'capability'),
'impact': rule_meta.get('impact', 'MEDIUM'),
'tags': rule_meta.get('tags', ''),
'path': str(rule_file.relative_to(Path.cwd())),
'order': order_value,
})
# Sort rules: first by explicit numeric 'order' if present, then by title for stability
rules.sort(key=lambda r: (
r['order'] is None,
r['order'] if r['order'] is not None else 0,
r['title'],
))

Copilot uses AI. Check for mistakes.
return skill_name, skill_desc, rules

def format_skill_section(skill_name: str, rules: List[Dict]) -> str:
"""Format a skill section with its rules."""
if not rules:
return ''

# Count rules by type
capability_rules = [r for r in rules if r['type'] == 'capability']
efficiency_rules = [r for r in rules if r['type'] == 'efficiency']

# Map skill names to display names
skill_display_names = {
'nextdns-api': 'NextDNS API',
'nextdns-cli': 'NextDNS CLI',
'nextdns-ui': 'NextDNS UI',
'integrations': 'Integrations'
}

display_name = skill_display_names.get(skill_name, skill_name)
total_count = len(rules)

output = f"### {skill_name} ({total_count} rules)\n\n"

Copilot AI Jan 29, 2026

Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

display_name is computed from skill_display_names but the output header uses skill_name instead, so display_name is dead code. Remove it, or use display_name in the generated markdown if the intent is human-friendly titles.

Suggested change
output = f"### {skill_name} ({total_count} rules)\n\n"
output = f"### {display_name} ({total_count} rules)\n\n"

Copilot uses AI. Check for mistakes.

# Get description based on skill
descriptions = {
'nextdns-api': 'NextDNS API integration best practices covering authentication, profile management, analytics, logs, and real-time streaming.',
'nextdns-cli': 'NextDNS CLI client best practices for installation, configuration, and management of DNS-over-HTTPS proxy.',
'nextdns-ui': 'NextDNS Web UI configuration and management best practices via the web dashboard.',
'integrations': 'NextDNS integration guides for third-party platforms and services including routers, network management, and home automation.'
}

output += descriptions.get(skill_name, '') + "\n\n"

# Create table
output += "| Type | Count | Examples |\n"
output += "|:-----------|:------|:-----------------------------------------------------------------------------------------------------------------------------------------------------------|\n"

# Capability row
if capability_rules:
cap_examples = ', '.join([r['title'] for r in capability_rules[:8]])
if len(capability_rules) > 8:
cap_examples += '...'
output += f"| Capability | {len(capability_rules):<5} | {cap_examples:<154} |\n"
else:
output += f"| Capability | 0 | None |\n"

# Efficiency row
if efficiency_rules:
eff_examples = ', '.join([r['title'] for r in efficiency_rules[:8]])
if len(efficiency_rules) > 8:
eff_examples += '...'
output += f"| Efficiency | {len(efficiency_rules):<5} | {eff_examples:<154} |\n"
else:
output += f"| Efficiency | 0 | Future additions |\n"

output += "\n"
return output

def main():
"""Main function to update README.md."""
repo_root = Path.cwd()
skills_dir = repo_root / 'skills'
readme_path = repo_root / 'README.md'

if not skills_dir.exists() or not readme_path.exists():
print("Error: skills directory or README.md not found")
return

# Get all skill directories
skill_order = ['nextdns-api', 'nextdns-cli', 'nextdns-ui', 'integrations']
skills_data = {}

for skill_dir in sorted(skills_dir.iterdir()):
Comment on lines +152 to +156

Copilot AI Jan 29, 2026

Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

skill_order hardcodes the set/order of skills rendered into README. This means adding a new skill directory won’t show up in the generated section unless this list is updated, which undermines the goal of automated README maintenance. Consider generating order dynamically (e.g., known skills first, then append any other skill dirs alphabetically) or deriving order from directory structure/metadata.

Copilot uses AI. Check for mistakes.
if skill_dir.is_dir():
skill_name, skill_desc, rules = get_skill_info(skill_dir)
if rules:
skills_data[skill_name] = rules

# Generate the skills section
skills_content = ""
for skill_name in skill_order:
if skill_name in skills_data:
skills_content += format_skill_section(skill_name, skills_data[skill_name])

# Read README.md
with open(readme_path, 'r', encoding='utf-8') as f:
readme = f.read()

# Find and replace content between markers
start_marker = "## Available Skills"
end_marker = "## Rule Types"

pattern = re.compile(
f"({re.escape(start_marker)})(.*?)({re.escape(end_marker)})",
re.DOTALL
)

if not pattern.search(readme):
print("Error: Could not find markers in README.md")
print(f"Looking for: '{start_marker}' and '{end_marker}'")
return

new_readme = pattern.sub(
f"\\1\n\n{skills_content}\\3",
readme
)

# Write back to README.md
with open(readme_path, 'w', encoding='utf-8') as f:
f.write(new_readme)

print("✅ README.md updated successfully!")
print(f"Total skills processed: {len(skills_data)}")
for skill_name, rules in skills_data.items():
print(f" - {skill_name}: {len(rules)} rules")

if __name__ == '__main__':
main()
PYTHON_SCRIPT

- name: Commit and push changes
uses: stefanzweifel/git-auto-commit-action@v7
with:
commit_message: 'docs: auto-update README.md with latest rules'
file_pattern: 'README.md'
commit_user_name: 'github-actions[bot]'
commit_user_email: 'github-actions[bot]@users.noreply.github.com'
commit_author: 'github-actions[bot] <github-actions[bot]@users.noreply.github.com>'
129 changes: 122 additions & 7 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -35,23 +35,32 @@ NextDNS API integration best practices covering authentication, profile manageme
| Capability | 15 | Authentication, Profile management, Security settings, Privacy settings, Parental control, Analytics queries, Time series data, Logs streaming, Error handling, Pagination |
| Efficiency | 2 | Response format parsing, Logs download |

### nextdns-cli (10 rules)
### nextdns-cli (14 rules)

NextDNS CLI client best practices for installation, configuration, and management of DNS-over-HTTPS proxy.

| Type | Count | Examples |
|:-----------|:------|:-----------------------------------------------------------------------------------------------------------------------------------------------------------|
| Capability | 8 | Installation, Daemon control, System configuration, Profile configuration, Advanced features, Monitoring, Platform-specific, Troubleshooting |
| Efficiency | 2 | Upgrade/uninstall, Best practices |
| Capability | 11 | Installation, Daemon control, System configuration, Profile configuration, Advanced features, Monitoring, Platform-specific, Troubleshooting |
| Efficiency | 3 | Upgrade/uninstall, Best practices, Docker deployment |

### nextdns-ui (8 rules)
### nextdns-ui (10 rules)

NextDNS Web UI configuration and management best practices via the web dashboard.

| Type | Count | Examples |
|:-----------|:------|:-----------------------------------------------------------------------------------------------------------------------------------------------------------|
| Capability | 6 | Security settings, Privacy settings, Parental control, Denylist/Allowlist, Analytics & Logs, Configuration management |
| Efficiency | 2 | Setup optimization, Troubleshooting via UI |
| Capability | 7 | Security settings, Privacy settings, Parental control, Denylist/Allowlist, DDNS settings, Analytics & Logs, Configuration management |
| Efficiency | 3 | Threat modeling, Setup optimization, Troubleshooting via UI |

### integrations (3 rules)

NextDNS integration guides for third-party platforms and services including routers, network management, and home automation.

| Type | Count | Examples |
|:-----------|:------|:-----------------------------------------------------------------------------------------------------------------------------------------------------------|
| Capability | 3 | DNSMasq integration, Public DNS and AdGuard Home, OpenWrt installation and troubleshooting |
| Efficiency | 0 | Future: Multi-platform deployment, Integration testing |

## Rule Types

Expand Down Expand Up @@ -129,14 +138,33 @@ Rules are classified into two categories:
- **Privacy Settings**: Blocklists and tracking protection
- **Parental Control**: Advanced filtering and schedules
- **Denylist/Allowlist**: Manual domain management
- **DDNS Settings**: Dynamic DNS configuration

#### Monitoring & Management

- **Analytics & Logs**: Identify and analyze network traffic
- **Configuration Management**: Profile settings and performance
- **Threat Modeling**: Security risk assessment and mitigation
- **Setup Optimization**: Best practices for dashboard setup
- **Troubleshooting UI**: Debugging issues using the web interface

### NextDNS Integration Skills

#### Platform Integration

- **DNSMasq Integration**: Configure DNSMasq and NextDNS together while maintaining client reporting
- **Public DNS Setup**: Configure NextDNS public DNS servers on browsers and operating systems
- **AdGuard Home**: Integrate NextDNS with AdGuard Home as upstream DNS provider
- **OpenWrt**: Installation, upgrade, and troubleshooting on OpenWrt routers

#### Supported Platforms

- Routers: OpenWrt, pfSense, Ubiquiti UniFi, DD-WRT
- Network: DNSMasq, AdGuard Home, conditional DNS forwarding
- Automation: Home Assistant, Tailscale
- Containers: Docker, Kubernetes
- NAS: Synology, QNAP

## Quick Examples

### API Examples
Expand Down Expand Up @@ -227,6 +255,52 @@ nextdns cache-stats
nextdns discovered
```

### Integration Examples

#### DNSMasq with NextDNS

```bash
# Configure DNSMasq to use NextDNS on custom port
# In /etc/dnsmasq.conf
server=127.0.0.1#5342
no-resolv

# Configure NextDNS CLI to listen on port 5342
sudo nextdns config set -listen=127.0.0.1:5342
sudo nextdns restart
```

#### OpenWrt Installation

```bash
# SSH into OpenWrt router
ssh root@192.168.1.1

# Install NextDNS
sh -c 'sh -c "$(curl -sL https://nextdns.io/install)"'

# Configure profile ID
nextdns config set -profile=abc123

# Install and start service
nextdns install
/etc/init.d/nextdns start
/etc/init.d/nextdns enable
```

#### AdGuard Home Integration

Configure NextDNS as upstream DNS in AdGuard Home settings:

```text
# Upstream DNS Servers
https://dns.nextdns.io/abc123

# Bootstrap DNS Servers
1.1.1.1
1.0.0.1
```

## Methodology

Every skill in this repository is created through a rigorous, evidence-based process:
Expand Down Expand Up @@ -255,9 +329,50 @@ Skills are tested with:

## Resources

### Official Documentation

- [NextDNS API Documentation](https://nextdns.github.io/api/)
- [NextDNS Account](https://my.nextdns.io/account)
- [NextDNS CLI Wiki](https://github.com/nextdns/nextdns/wiki)
- [NextDNS Help Center](https://help.nextdns.io)
- [NextDNS Account Management](https://my.nextdns.io/account)

### Community Resources

- [NextDNS-Config Community Guidelines](https://github.com/yokoffing/NextDNS-Config)

## Contributing

This repository follows strict quality standards to ensure AI agents can reliably use the skills.

### Development Setup

```bash
# Install dependencies
pnpm install

# Run linting
pnpm lint

# Fix linting issues
pnpm lint:fix
```

### Adding a New Rule

1. Use the template from `templates/rule-template.md`
2. Create a new file in `skills/<skill-name>/rules/` using kebab-case naming
3. Add required YAML frontmatter (title, impact, impactDescription, type, tags)
4. Include the H1 title followed by the bolded Impact Line
5. Add the entry to the skill's capability or efficiency table in `SKILL.md`
6. Run `pnpm lint:fix` to ensure compliance

### Quality Requirements

- **Language**: All content must be in English
- **Indentation**: Use 4-space indentation for lists
- **Filenames**: Use kebab-case for all rule files
- **Case Police**: Follow technical term casing (NextDNS, OpenWrt, macOS, etc.)
- **Code Blocks**: Always specify language for syntax highlighting

## License

Expand Down