Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
23 commits
Select commit Hold shift + click to select a range
3b61d67
add gifs and assets to the README
SterlingChin Feb 20, 2026
d3eb01c
update alt text for gifs
SterlingChin Feb 20, 2026
964c825
chore: add LICENSE, cross-links, and repo topics
SterlingChin Mar 9, 2026
f34c871
Search Strategy update to utilise private network search (#1)
chirag-yaduwanshi Mar 26, 2026
6c910be
add postman cli workflow skills
nnandan-postman Apr 9, 2026
5ddc58e
Add checks
nnandan-postman Apr 9, 2026
ce2da6b
Merge pull request #3 from Postman-Devrel/task/add_postman_cli_skills
quintonwall Apr 9, 2026
4fa5ac0
fix postman API key links
yokawasa Apr 2, 2026
79ab82d
fix postman API key links
yokawasa Apr 2, 2026
51b037d
Re-run CI
yokawasa Apr 10, 2026
120f0e8
fix: use pull_request_target for labeler to support fork PRs
yokawasa Apr 10, 2026
a0fe4d8
Merge pull request #4 from yokawasa/use-pull_request_target
nnandan-postman Apr 11, 2026
c583c5b
Merge branch 'main' into fix-api-keys-link
yokawasa Apr 11, 2026
f2c0319
Merge pull request #2 from yokawasa/fix-api-keys-link
nnandan-postman Apr 16, 2026
eb6d25d
added support for OAuth.
quintonwall Apr 29, 2026
5c45bad
removed redundant tooling
quintonwall Apr 29, 2026
bce21f2
Merge pull request #5 from Postman-Devrel/oauth-support
quintonwall Apr 29, 2026
ecf1726
added unique headers and bumped version
quintonwall Apr 29, 2026
65cd9a2
Add Postman Context to skills
jiaxpostman May 8, 2026
ee0f0ee
Merge pull request #7 from Postman-Devrel/feature/add-postman-context
quintonwall May 11, 2026
56268ba
Merge branch 'main' into oauth-support
quintonwall May 11, 2026
56fe054
Merge pull request #6 from Postman-Devrel/oauth-support
quintonwall May 11, 2026
6b9faa7
Shrink repo by removing GIFs and hosting videos on GitHub
jonico Jun 4, 2026
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
2 changes: 1 addition & 1 deletion .claude-plugin/plugin.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{
"name": "postman",
"version": "1.0.0",
"version": "1.1.0",
"description": "Full API lifecycle management for Claude Code. Sync collections, generate client code, discover APIs, run tests, create mocks, publish docs, and audit security. Powered by the Postman MCP Server.",
"author": {
"name": "Postman",
Expand Down
14 changes: 14 additions & 0 deletions .github/.markdownlint.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
{
"MD009": false,
"MD013": false,
"MD022": false,
"MD026": false,
"MD029": false,
"MD031": false,
"MD032": false,
"MD033": false,
"MD034": false,
"MD040": false,
"MD041": false,
"MD060": false
}
29 changes: 29 additions & 0 deletions .github/labeler.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
commands:
- changed-files:
- any-glob-to-any-file: "commands/**"

skills:
- changed-files:
- any-glob-to-any-file: "skills/**"

agents:
- changed-files:
- any-glob-to-any-file: "agents/**"

docs:
- changed-files:
- any-glob-to-any-file:
- "README.md"
- "CLAUDE.md"
- "examples/**"

config:
- changed-files:
- any-glob-to-any-file:
- ".claude-plugin/**"
- ".mcp.json"
- ".github/**"

examples:
- changed-files:
- any-glob-to-any-file: "examples/**"
63 changes: 63 additions & 0 deletions .github/scripts/check-internal-links.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,63 @@
#!/usr/bin/env python3
"""Check that internal markdown links resolve to existing files."""

import re
import sys
from pathlib import Path

# Matches [text](path) and ![alt](path) — excludes URLs
LINK_RE = re.compile(r"!?\[([^\]]*)\]\(([^)]+)\)")


def check_file_links(file_path: Path, root: Path) -> list[str]:
errors = []
text = file_path.read_text()
file_dir = file_path.parent

for match in LINK_RE.finditer(text):
target = match.group(2)

# Skip external URLs
if target.startswith(("http://", "https://", "mailto:")):
continue

# Skip anchor-only links
if target.startswith("#"):
continue

# Strip anchor fragments from file paths
target_path = target.split("#")[0]
if not target_path:
continue

# Resolve relative to the file's directory
resolved = (file_dir / target_path).resolve()
if not resolved.exists():
rel = file_path.relative_to(root)
errors.append(f"{rel}: Broken link '{target}' — file not found")

return errors


def main():
root = Path(__file__).resolve().parent.parent.parent
errors = []

for md_file in sorted(root.rglob("*.md")):
# Skip .git and node_modules
parts = md_file.relative_to(root).parts
if any(p.startswith(".git") or p == "node_modules" for p in parts):
continue
errors.extend(check_file_links(md_file, root))

if errors:
print("Link check failed:")
for e in errors:
print(f" ✗ {e}")
sys.exit(1)
else:
print("✓ Link check passed")


if __name__ == "__main__":
main()
125 changes: 125 additions & 0 deletions .github/scripts/validate-frontmatter.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,125 @@
#!/usr/bin/env python3
"""Validate YAML frontmatter in commands, skills, and agents."""

import re
import sys
from pathlib import Path
from typing import Optional

# PyYAML is not guaranteed on all runners, so parse simple YAML manually
FRONTMATTER_RE = re.compile(r"^---\s*\n(.*?)\n---", re.DOTALL)

KNOWN_TOOLS = {"Bash", "Read", "Write", "Glob", "Grep", "mcp__postman__*"}


def parse_frontmatter(text: str) -> Optional[dict]:
match = FRONTMATTER_RE.match(text)
if not match:
return None
result = {}
for line in match.group(1).splitlines():
line = line.strip()
if not line or line.startswith("#"):
continue
if ":" in line:
key, _, value = line.partition(":")
key = key.strip()
value = value.strip().strip('"').strip("'")
result[key] = value
return result


def validate_commands(root: Path) -> list[str]:
errors = []
commands_dir = root / "commands"
if not commands_dir.is_dir():
return [f"{commands_dir}: Directory not found"]

for f in sorted(commands_dir.glob("*.md")):
text = f.read_text()
fm = parse_frontmatter(text)
if fm is None:
errors.append(f"{f.name}: Missing YAML frontmatter")
continue
if "description" not in fm or not fm["description"]:
errors.append(f"{f.name}: Missing required field 'description'")
if "allowed-tools" in fm and fm["allowed-tools"]:
tools = [t.strip() for t in fm["allowed-tools"].split(",")]
for tool in tools:
if tool not in KNOWN_TOOLS:
errors.append(f"{f.name}: Unknown tool '{tool}' in allowed-tools")

return errors


def validate_skills(root: Path) -> list[str]:
errors = []
skills_dir = root / "skills"
if not skills_dir.is_dir():
return [f"{skills_dir}: Directory not found"]

for skill_dir in sorted(skills_dir.iterdir()):
if not skill_dir.is_dir():
continue
skill_file = skill_dir / "SKILL.md"
if not skill_file.exists():
errors.append(f"skills/{skill_dir.name}/: Missing SKILL.md")
continue
text = skill_file.read_text()
fm = parse_frontmatter(text)
if fm is None:
errors.append(f"skills/{skill_dir.name}/SKILL.md: Missing YAML frontmatter")
continue
if "name" not in fm or not fm["name"]:
errors.append(f"skills/{skill_dir.name}/SKILL.md: Missing required field 'name'")
if "description" not in fm or not fm["description"]:
errors.append(f"skills/{skill_dir.name}/SKILL.md: Missing required field 'description'")

return errors


def validate_agents(root: Path) -> list[str]:
errors = []
agents_dir = root / "agents"
if not agents_dir.is_dir():
return [f"{agents_dir}: Directory not found"]

required_fields = ["name", "description", "model", "allowed-tools"]

for f in sorted(agents_dir.glob("*.md")):
text = f.read_text()
fm = parse_frontmatter(text)
if fm is None:
errors.append(f"agents/{f.name}: Missing YAML frontmatter")
continue
for field in required_fields:
if field not in fm or not fm[field]:
errors.append(f"agents/{f.name}: Missing required field '{field}'")
if "allowed-tools" in fm and fm["allowed-tools"]:
tools = [t.strip() for t in fm["allowed-tools"].split(",")]
for tool in tools:
if tool not in KNOWN_TOOLS:
errors.append(f"agents/{f.name}: Unknown tool '{tool}' in allowed-tools")

return errors


def main():
root = Path(__file__).resolve().parent.parent.parent
errors = []

errors.extend(validate_commands(root))
errors.extend(validate_skills(root))
errors.extend(validate_agents(root))

if errors:
print("Frontmatter validation failed:")
for e in errors:
print(f" ✗ {e}")
sys.exit(1)
else:
print("✓ Frontmatter validation passed")


if __name__ == "__main__":
main()
80 changes: 80 additions & 0 deletions .github/scripts/validate-json.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,80 @@
#!/usr/bin/env python3
"""Validate JSON config files for the Claude Code plugin."""

import json
import sys
from pathlib import Path


def validate_plugin_json(path: Path) -> list[str]:
errors = []
try:
with open(path) as f:
data = json.load(f)
except json.JSONDecodeError as e:
return [f"{path}: Invalid JSON — {e}"]

required_fields = ["name", "version", "description"]
for field in required_fields:
if field not in data:
errors.append(f"{path}: Missing required field '{field}'")
elif not isinstance(data[field], str) or not data[field].strip():
errors.append(f"{path}: Field '{field}' must be a non-empty string")

if "version" in data and isinstance(data["version"], str):
parts = data["version"].split(".")
if len(parts) != 3 or not all(p.isdigit() for p in parts):
errors.append(f"{path}: Field 'version' must be semver (e.g. 1.0.0)")

return errors


def validate_mcp_json(path: Path) -> list[str]:
errors = []
try:
with open(path) as f:
data = json.load(f)
except json.JSONDecodeError as e:
return [f"{path}: Invalid JSON — {e}"]

if "mcpServers" not in data:
errors.append(f"{path}: Missing required key 'mcpServers'")
elif not isinstance(data["mcpServers"], dict):
errors.append(f"{path}: 'mcpServers' must be an object")
else:
for name, config in data["mcpServers"].items():
if "type" not in config:
errors.append(f"{path}: Server '{name}' missing 'type'")
if "url" not in config:
errors.append(f"{path}: Server '{name}' missing 'url'")

return errors


def main():
root = Path(__file__).resolve().parent.parent.parent
errors = []

plugin_json = root / ".claude-plugin" / "plugin.json"
if plugin_json.exists():
errors.extend(validate_plugin_json(plugin_json))
else:
errors.append(f"{plugin_json}: File not found")

mcp_json = root / ".mcp.json"
if mcp_json.exists():
errors.extend(validate_mcp_json(mcp_json))
else:
errors.append(f"{mcp_json}: File not found")

if errors:
print("JSON validation failed:")
for e in errors:
print(f" ✗ {e}")
sys.exit(1)
else:
print("✓ JSON validation passed")


if __name__ == "__main__":
main()
Loading
Loading