From 458e727c660502ff5e61e6ee2a2f9c3c885d048c Mon Sep 17 00:00:00 2001 From: jeremy-london Date: Tue, 31 Mar 2026 09:57:01 -0600 Subject: [PATCH] feat: adds updated support for claude and cursor marketplace --- .claude-plugin/marketplace.json | 91 +-- .claude-plugin/plugin.json | 30 - .cursor-plugin/marketplace.json | 81 +++ .github/ACT.md | 28 +- .github/pull_request_template.md | 4 +- .github/workflows/test-skills.yml | 253 -------- .github/workflows/validate-plugin.yml | 359 ----------- .pre-commit-config.yaml | 11 +- .release-please-manifest.json | 2 +- CHANGELOG.md | 21 +- CONTRIBUTING.md | 12 +- README.md | 45 +- Taskfile.yaml | 5 + .../keeper-admin/.claude-plugin/plugin.json | 22 + .../keeper-admin/.cursor-plugin/plugin.json | 23 + plugins/keeper-admin/assets/logo.png | Bin 0 -> 24935 bytes .../skills}/keeper-admin/SKILL.md | 0 .../references/commander-commands.md | 0 .../references/enterprise-mgmt.md | 0 .../keeper-admin/references/pam-commands.md | 0 .../references/rotation-commands.md | 0 .../keeper-secrets/.claude-plugin/plugin.json | 24 + .../keeper-secrets/.cursor-plugin/plugin.json | 25 + plugins/keeper-secrets/assets/logo.png | Bin 0 -> 24935 bytes .../skills}/keeper-secrets/SKILL.md | 0 .../references/keeper-notation.md | 0 .../keeper-secrets/references/ksm-commands.md | 0 .../references/ksm-exec-patterns.md | 0 .../keeper-setup/.claude-plugin/plugin.json | 22 + .../keeper-setup/.cursor-plugin/plugin.json | 23 + plugins/keeper-setup/assets/logo.png | Bin 0 -> 24935 bytes .../skills}/keeper-setup/SKILL.md | 0 release-please-config.json | 34 +- scripts/act-all.sh | 12 +- scripts/validate-plugins.sh | 574 ++++++++++++++++++ version.txt | 2 +- 36 files changed, 931 insertions(+), 772 deletions(-) delete mode 100644 .claude-plugin/plugin.json create mode 100644 .cursor-plugin/marketplace.json delete mode 100644 .github/workflows/test-skills.yml delete mode 100644 .github/workflows/validate-plugin.yml create mode 100644 plugins/keeper-admin/.claude-plugin/plugin.json create mode 100644 plugins/keeper-admin/.cursor-plugin/plugin.json create mode 100644 plugins/keeper-admin/assets/logo.png rename {skills => plugins/keeper-admin/skills}/keeper-admin/SKILL.md (100%) rename {skills => plugins/keeper-admin/skills}/keeper-admin/references/commander-commands.md (100%) rename {skills => plugins/keeper-admin/skills}/keeper-admin/references/enterprise-mgmt.md (100%) rename {skills => plugins/keeper-admin/skills}/keeper-admin/references/pam-commands.md (100%) rename {skills => plugins/keeper-admin/skills}/keeper-admin/references/rotation-commands.md (100%) create mode 100644 plugins/keeper-secrets/.claude-plugin/plugin.json create mode 100644 plugins/keeper-secrets/.cursor-plugin/plugin.json create mode 100644 plugins/keeper-secrets/assets/logo.png rename {skills => plugins/keeper-secrets/skills}/keeper-secrets/SKILL.md (100%) rename {skills => plugins/keeper-secrets/skills}/keeper-secrets/references/keeper-notation.md (100%) rename {skills => plugins/keeper-secrets/skills}/keeper-secrets/references/ksm-commands.md (100%) rename {skills => plugins/keeper-secrets/skills}/keeper-secrets/references/ksm-exec-patterns.md (100%) create mode 100644 plugins/keeper-setup/.claude-plugin/plugin.json create mode 100644 plugins/keeper-setup/.cursor-plugin/plugin.json create mode 100644 plugins/keeper-setup/assets/logo.png rename {skills => plugins/keeper-setup/skills}/keeper-setup/SKILL.md (100%) create mode 100755 scripts/validate-plugins.sh diff --git a/.claude-plugin/marketplace.json b/.claude-plugin/marketplace.json index 9c3d5bb..f03d1b8 100644 --- a/.claude-plugin/marketplace.json +++ b/.claude-plugin/marketplace.json @@ -1,55 +1,74 @@ { - "marketplace": "claude-plugins", + "name": "keeper-security", + "owner": { + "name": "Keeper Security", + "email": "commanders@keepersecurity.com" + }, + "metadata": { + "description": "Keeper Security Agent Kit - KSM CLI, Commander, and setup skills for Claude Code", + "version": "0.1.0" + }, "plugins": [ { - "id": "keeper-secrets", - "name": "keeper-secrets", - "description": "Retrieve, inject, and manage secrets from Keeper Vault using KSM CLI for developer workflows", - "version": "1.0.0", - "author": "Keeper Security", - "keywords": [ - "secrets", - "vault", - "credentials", - "ksm", - "devops" - ], - "path": "./skills/keeper-secrets" - }, - { - "id": "keeper-admin", "name": "keeper-admin", - "description": "Manage Keeper Vault, enterprise administration, PAM, and privileged access using Commander CLI", - "version": "1.0.0", - "author": "Keeper Security", + "source": "./plugins/keeper-admin", + "description": "Manage Keeper Vault, enterprise administration, PAM, and privileged access using Keeper Commander CLI (keeper).", + "version": "0.1.0", + "author": { + "name": "Keeper Security", + "email": "commanders@keepersecurity.com" + }, + "homepage": "https://github.com/Keeper-Security/agent-kit", + "repository": "https://github.com/Keeper-Security/agent-kit", + "license": "Apache-2.0", + "category": "development", "keywords": [ "admin", "vault", "pam", "enterprise", "keeper" - ], - "path": "./skills/keeper-admin" + ] + }, + { + "name": "keeper-secrets", + "source": "./plugins/keeper-secrets", + "description": "Retrieve, inject, and manage secrets from Keeper Vault using KSM CLI (ksm) for developer workflows, including ksm exec, interpolate, and Keeper notation.", + "version": "0.1.0", + "author": { + "name": "Keeper Security", + "email": "commanders@keepersecurity.com" + }, + "homepage": "https://github.com/Keeper-Security/agent-kit", + "repository": "https://github.com/Keeper-Security/agent-kit", + "license": "Apache-2.0", + "category": "development", + "keywords": [ + "secrets", + "vault", + "credentials", + "ksm", + "devops" + ] }, { - "id": "keeper-setup", "name": "keeper-setup", - "description": "Install and configure Keeper CLI tools (KSM CLI and Commander) for the Keeper Security agent kit", - "version": "1.0.0", - "author": "Keeper Security", + "source": "./plugins/keeper-setup", + "description": "Install and configure Keeper CLI tools (KSM CLI and Commander) for the Keeper Security agent kit, including profiles and authentication.", + "version": "0.1.0", + "author": { + "name": "Keeper Security", + "email": "commanders@keepersecurity.com" + }, + "homepage": "https://github.com/Keeper-Security/agent-kit", + "repository": "https://github.com/Keeper-Security/agent-kit", + "license": "Apache-2.0", + "category": "development", "keywords": [ "setup", "install", "configuration" - ], - "path": "./skills/keeper-setup" + ] } - ], - "homepage": "https://github.com/Keeper-Security/agent-kit", - "repository": "https://github.com/Keeper-Security/agent-kit", - "license": "Apache-2.0", - "support": { - "email": "commanders@keepersecurity.com", - "url": "https://keepersecurity.com/support" - } + ] } diff --git a/.claude-plugin/plugin.json b/.claude-plugin/plugin.json deleted file mode 100644 index 16b2e20..0000000 --- a/.claude-plugin/plugin.json +++ /dev/null @@ -1,30 +0,0 @@ -{ - "name": "keeper-security", - "description": "Keeper Security Agent Kit: zero-trust secrets management and privileged access for AI agents. Enables agents to securely retrieve, inject, and manage credentials through KSM CLI and Keeper Commander.", - "version": "1.1.0", - "homepage": "https://github.com/Keeper-Security/agent-kit", - "author": { - "name": "Keeper Security", - "email": "commander@keepersecurity.com", - "url": "https://keepersecurity.com" - }, - "keywords": [ - "agent-kit", - "credentials", - "devops", - "keeper-security", - "keeper", - "ksm", - "nhi", - "non-human-identity", - "pam", - "password-manager", - "privileged-access", - "secrets", - "security", - "vault", - "zero-knowledge", - "zero-trust" - ], - "category": "security" -} diff --git a/.cursor-plugin/marketplace.json b/.cursor-plugin/marketplace.json new file mode 100644 index 0000000..fe25ff8 --- /dev/null +++ b/.cursor-plugin/marketplace.json @@ -0,0 +1,81 @@ +{ + "name": "keeper-security", + "owner": { + "name": "Keeper Security", + "email": "commanders@keepersecurity.com" + }, + "metadata": { + "description": "Keeper Security Agent Kit - KSM CLI, Commander, and setup skills for Claude Code", + "version": "0.1.0", + "homepage": "https://github.com/Keeper-Security/agent-kit", + "repository": "https://github.com/Keeper-Security/agent-kit", + "license": "Apache-2.0", + "support": { + "email": "commanders@keepersecurity.com", + "url": "https://keepersecurity.com/support" + } + }, + "plugins": [ + { + "name": "keeper-admin", + "source": "./plugins/keeper-admin", + "description": "Manage Keeper Vault, enterprise administration, PAM, and privileged access using Commander CLI", + "version": "0.1.0", + "author": { + "name": "Keeper Security", + "email": "commanders@keepersecurity.com" + }, + "homepage": "https://github.com/Keeper-Security/agent-kit", + "repository": "https://github.com/Keeper-Security/agent-kit", + "license": "Apache-2.0", + "category": "development", + "keywords": [ + "admin", + "vault", + "pam", + "enterprise", + "keeper" + ] + }, + { + "name": "keeper-secrets", + "source": "./plugins/keeper-secrets", + "description": "Retrieve, inject, and manage secrets from Keeper Vault using KSM CLI for developer workflows", + "version": "0.1.0", + "author": { + "name": "Keeper Security", + "email": "commanders@keepersecurity.com" + }, + "homepage": "https://github.com/Keeper-Security/agent-kit", + "repository": "https://github.com/Keeper-Security/agent-kit", + "license": "Apache-2.0", + "category": "development", + "keywords": [ + "secrets", + "vault", + "credentials", + "ksm", + "devops" + ] + }, + { + "name": "keeper-setup", + "source": "./plugins/keeper-setup", + "description": "Install and configure Keeper CLI tools (KSM CLI and Commander) for the Keeper Security agent kit", + "version": "0.1.0", + "author": { + "name": "Keeper Security", + "email": "commanders@keepersecurity.com" + }, + "homepage": "https://github.com/Keeper-Security/agent-kit", + "repository": "https://github.com/Keeper-Security/agent-kit", + "license": "Apache-2.0", + "category": "development", + "keywords": [ + "setup", + "install", + "configuration" + ] + } + ] +} diff --git a/.github/ACT.md b/.github/ACT.md index dc6e19d..dc78193 100644 --- a/.github/ACT.md +++ b/.github/ACT.md @@ -4,7 +4,7 @@ **act** is a CLI that runs **GitHub Actions workflows on your computer** instead of on GitHub. It starts **Docker** containers that behave like GitHub’s `ubuntu-latest` runners, checks out your repo (or copies the working tree), and runs the same `run:` steps your YAML defines. -You use it to **catch CI failures before you push**-for example broken `plugin.json`, bad SKILL frontmatter, or failed skill checks. +You use it to **smoke-test GitHub workflows before you push** (semantic PR, release-please). **Plugin and skill validation** runs via **pre-commit** (`plugin-ci`) or **`task validate`**, not as a hosted workflow. ## How it works (mental model) @@ -30,25 +30,16 @@ From the **repository root**: ./scripts/act-all.sh ``` -That script lists and runs **validate-plugin** and **test-skills** with `act push` (these **must** succeed - the script exits non-zero if either fails). It then runs **semantic-pull-request** (`act pull_request` + [`.github/act/event-pull_request.json`](act/event-pull_request.json)) and **release-please** (`act push`) as **extra smoke tests**. Those two workflows call the **GitHub REST API** (`pulls.get`, release-please), so they **usually fail inside act** without a real `GITHUB_TOKEN`; that is expected and the script still exits **0** after the first two workflows pass. +That script runs **`task validate`** first (same as the **pre-commit** `plugin-ci` hook; **must** succeed—the script exits non-zero if it fails). It then runs **semantic-pull-request** (`act pull_request` + [`.github/act/event-pull_request.json`](act/event-pull_request.json)) and **release-please** (`act push`) as **extra smoke tests**. Those two workflows call the **GitHub REST API** (`pulls.get`, release-please), so they **usually fail inside act** without a real `GITHUB_TOKEN`; that is expected and the script still exits **0** after plugin validation passes. -**Manual - plugin + skills only:** - -**1. See what would run (job names):** - -```bash -act -l -W .github/workflows/validate-plugin.yml -act -l -W .github/workflows/test-skills.yml -``` - -**2. Run the same checks GitHub runs (simulates `push`):** +**Manual - plugin + skills only (no act):** ```bash -act push -W .github/workflows/validate-plugin.yml -act push -W .github/workflows/test-skills.yml +task validate +# or: pre-commit run plugin-ci --all-files ``` -Order does not matter between the two files; run both to match what CI exercises for plugin + skills. +Requires **`jq`** (same as `scripts/validate.sh`). **Manual - semantic PR workflow (`pull_request`):** @@ -70,14 +61,13 @@ Locally, **release-please** usually fails at the step that talks to the GitHub A | Workflow file | What it mainly validates | | --- | --- | -| [`validate-plugin.yml`](workflows/validate-plugin.yml) | `plugin.json`, `marketplace.json`, SKILL frontmatter, reference files, etc. | -| [`test-skills.yml`](workflows/test-skills.yml) | Skill descriptions/triggers, example checks, doc headings in root files | +| *(none on GitHub for plugins)* | Plugin/skill checks: **`task validate`** (also **pre-commit** hook `plugin-ci`). | | [`semantic-pull-request.yml`](workflows/semantic-pull-request.yml) | PR title matches Conventional Commits (squash-merge title) | | [`release-please.yml`](workflows/release-please.yml) | Release PR + changelog + GitHub Release via release-please | ## Convenience: Task wrapper -If you use [Task](https://taskfile.dev/), [`Taskfile.yaml`](../Taskfile.yaml) defines **`task act`**, which runs [`scripts/act-all.sh`](../scripts/act-all.sh) (all workflows). For Markdown formatting before a PR, run **`task fmt`** separately. You can ignore Task and run `./scripts/act-all.sh` or the manual commands above. +If you use [Task](https://taskfile.dev/), [`Taskfile.yaml`](../Taskfile.yaml) defines **`task validate`** (plugin validation) and **`task act`**, which runs [`scripts/act-all.sh`](../scripts/act-all.sh). For Markdown formatting before a PR, run **`task fmt`** separately. You can ignore Task and run `./scripts/act-all.sh` or the manual commands above. ## Differences vs running on GitHub @@ -86,7 +76,6 @@ If you use [Task](https://taskfile.dev/), [`Taskfile.yaml`](../Taskfile.yaml) de | `paths:` filters | Workflow may be skipped if paths don’t match | Ignored for “should this run?” - you chose the file with `-W` | | Runners | GitHub-hosted Ubuntu | Docker image (e.g. `ghcr.io/catthehacker/ubuntu:act-latest`) | | **`GITHUB_TOKEN`** | Injected automatically for each job as `secrets.GITHUB_TOKEN` (scoped to the repo, permissions from the workflow `permissions:` block). You do **not** add it under **Settings → Secrets**. | **Not** the same as GitHub’s token. act may set a placeholder or empty value; it does **not** mint a real token that can call `api.github.com` like the hosted runner. To exercise API-using actions locally you must pass a token yourself (e.g. `act … -s GITHUB_TOKEN=…` with a fine-scoped PAT)-optional and easy to get wrong, so we treat API workflows as **CI-only** for “full success.” | -| `import yaml` in Python | Works after we install **`python3-yaml`** via `apt` in the workflow | Same; that step exists so act matches GitHub | | **release-please** / **semantic PR** actions | Full GitHub API + real `GITHUB_TOKEN` | May fail or partially run without a PAT; use for YAML/container smoke tests | ## Troubleshooting @@ -95,7 +84,6 @@ If you use [Task](https://taskfile.dev/), [`Taskfile.yaml`](../Taskfile.yaml) de | --- | --- | | Docker not running | Start Docker Desktop / Linux daemon | | `unable to get git ref` | Add an initial commit; harmless for file copy | -| `ModuleNotFoundError: yaml` | Workflows should install `python3-yaml`; pull latest `.github/workflows` | | Slow first run | Normal while Docker pulls the runner image | ## See also diff --git a/.github/pull_request_template.md b/.github/pull_request_template.md index fe644e0..6c4eecc 100644 --- a/.github/pull_request_template.md +++ b/.github/pull_request_template.md @@ -1,5 +1,3 @@ -# Pull request - ## Summary @@ -13,6 +11,6 @@ - [ ] Documentation (skills, references, README, etc.) -- [ ] Plugin / marketplace metadata (`.claude-plugin/`) +- [ ] Plugin / marketplace metadata (`.claude-plugin/`or `.cursor-plugin/`) - [ ] CI / tooling (workflows, hooks, Taskfile) - [ ] Other (describe below) diff --git a/.github/workflows/test-skills.yml b/.github/workflows/test-skills.yml deleted file mode 100644 index 31a7e3f..0000000 --- a/.github/workflows/test-skills.yml +++ /dev/null @@ -1,253 +0,0 @@ -name: Test Skills Trigger - -on: - push: - branches: [main, develop] - paths: - - "skills/**" - pull_request: - branches: [main, develop] - paths: - - "skills/**" - -jobs: - test-skill-triggers: - name: Verify Skill Trigger Patterns - runs-on: ubuntu-latest - steps: - - uses: actions/checkout@v6 - - - name: Install PyYAML (frontmatter validation) - run: | - sudo apt-get update -qq - sudo apt-get install -y python3-yaml - - - name: Extract and Test Skill Descriptions - run: | - python << 'EOF' - import os - import re - import yaml - - # Map of skill names to trigger keywords they should recognize - expected_triggers = { - "keeper-secrets": [ - "keeper", "ksm", "secret", "vault", "credentials", - "keeper://", "password", "api key", "certificate", - "ci/cd", "docker", "kubernetes" - ], - "keeper-admin": [ - "keeper", "commander", "admin", "enterprise", - "user management", "pam", "rotation", "session" - ], - "keeper-setup": [ - "install", "keeper", "setup", "configure", - "ksm", "commander", "cli" - ] - } - - skills_dir = "skills" - found_skills = {} - - for skill_name in os.listdir(skills_dir): - skill_path = os.path.join(skills_dir, skill_name, "SKILL.md") - if not os.path.exists(skill_path): - continue - - with open(skill_path) as f: - content = f.read() - - # Extract frontmatter - match = re.match(r'^---\n(.*?)\n---', content, re.DOTALL) - if not match: - print(f"⚠ {skill_name}: No frontmatter") - continue - - try: - frontmatter = yaml.safe_load(match.group(1)) - except: - print(f"⚠ {skill_name}: Invalid YAML") - continue - - description = frontmatter.get("description", "") - found_skills[skill_name] = description - - print(f"\n📋 Skill: {skill_name}") - print(f" Description length: {len(description)} chars") - - # Check for trigger keywords - description_lower = description.lower() - expected = expected_triggers.get(skill_name, []) - found_triggers = [] - - for trigger in expected: - if trigger.lower() in description_lower: - found_triggers.append(trigger) - - if found_triggers: - print(f" ✓ Found {len(found_triggers)}/{len(expected)} expected triggers") - else: - print(f" ⚠ No expected triggers found in description") - - print(f"\n✓ Analyzed {len(found_skills)} skills") - EOF - - - name: Verify Skill Content - run: | - python << 'EOF' - import os - - skills_content_checks = { - "keeper-secrets/SKILL.md": [ - "When to Use KSM", - "Prerequisites", - "Core Commands", - "ksm exec", - "ksm interpolate", - "Guardrails" - ], - "keeper-admin/SKILL.md": [ - "When to Use Commander", - "Prerequisites", - "Vault Operations", - "Enterprise Administration", - "Guardrails" - ], - "keeper-setup/SKILL.md": [ - "Quick Install", - "KSM CLI", - "Commander", - "Troubleshooting" - ] - } - - print("Checking skill content coverage...") - errors = [] - - for skill_path, expected_sections in skills_content_checks.items(): - full_path = f"skills/{skill_path}" - if not os.path.exists(full_path): - errors.append(f"Missing: {full_path}") - continue - - with open(full_path) as f: - content = f.read() - - skill_name = skill_path.split('/')[0] - missing_sections = [] - - for section in expected_sections: - if section not in content: - missing_sections.append(section) - - if missing_sections: - print(f"⚠ {skill_name}: Missing sections: {', '.join(missing_sections)}") - else: - print(f"✓ {skill_name}: All expected sections present") - - if errors: - print("\n❌ Content check failed:") - for error in errors: - print(f" - {error}") - exit(1) - EOF - - validate-examples: - name: Validate Code Examples - runs-on: ubuntu-latest - steps: - - uses: actions/checkout@v6 - - - name: Check Code Block Formatting - run: | - python << 'EOF' - import os - import re - - def check_markdown_file(filepath): - with open(filepath) as f: - content = f.read() - - # Find all code blocks - code_blocks = re.findall(r'```(\w+)?\n(.*?)```', content, re.DOTALL) - - issues = [] - - for lang, code in code_blocks: - # Check for language specification - if not lang: - # Count newlines to identify problematic blocks - if '\n' in code and code.strip(): - issues.append("Code block without language specification") - - # Check for hardcoded secrets - if re.search(r'password\s*=\s*["\'](?!<)(?!keeper)', code, re.IGNORECASE): - issues.append("Possible hardcoded password in example") - if re.search(r'token\s*=\s*["\'](?!<)(?!keeper)', code, re.IGNORECASE): - issues.append("Possible hardcoded token in example") - - return issues - - print("Checking for insecure examples...") - - for root, dirs, files in os.walk("skills"): - for file in files: - if file.endswith(".md"): - filepath = os.path.join(root, file) - issues = check_markdown_file(filepath) - - if issues: - print(f"\n⚠ {filepath}:") - for issue in issues: - print(f" - {issue}") - else: - print(f"✓ {filepath}") - EOF - - documentation-checks: - name: Documentation Quality - runs-on: ubuntu-latest - steps: - - uses: actions/checkout@v6 - - - name: Check Documentation Completeness - run: | - python << 'EOF' - import os - - required_docs = [ - ("README.md", ["Installation", "Prerequisites", "Documentation", "Contributing", "License", "Security"]), - ("SECURITY.md", ["Reporting", "Supported versions"]), - ("CONTRIBUTING.md", ["How to Contribute", "Testing Guidelines", "Submission Checklist"]), - ("LICENSE.md", []) - ] - - print("Checking documentation files...") - - for doc_file, expected_sections in required_docs: - if not os.path.exists(doc_file): - print(f"⚠ Missing: {doc_file}") - continue - - with open(doc_file) as f: - content = f.read() - - missing = [] - for section in expected_sections: - if section not in content: - missing.append(section) - - if missing: - print(f"⚠ {doc_file}: Missing: {', '.join(missing)}") - else: - print(f"✓ {doc_file}") - EOF - - summary: - name: Test Summary - runs-on: ubuntu-latest - needs: [test-skill-triggers, validate-examples, documentation-checks] - if: always() - steps: - - name: Results - run: echo "✓ Skill testing complete" diff --git a/.github/workflows/validate-plugin.yml b/.github/workflows/validate-plugin.yml deleted file mode 100644 index 45fab2a..0000000 --- a/.github/workflows/validate-plugin.yml +++ /dev/null @@ -1,359 +0,0 @@ -name: Validate Plugin Configuration - -on: - push: - branches: [main, develop] - paths: - - ".claude-plugin/**" - - "skills/**" - - ".github/workflows/validate-plugin.yml" - pull_request: - branches: [main, develop] - paths: - - ".claude-plugin/**" - - "skills/**" - - ".github/workflows/validate-plugin.yml" - -jobs: - validate-plugin-json: - name: Validate plugin.json Schema - runs-on: ubuntu-latest - steps: - - uses: actions/checkout@v6 - - - name: Validate plugin.json - run: | - # Check file exists - if [ ! -f ".claude-plugin/plugin.json" ]; then - echo "ERROR: .claude-plugin/plugin.json not found" - exit 1 - fi - - # Validate JSON syntax - if ! python -m json.tool .claude-plugin/plugin.json > /dev/null; then - echo "ERROR: Invalid JSON in plugin.json" - exit 1 - fi - - echo "✓ plugin.json is valid JSON" - - - name: Check Required Fields - run: | - python << 'EOF' - import json - - with open(".claude-plugin/plugin.json") as f: - data = json.load(f) - - required = ["name", "description", "version", "author", "keywords", "category"] - for field in required: - if field not in data: - print(f"ERROR: Missing required field '{field}'") - exit(1) - print(f"✓ {field}: {data[field]}") - - if not isinstance(data.get("keywords"), list): - print("ERROR: 'keywords' must be an array") - exit(1) - - print("✓ All required fields present and valid") - EOF - - validate-marketplace-json: - name: Validate marketplace.json - runs-on: ubuntu-latest - steps: - - uses: actions/checkout@v6 - - - name: Validate marketplace.json - run: | - if [ -f ".claude-plugin/marketplace.json" ]; then - if ! python -m json.tool .claude-plugin/marketplace.json > /dev/null; then - echo "ERROR: Invalid JSON in marketplace.json" - exit 1 - fi - echo "✓ marketplace.json is valid JSON" - else - echo "⚠ marketplace.json not found (optional for self-hosted)" - fi - - - name: Check Marketplace Structure - run: | - python << 'EOF' - import json - import os - - if not os.path.exists(".claude-plugin/marketplace.json"): - print("marketplace.json not present - skipping") - exit(0) - - with open(".claude-plugin/marketplace.json") as f: - data = json.load(f) - - if "plugins" not in data: - print("ERROR: Missing 'plugins' array in marketplace.json") - exit(1) - - for plugin in data["plugins"]: - required = ["id", "name", "description", "version"] - for field in required: - if field not in plugin: - print(f"ERROR: Plugin missing '{field}'") - exit(1) - - print(f"✓ marketplace.json contains {len(data['plugins'])} plugins") - for plugin in data["plugins"]: - print(f" - {plugin['name']} v{plugin['version']}") - EOF - - validate-skill-files: - name: Validate SKILL.md Files - runs-on: ubuntu-latest - steps: - - uses: actions/checkout@v6 - - - name: Install PyYAML (frontmatter validation) - run: | - sudo apt-get update -qq - sudo apt-get install -y python3-yaml - - - name: Find All SKILL.md Files - run: | - echo "Found SKILL.md files:" - find skills -name "SKILL.md" -type f | while read file; do - echo " - $file" - done - - - name: Validate Frontmatter - run: | - python << 'EOF' - import os - import re - import yaml - - skill_files = [] - for root, dirs, files in os.walk("skills"): - for file in files: - if file == "SKILL.md": - skill_files.append(os.path.join(root, file)) - - if not skill_files: - print("ERROR: No SKILL.md files found") - exit(1) - - errors = [] - - for filepath in skill_files: - print(f"\nValidating {filepath}") - - with open(filepath, 'r') as f: - content = f.read() - - # Check for frontmatter - if not content.startswith('---'): - errors.append(f"{filepath}: Missing frontmatter") - continue - - # Extract frontmatter - match = re.match(r'^---\n(.*?)\n---\n', content, re.DOTALL) - if not match: - errors.append(f"{filepath}: Invalid frontmatter syntax") - continue - - try: - frontmatter = yaml.safe_load(match.group(1)) - except yaml.YAMLError as e: - errors.append(f"{filepath}: Invalid YAML frontmatter: {e}") - continue - - # Validate required fields - if "name" not in frontmatter: - errors.append(f"{filepath}: Missing 'name' field") - elif not isinstance(frontmatter["name"], str): - errors.append(f"{filepath}: 'name' must be a string") - elif len(frontmatter["name"]) > 64: - errors.append(f"{filepath}: 'name' must be ≤64 characters") - else: - print(f" ✓ name: {frontmatter['name']}") - - if "description" not in frontmatter: - errors.append(f"{filepath}: Missing 'description' field") - elif not isinstance(frontmatter["description"], str): - errors.append(f"{filepath}: 'description' must be a string") - else: - # Warn if too long for API but don't fail - if len(frontmatter["description"]) > 200: - print(f" ⚠ description is {len(frontmatter['description'])} chars (200+ may be truncated in some APIs)") - print(f" ✓ description: {frontmatter['description'][:50]}...") - - # Check for content after frontmatter - content_after = content[match.end():].strip() - if not content_after: - errors.append(f"{filepath}: No content after frontmatter") - else: - print(f" ✓ Has {len(content_after.split())} words of content") - - if errors: - print("\n❌ ERRORS FOUND:") - for error in errors: - print(f" - {error}") - exit(1) - else: - print("\n✓ All SKILL.md files are valid") - EOF - - - name: Check Markdown Syntax - run: | - python << 'EOF' - import os - import re - - skill_files = [] - for root, dirs, files in os.walk("skills"): - for file in files: - if file == "SKILL.md": - skill_files.append(os.path.join(root, file)) - - errors = [] - - for filepath in skill_files: - with open(filepath, 'r') as f: - lines = f.readlines() - - in_frontmatter = False - frontmatter_count = 0 - last_heading_level = 0 - in_code_block = False - code_language = None - - for i, line in enumerate(lines, 1): - # Track frontmatter - if line.strip() == '---': - frontmatter_count += 1 - if frontmatter_count == 2: - in_frontmatter = False - continue - - if in_frontmatter: - continue - - # Track code blocks - if line.startswith('```'): - if not in_code_block: - match = re.match(r'^```(\w+)?', line) - code_language = match.group(1) if match else None - if not code_language: - errors.append(f"{filepath}:{i}: Code block missing language specification") - in_code_block = not in_code_block - - # Check heading hierarchy - if line.startswith('##'): - match = re.match(r'^(#+)', line) - if match: - level = len(match.group(1)) - if level > last_heading_level + 1: - errors.append(f"{filepath}:{i}: Heading jumps from H{last_heading_level} to H{level}") - last_heading_level = level - - if errors: - print("❌ Markdown issues found:") - for error in errors: - print(f" - {error}") - # Don't fail - these are warnings - print("\n⚠ Fix these issues for better readability") - else: - print("✓ Markdown syntax looks good") - EOF - - check-references: - name: Validate Reference Files - runs-on: ubuntu-latest - steps: - - uses: actions/checkout@v6 - - - name: Check Reference Files Exist - run: | - echo "Checking for reference documentation files..." - - required_refs=( - "skills/keeper-secrets/references/ksm-commands.md" - "skills/keeper-secrets/references/keeper-notation.md" - "skills/keeper-secrets/references/ksm-exec-patterns.md" - "skills/keeper-admin/references/commander-commands.md" - "skills/keeper-admin/references/enterprise-mgmt.md" - "skills/keeper-admin/references/pam-commands.md" - "skills/keeper-admin/references/rotation-commands.md" - ) - - missing=() - for ref in "${required_refs[@]}"; do - if [ ! -f "$ref" ]; then - missing+=("$ref") - else - echo " ✓ $ref" - fi - done - - if [ ${#missing[@]} -gt 0 ]; then - echo "" - echo "❌ Missing reference files:" - for ref in "${missing[@]}"; do - echo " - $ref" - done - exit 1 - fi - - echo "" - echo "✓ All reference files present" - - - name: Check Reference Content - run: | - python << 'EOF' - import os - - refs = { - "skills/keeper-secrets/references/ksm-commands.md": 50, - "skills/keeper-secrets/references/keeper-notation.md": 40, - "skills/keeper-secrets/references/ksm-exec-patterns.md": 60, - "skills/keeper-admin/references/commander-commands.md": 50, - "skills/keeper-admin/references/enterprise-mgmt.md": 40, - "skills/keeper-admin/references/pam-commands.md": 40, - "skills/keeper-admin/references/rotation-commands.md": 40, - } - - for filepath, min_lines in refs.items(): - if not os.path.exists(filepath): - print(f"⚠ {filepath} not found (skipping)") - continue - - with open(filepath) as f: - content = f.read() - lines = content.count('\n') - - if lines < min_lines: - print(f"⚠ {filepath}: {lines} lines (expected ≥{min_lines})") - else: - print(f"✓ {filepath}: {lines} lines") - EOF - - summary: - name: Validation Summary - runs-on: ubuntu-latest - needs: - [ - validate-plugin-json, - validate-marketplace-json, - validate-skill-files, - check-references, - ] - if: always() - steps: - - name: Check Results - run: | - if [ "${{ needs.validate-plugin-json.result }}" = "failure" ] || \ - [ "${{ needs.validate-skill-files.result }}" = "failure" ] || \ - [ "${{ needs.check-references.result }}" = "failure" ]; then - echo "❌ Validation failed" - exit 1 - fi - echo "✓ All validations passed" diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index ec247b4..a1ebd10 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -14,11 +14,20 @@ repos: # Markdown formatting - id: fmt name: rumdl fmt (check) - stages: [commit] + stages: [pre-commit] language: system entry: task fmt pass_filenames: false + # Plugin manifests (Claude/Cursor), marketplaces, skills, triggers, docs (scripts/validate.sh) + - id: plugin-ci + name: Plugin and skill validation + stages: [pre-commit] + language: system + entry: task validate + pass_filenames: false + files: ^(plugins/|\.claude-plugin/|\.cursor-plugin/|scripts/validate\.sh|Taskfile\.yaml|README\.md|SECURITY\.md|CONTRIBUTING\.md|LICENSE\.md) + # Standard pre-commit hooks - repo: https://github.com/pre-commit/pre-commit-hooks rev: v6.0.0 diff --git a/.release-please-manifest.json b/.release-please-manifest.json index 5fdd883..466df71 100644 --- a/.release-please-manifest.json +++ b/.release-please-manifest.json @@ -1,3 +1,3 @@ { - ".": "1.1.0" + ".": "0.1.0" } diff --git a/CHANGELOG.md b/CHANGELOG.md index 2d56da5..6d13c6c 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -2,27 +2,20 @@ All notable changes to **Keeper Security Agent Kit** are documented in this file. -The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/), and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html). +The format is based on [Keep a Changelog](https://keepachangelog.com/en/0.1.0/), and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html). -## [1.1.0](https://github.com/Keeper-Security/agent-kit/compare/v1.0.0...v1.1.0) (2026-03-25) - - -### Features - -* initial commit ([aef4ab0](https://github.com/Keeper-Security/agent-kit/commit/aef4ab0d8fe469443118dcfeec073b939b62e2fa)) - -## [1.0.0] - 2026-03-25 +## [0.1.0] - 2026-03-31 ### Added -- **Skills** - Three agent skills with reference documentation under `skills/*/references/`: - - `keeper-secrets` - KSM CLI (`ksm`): secrets retrieval, `ksm exec`, `ksm interpolate`, Keeper notation, Docker/Kubernetes/CI patterns. +- **Plugins** - Three agent plugins with reference documentation under `plugins/*/references/`: - `keeper-admin` - Keeper Commander (`keeper`): vault, enterprise admin, PAM, rotation, and related command references. + - `keeper-secrets` - KSM CLI (`ksm`): secrets retrieval, `ksm exec`, `ksm interpolate`, Keeper notation, Docker/Kubernetes/CI patterns. - `keeper-setup` - Installing and configuring the KSM and Commander CLIs, regions, and troubleshooting. -- **Distribution** - Claude Code plugin metadata (`.claude-plugin/`), marketplace listing, and install paths via [Vercel Skills CLI](https://github.com/vercel-labs/skills) (`npx skills add`) or manual copy into an agent’s skills directory. +- **Distribution** - Claude Code plugin metadata (`.claude-plugin/`), Cursor plugin metadata (`.cursor-plugin/`), marketplace listing, and install paths via [Vercel Skills CLI](https://github.com/vercel-labs/skills) (`npx skills add`) or manual copy into an agent’s skills directory. - **Documentation** - README, CONTRIBUTING, SECURITY, `TEST_PROMPTS.md`, and links to official Keeper docs (Secrets Manager overview, Commander overview, Keeper notation). - **CI** - GitHub Actions workflows to validate plugin JSON, marketplace JSON, SKILL frontmatter, reference files, and skill trigger/content checks; `python3-yaml` installed in workflows for reliable frontmatter parsing locally and on runners. - **Developer tooling** - `Taskfile` tasks for Markdown (`task fmt`), dependencies (`task install`), and running CI locally with [act](https://github.com/nektos/act) (`task act`); [`.github/ACT.md`](.github/ACT.md) documents local workflow runs. -- **Community** - Issue templates, pull request template, `CODEOWNERS`, Apache 2.0 [`LICENSE.md`](LICENSE.md). +- **Community** - Issue templates, pull request template, `CODEOWNERS`, Apache 2.0 [`LICENSE.md`](LICENSE.md), and pre-commit hooks for Markdown format checks, Conventional Commit messages, and plugin/skill validation when you commit. -[1.0.0]: https://github.com/Keeper-Security/agent-kit/releases/tag/v1.0.0 +[0.1.0]: https://github.com/Keeper-Security/agent-kit/releases/tag/v0.1.0 diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index b2f495e..57955ce 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -91,7 +91,9 @@ pip install pre-commit pre-commit install ``` -Hooks call `task fmt` (rumdl) and validate **commit messages** (Conventional Commits via `scripts/validate-commit-msg.sh`). `default_install_hook_types` includes `commit-msg`, so `pre-commit install` registers both. Before you open a PR, run `task fmt` or `pre-commit run --all-files`. Deeper validation (plugin JSON, skill frontmatter, references) runs in **GitHub Actions** on push/PR. +You also need **`jq`** on your PATH for `scripts/validate.sh` (e.g. `brew install jq` on macOS). + +Hooks call `task fmt` (rumdl), validate **commit messages** (Conventional Commits via `scripts/validate-commit-msg.sh`), and **`task validate`** (full check: `scripts/validate.sh` — manifests, skills, triggers, examples, root docs). `default_install_hook_types` includes `commit-msg`, so `pre-commit install` registers both stages. Before you open a PR, run `task fmt`, **`task validate`** (or `pre-commit run plugin-ci --all-files`), or `pre-commit run --all-files`. ### Commit messages (Conventional Commits) @@ -107,19 +109,19 @@ Pull requests are checked by **Semantic PR** (`.github/workflows/semantic-pull-r ### Releases and changelog -- **[release-please](https://github.com/googleapis/release-please)** runs on pushes to `main` (`.github/workflows/release-please.yml`). It opens a **release pull request** that bumps `version.txt`, `.release-please-manifest.json`, `CHANGELOG.md`, and versions in `.claude-plugin/plugin.json` and `marketplace.json` based on merged conventional commits. +- **[release-please](https://github.com/googleapis/release-please)** runs on pushes to `main` (`.github/workflows/release-please.yml`). It opens a **release pull request** that bumps `version.txt`, `.release-please-manifest.json`, `CHANGELOG.md`, and every version field listed in `release-please-config.json` → `extra-files` (`.claude-plugin/plugin.json`, `.claude-plugin/marketplace.json` `metadata.version` and each plugin’s `version`, `.cursor-plugin/marketplace.json` `metadata.version`, `plugins/**/.cursor-plugin/plugin.json`, and the versioned example in `docs/add-a-plugin.md`). - When that release PR is **merged**, GitHub receives a **release** with notes derived from those commits (and the updated changelog). - Configure paths in `release-please-config.json` and `.release-please-manifest.json`. #### `version.txt` (how the version is updated) - **Role** - One line, semver only (e.g. `1.2.3`). With **`release-type: simple`** in `release-please-config.json`, this file is the **canonical project version** release-please reads and writes. -- **Normal workflow** - **Do not** edit `version.txt` by hand to ship a release. Land conventional commits on `main`, let release-please open its **release PR**, review it, and **merge that PR**. The PR updates `version.txt` together with `.release-please-manifest.json`, `CHANGELOG.md`, and the `version` fields in `.claude-plugin/plugin.json` and `marketplace.json` (see `extra-files` in `release-please-config.json`). +- **Normal workflow** - **Do not** edit `version.txt` by hand to ship a release. Land conventional commits on `main`, let release-please open its **release PR**, review it, and **merge that PR**. The PR updates `version.txt` together with `.release-please-manifest.json`, `CHANGELOG.md`, and all paths in `release-please-config.json` → `extra-files` (see above). - **Avoid drift** - If you ever change a version **outside** that flow (unusual), update **every** file release-please keeps in sync so they all match; mismatches break automation and CI expectations. ### Optional: run CI workflows locally with [act](https://github.com/nektos/act) -Use **[`.github/ACT.md`](.github/ACT.md)** for prerequisites, how `act` maps to GitHub Actions, and the exact commands. From the repo root, **`./scripts/act-all.sh`** (or **`task act`**) runs **act** against every workflow under `.github/workflows`. **validate-plugin** and **test-skills** must pass locally (same as CI). **semantic-pull-request** and **release-please** are included for smoke coverage but typically do not finish inside act without GitHub API access; see **ACT.md**. +Use **[`.github/ACT.md`](.github/ACT.md)** for prerequisites, how `act` maps to GitHub Actions, and the exact commands. From the repo root, **`./scripts/act-all.sh`** (or **`task act`**) runs **`task validate`** first (same checks as the **pre-commit** `plugin-ci` hook), then **act** against **semantic-pull-request** and **release-please**. Plugin validation is **not** duplicated on GitHub; use pre-commit (or `task validate`) before you push. **semantic-pull-request** and **release-please** often do not finish inside act without GitHub API access; see **ACT.md**. ## Testing Guidelines @@ -293,7 +295,7 @@ Focus on: ## Versioning -**Plugin / repo version** - [release-please](https://github.com/googleapis/release-please) manages **one** semver for the whole distribution: `version.txt`, `.release-please-manifest.json`, `CHANGELOG.md`, and the `version` fields in **`.claude-plugin/plugin.json`** and **`.claude-plugin/marketplace.json`** (see `release-please-config.json` → `extra-files`). It does **not** bump per-skill versions; skills under `skills/` ship as part of that plugin release. +**Plugin / repo version** - [release-please](https://github.com/googleapis/release-please) manages **one** semver for the whole distribution: `version.txt`, `.release-please-manifest.json`, `CHANGELOG.md`, **`.claude-plugin/plugin.json`**, **`.claude-plugin/marketplace.json`** (`metadata.version` and each listed plugin’s `version`), **`.cursor-plugin/marketplace.json`** (`metadata.version`), **`plugins/**/.cursor-plugin/plugin.json`**, and the **docs/example** block in **`docs/add-a-plugin.md`** (see `release-please-config.json` → `extra-files`). CI runs **`scripts/verify-repo-versions.py`** to ensure `version.txt` matches those files. It does **not** bump per-skill versions; skills under `skills/` ship as part of that plugin release. **Change significance** (for commit messages and changelog tone; not separate skill semver in this repo): diff --git a/README.md b/README.md index 444177b..a589167 100644 --- a/README.md +++ b/README.md @@ -35,11 +35,9 @@ Pick **one** path. Official paths may require the plugin to be listed in each ma ```bash /plugin marketplace add Keeper-Security/agent-kit -/plugin install keeper-security@keeper-security +/plugin install keeper-secrets@keeper-security ``` -You can also install individual plugins if your marketplace lists them separately (e.g. `keeper-secrets@keeper-security`, `keeper-admin@keeper-security`, `keeper-setup@keeper-security`). - ### Any agent (Vercel Skills CLI) ```bash @@ -67,14 +65,13 @@ cd agent-kit | Claude Code | `~/.claude/skills/` | | Cursor | `~/.cursor/skills/` | | Codex | `~/.codex/skills/` | -| GitHub Copilot | `~/.github-copilot/extensions/skills/` | -| Windsurf / Roo Code | `~/.windsurf/skills/` (adjust per product docs) | +| GitHub Copilot | `~/.github/skills/` | ```bash -mkdir -p ~/.claude/skills # example: Claude Code +# example: Claude Code +mkdir -p ~/.claude/skills cp -r skills/keeper-secrets skills/keeper-admin skills/keeper-setup ~/.claude/skills/ ls ~/.claude/skills/ -# expect: keeper-secrets keeper-admin keeper-setup ``` Repeat for each agent you use; paths differ per product. @@ -93,20 +90,6 @@ Remove the skill directories from that agent’s `skills` folder, or with Vercel --- -## Repository layout - -- `.claude-plugin/` - Claude Code plugin and marketplace metadata -- `skills/` - `keeper-secrets`, `keeper-admin`, `keeper-setup` plus reference markdown -- `TEST_PROMPTS.md` - example prompts to check that agents load the right skill -- `Taskfile.yaml` - dev tasks (`task fmt`, `task install`, `task act` - see [`.github/ACT.md`](.github/ACT.md)) -- `CHANGELOG.md` - release history -- `version.txt` - single-line semver for the repo; **updated by release-please** when you merge its release PR (do not hand-edit for normal releases - see [CONTRIBUTING.md - Releases and changelog](CONTRIBUTING.md#releases-and-changelog)) -- `.github/workflows/` - `validate-plugin.yml` (manifests, frontmatter, references) and `test-skills.yml` (skill trigger patterns) - -CI validates manifests, skill frontmatter, and documentation structure on pushes and PRs. - ---- - ## Documentation **Keeper (install, configure, command reference):** @@ -115,8 +98,6 @@ CI validates manifests, skill frontmatter, and documentation structure on pushes - [Commander CLI](https://docs.keeper.io/en/keeperpam/commander-cli/overview) - install, shell, admin commands - [Keeper notation](https://docs.keeper.io/en/keeperpam/secrets-manager/about/keeper-notation) - `keeper://` references for secrets -**This repo:** After the CLIs are installed and authenticated, use each skill’s `SKILL.md` and `skills/*/references/*.md` for agent-oriented patterns (e.g. `ksm exec`, Docker/Kubernetes, CI, Commander admin). - --- ## Security @@ -128,24 +109,6 @@ The **[Keeper Security](https://keepersecurity.com)** agent kit is documentation --- -## Supported agents - -Works with many agents via the Vercel Skills CLI (Claude Code, Cursor, Codex, Copilot, Windsurf, Roo Code, Gemini CLI, and others-see upstream docs for the current list). - ---- - -## Troubleshooting (short) - -| Issue | What to try | -| --- | --- | -| Agent ignores Keeper skills | Confirm `skills/keeper-*/SKILL.md` exists under the right `~/.…/skills/` path; restart the agent | -| `ksm: command not found` | Install the KSM CLI per [KSM CLI docs](https://docs.keeper.io/en/keeperpam/secrets-manager/overview); ensure your shell `PATH` includes the install location | -| `keeper: command not found` | Install Commander per [Commander CLI docs](https://docs.keeper.io/en/keeperpam/commander-cli/overview) | -| Keyring / profile issues | See KSM CLI docs for keyring and profile options (including file-based config where appropriate) | -| Skills in one agent only | Install or copy skills per agent, or use `npx skills add` per environment | - ---- - ## Contributing We welcome issues and pull requests. diff --git a/Taskfile.yaml b/Taskfile.yaml index adeed1b..0d14c1f 100644 --- a/Taskfile.yaml +++ b/Taskfile.yaml @@ -25,3 +25,8 @@ tasks: - if ! command -v cargo-binstall >/dev/null 2>&1; then curl -L --proto '=https' --tlsv1.2 -sSf https://raw.githubusercontent.com/cargo-bins/cargo-binstall/main/install-from-binstall-release.sh | bash; fi - cargo binstall rumdl prek - if [ -z "${SKIP_PREK_INSTALL:-}" ]; then prek install; else echo "Skipping prek install"; fi + + validate: + desc: Run full validation on plugins + cmds: + - ./scripts/validate-plugins.sh diff --git a/plugins/keeper-admin/.claude-plugin/plugin.json b/plugins/keeper-admin/.claude-plugin/plugin.json new file mode 100644 index 0000000..aaeb5bb --- /dev/null +++ b/plugins/keeper-admin/.claude-plugin/plugin.json @@ -0,0 +1,22 @@ +{ + "name": "keeper-admin", + "description": "Manage Keeper Vault, enterprise administration, PAM, and privileged access using Keeper Commander CLI (keeper).", + "version": "0.1.0", + "author": { + "name": "Keeper Security", + "email": "commanders@keepersecurity.com" + }, + "license": "Apache-2.0", + "keywords": [ + "admin", + "vault", + "pam", + "enterprise", + "keeper", + "commander", + "sso", + "rotation", + "privileged-access" + ], + "repository": "https://github.com/Keeper-Security/agent-kit" +} diff --git a/plugins/keeper-admin/.cursor-plugin/plugin.json b/plugins/keeper-admin/.cursor-plugin/plugin.json new file mode 100644 index 0000000..052b4a6 --- /dev/null +++ b/plugins/keeper-admin/.cursor-plugin/plugin.json @@ -0,0 +1,23 @@ +{ + "name": "keeper-admin", + "description": "Manage Keeper Vault, enterprise administration, PAM, and privileged access using Keeper Commander CLI (keeper).", + "version": "0.1.0", + "author": { + "name": "Keeper Security", + "email": "commanders@keepersecurity.com" + }, + "license": "Apache-2.0", + "keywords": [ + "admin", + "vault", + "pam", + "enterprise", + "keeper", + "commander", + "sso", + "rotation", + "privileged-access" + ], + "repository": "https://github.com/Keeper-Security/agent-kit", + "logo": "assets/logo.png" +} diff --git a/plugins/keeper-admin/assets/logo.png b/plugins/keeper-admin/assets/logo.png new file mode 100644 index 0000000000000000000000000000000000000000..d4d6f4f64bf91681d59208b0fefd119446ac06e4 GIT binary patch literal 24935 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 literal 0 HcmV?d00001 diff --git a/skills/keeper-admin/SKILL.md b/plugins/keeper-admin/skills/keeper-admin/SKILL.md similarity index 100% rename from skills/keeper-admin/SKILL.md rename to plugins/keeper-admin/skills/keeper-admin/SKILL.md diff --git a/skills/keeper-admin/references/commander-commands.md b/plugins/keeper-admin/skills/keeper-admin/references/commander-commands.md similarity index 100% rename from skills/keeper-admin/references/commander-commands.md rename to plugins/keeper-admin/skills/keeper-admin/references/commander-commands.md diff --git a/skills/keeper-admin/references/enterprise-mgmt.md b/plugins/keeper-admin/skills/keeper-admin/references/enterprise-mgmt.md similarity index 100% rename from skills/keeper-admin/references/enterprise-mgmt.md rename to plugins/keeper-admin/skills/keeper-admin/references/enterprise-mgmt.md diff --git a/skills/keeper-admin/references/pam-commands.md b/plugins/keeper-admin/skills/keeper-admin/references/pam-commands.md similarity index 100% rename from skills/keeper-admin/references/pam-commands.md rename to plugins/keeper-admin/skills/keeper-admin/references/pam-commands.md diff --git a/skills/keeper-admin/references/rotation-commands.md b/plugins/keeper-admin/skills/keeper-admin/references/rotation-commands.md similarity index 100% rename from skills/keeper-admin/references/rotation-commands.md rename to plugins/keeper-admin/skills/keeper-admin/references/rotation-commands.md diff --git a/plugins/keeper-secrets/.claude-plugin/plugin.json b/plugins/keeper-secrets/.claude-plugin/plugin.json new file mode 100644 index 0000000..c093763 --- /dev/null +++ b/plugins/keeper-secrets/.claude-plugin/plugin.json @@ -0,0 +1,24 @@ +{ + "name": "keeper-secrets", + "version": "0.1.0", + "description": "Retrieve, inject, and manage secrets from Keeper Vault using KSM CLI (ksm) for developer workflows, including ksm exec, interpolate, and Keeper notation.", + "author": { + "name": "Keeper Security", + "email": "commanders@keepersecurity.com" + }, + "license": "Apache-2.0", + "keywords": [ + "secrets", + "vault", + "credentials", + "ksm", + "devops", + "keeper", + "keeper-secrets-manager", + "non-human-identity", + "ci-cd", + "docker", + "kubernetes" + ], + "repository": "https://github.com/Keeper-Security/agent-kit" +} diff --git a/plugins/keeper-secrets/.cursor-plugin/plugin.json b/plugins/keeper-secrets/.cursor-plugin/plugin.json new file mode 100644 index 0000000..5e05db5 --- /dev/null +++ b/plugins/keeper-secrets/.cursor-plugin/plugin.json @@ -0,0 +1,25 @@ +{ + "name": "keeper-secrets", + "version": "0.1.0", + "description": "Retrieve, inject, and manage secrets from Keeper Vault using KSM CLI (ksm) for developer workflows, including ksm exec, interpolate, and Keeper notation.", + "author": { + "name": "Keeper Security", + "email": "commanders@keepersecurity.com" + }, + "license": "Apache-2.0", + "keywords": [ + "secrets", + "vault", + "credentials", + "ksm", + "devops", + "keeper", + "keeper-secrets-manager", + "non-human-identity", + "ci-cd", + "docker", + "kubernetes" + ], + "repository": "https://github.com/Keeper-Security/agent-kit", + "logo": "assets/logo.png" +} diff --git a/plugins/keeper-secrets/assets/logo.png b/plugins/keeper-secrets/assets/logo.png new file mode 100644 index 0000000000000000000000000000000000000000..d4d6f4f64bf91681d59208b0fefd119446ac06e4 GIT binary patch literal 24935 zcmeEuWmi;v*zXkGA=04IsdNdWAl;~RC?F*Q(mkUBqJVVAC>_$BqjV!(GDz2u(sef8 z&vQP&d41NJwMKCN|K9+dT!IP^Hww9R6TG|u;q4SAdhhkbY=-N?zpda<#>ATdFk65-?kdq zGbp-=jB5e66h{RXBG|8?G$pYaya?=DQowf50)UZh*$dilf|1eeh%-^7)EIIj4k^zI zTF!bes6)-g#Z>Px=Zj^(CVsJ(u(p?Qk*nGE%7uE|5f=aX{VpaOy@i7ouRwAOBda!v zbrS6X^|vC|X81NJ2yD;#7&6_RVa#G{fS2cSU)yYMNv$`<_s?Uvte(#i!u$1YwTg8u zi>fd-zL==@l0--q49H3It1J0^dm1H(3Iz%pNzK8m09mIP5)fZAk$7zLgFDk<9p z!n@nn%HU6%v;SM0gFI7$waTA6DF~bvj#MPYPWRKFWDU$eFe(Vx{$m6w z+-z;nq=bN)55Mvaex<~%JY#4c>%m|8R3(Bh2wVn%xRZDJ6^ODJq)rUmwuP(Lgk?9| zybu5j0yP4gG+fAU`(lCaMDu>@yI+bRv3zedSgcbRBUDbOI7EqAOztXi=#}rGzmPgCV6AXmu`bFd zgancBcOU@82re7^0eYquZdQ>L`<$VsdWc#>lMn`8Lpz?`}Z5|37BU@SC z=|0+ZDVv56v*8$C-`Bt_rrZfFC=^E7g_&Q1Y8~$DQ?nt%LJZUaRBjJf8mr(OQW)Nv zyT=rWEkMt09aiAAJC(DS$~Pj{U9Ah08IhrS>jnytWAnUImsOhd%*}`YVc!j z+I<@5-WYWyMn1!Mnp*&&I#Da`kUOZT3#b!#AWHJ;qOXAkUymkQ95Q1S_^yUs5#<_K z%-Y4ElFlT0^ElP&;qYB&6Zh^V57(C?Oigy&Y!>uj(?_TjYIi1(dWvJR$7l%b2=H7M zS{xT`tdQG1`%UK!ppe+bdjI#yK?O^2I1|!59$NE|q*0)k09eig?3+^^w(8>^N8;sU zP7oX46+lQZscLa3g_>{`@?mG7(osf>0CH4!pcp)?yD*vmv6;1qnQ$d9YSx22FTtyq z12^vG=#9O!<^yyPy^rhmTDg_Yq%@TVUC*q3PZLsDwquW2`H{F34QXXyV?}-NH z9<)ylbEdr%0c!}$oLQnwonFz1)PH5vjUYwHhuhgd^D)7lXUd<+53HbO?lzIup^HW)uJeF82uPDAkJj{*nH7x z)k)P@o6HwKFSBScOji1gSVx(a54hXUyPGa`x7f_DNAFNh{^d$I^!8&Y-SB2Zsl7a; z8@5r&;3g!1-EeV8;h|tjNfy>rf>ffIJ|yj7%dJvxhTVid2fblVl%Dt*-@0)2AWabL z9iSDg%E`F_9OmScG3=p-eN?)pwtk1jK?3XyPHn?}C4KW@mx?;#@RP4T+G| zJA1t@oHIxdEN6x5F~Ob;_R`TJLDsFYWGpHF3vkB+6G zmqRk+8ua?)J?W41wzpRb84vu&|0ZQd!p(qitBd#1mqEEEfzlO!RIB6_KlY$@>fq^N zRk@^QARV2=slA0c9TOL)uiUJt!AhMCltFytjxuhsK<1c@X2wH*TyC~V0YGKfqYqAG zOh>MdTG4o4dW}sVe0c=0hokhRsPh+C9aEsB_Ld0hty?@~x8XLVSA+N256?9MEI@Ya<%j1ukS2EUGvN{(nMQ{y+@;oSZ zhj`InT9n^18nS3jr6FYBpYgqtiP?V*Y;)^oo3G2KWv#wYr-y98b&;#A2A5sX-JSJPJ|tqj3|9#1^FaTAu$&L-oi1B!BX2O; zj>_Ky;3X9}0<@&ixw!M`c3jm=xk;hNvw=fjKL!K+LzNNGWp3tkxu#cXXGrcMZ|GSM z7aFI0ctJ#kwep3f#ghuw5PWAC`fCSq0nch0VUd?Hq-^pOFDQ07621g;Q-x=|@Q*)C z0?hIiz$GE7y^EdPdg$UAIOuqf5mu-Li69@cG0a%!bn?i0T%>3L*e)!1c>5h;uhhbN zG%8z-i1c*kcl2mFfM}>pu&FV7x{LofHya9ga96ggi@(_okBYCF2zh16Hmb*@NQmzN*b}BG&NU`$rW>zRG})Qj!HWP zRsF~j*3NQs1G~*PI<=#9u3ap|5~d8xYGAnqv9}y!-xds z>-|^H-i-O6bX_ev{3`x_6JsM6L$>^EhAS@@lIR$J6AM~k2Fx&`%5IquJv(7#>f|EK zTkX#4Jj#wX)&m)be@n8;kIRRx@WGNXgcp0c`SdGA3DcBS-1f#?Z-9_)UfXv(57I{u zrmnE)+MZ28D4Atb%@1Ka3#{&^)%&M^%FfYGF+I1+Dfm(xoVtUdHEg#D_TV^0oPeA-lJ9um@EUy9do*p> zo%S9@Z%!wOwIlWuYi+i>UnOeqI(nqN#R2%Vg`Kr|}uPP_%To3KhoAYCIB@e5^piq_ogd1uz znk(_bqC}5T=Ah(dQTr zotaFrkD3UL@6`>(r0y&Y`hCkSZfn>v**7^y&!9eA*62j7Ee3k`({#(<KZj3O4Z$um&R3e9|4`uwha4B$ZUTI=f#AaEyl7s6sJ%KjBAWTh8nKa)qij z8(OHD?XnSOMzT!;Di$s%i=7myWvExSKUt=Li)nZrX~j1^}0vDbWW;S{3r=6ipO*{ z1`h3CFrZ)D2B}4dvE!eet;gnysW?(4;OEE-HoLBY+B#*kc^x1TgVv{!{lBibpPlTy zKg4eIZ6f(iI1))^)2Z%*nIe6hc$`1NitPJT4a#lnBxqN=chB&S)Cuw&bs5^N9=-T| z>qmkb9;;UlG9iy!BA2AhH7$_*K8FU>-i){iL#trN_Z~KU4<-NCKLld)S)M-5WLEFp z7F$vzzqFlwI;iTIIs@YvQ*_NH;0765h6cwqBwjVbCS(5y5_28Iv&9n#7*Qy$^Toj4 za-dxI``HDa#GEMbCu`OYpCV+TVjrh@stjl0T^Hlupcc*y>tdy93#bv z=zK1Ho>S{DiYl`{viLshDS9=q%5pm<&H>rVE-;@|<^k*2m;>84)W$iRc&fjh$c9ET z<{CttJ^D7U@q0{=hV`uuQ}B1sAXC>&m$C)-1I_tP^CgYyBuOWXDcf{&R2E(J3!6t( zruMB|D*!xN*v|OzF@&y}&BhiHtj(@H6q?|=0tLY!oT%fmkH<(ON9!wp*ZKC4W28hcN zn!MG|5hOlyeGhA6zDH28olxg7;yLovI7ck}YG7x9ixW z&mGEQC>^liG&m;FX2`9Mc!>+pfm5J*LrrU!bH6 z*N>BtrIauX{&nbSyu4OWPO=BHXnFZ+0`8bPEmh?mdjiB&A6rOv56KM$jk`ck7_}dT z*U}%npk%Npd+3Q`?<;w?Ei+Y2wwn>sKOxFVA_$<-0Yuwmd~Sx-esgq1XW=G!ToI%7 zZfUHF|7=KrCsKXrM<9Tj-9%3<}S!+&c`lHL}!x?V2(in7L0^}AOkAFvFTE^tDt6-8= zh75kKSvhoLN9@zIY!x#01n_hpL;b(EA~@T9YyM7!hZ=1p_+LU%0i5szPn`=6l-^^f z00CocHLe>|(|6r!x0lV=v6-*GKYHv`Ld+KykR6;%JiCUKq&GU}y6 zRjEV&{A4yCUcqT;f<`y--8GW!6#|`>iE9P-u-6Sj2+k^=5%vt7gi)ZgZ5mhvv#NC*+1PJVmAfxZ8@YCkSdLj1 zK05CUu|?iP)_t~0GMBd49A_jGKI`SVg|wzZ+#`En)Q^rGL7@Sc;D`H?m-`nv3QA70 z8pi*C=)n;C-RVYRf=Lay>&l=v4%f5UZJ(?H{}-W7^G84Ob=9uDfVjFs?W~VEp?gMo zEQ@Rvc~iopTKi2vd(EoN+LA#&`n;e&>w^}McXWjnGP9`-Nx81cS@rRy!%|sy(yx{(p7V*_nlbY zw0c!qtTVtz{HUGXIW#i+aUgJ}0fr(JvtE*5+0$j#1p^W1!agQwE(&aaK+L*>ep&cO(d zW;!2Ey?3jlC@J59>Zgu8>|Vfg?`%G~kb$094LSg25ukF#figZ05ICoFi)s^}{*lC2 zAM$wi+y8wU^2*1@bbgBpA`sgi1tO4mgfgK6P=aLYYkBA~#r`mjPR_NiPYYtja({Ryu%KG|6lCUe0H7f*Cya&kc_>haOVr z^lPFpjdT+0_7*8&_-#O>HP)f~?jB$ltnpP=;)NbIcu#3R1N9~6VDv{>wERR$OH9VV z@t0IK0xu&dgV=UN;JDvBp*Qr4GzZ+VXxd2iK7ehoRthII+>$0qnOo!wImk`XFm6}LQA z+c&`1;=H*tyf-*h{Q8pbBi@-Eg?2ODZa?PWvRmYFQ!nz6Ax2WD(QPt~dA7s*Csdt>sR}~hLA?4#3t6y~&`g3>%rj@IHfp2`AqEYCW?DQGWB@Zhv z%xX-!nxi1Q6F~J6(mc}#U8c136_ljr-dpZqPkyT6_<;Q(pliePIShKU{D$RRfSO!U z)|x~w`yI+?Kixuhe{22Qx~$Qp$Y7Bf27>W-c7J?V%=S@jl*Y9J^>=Lqto(jvw_!P$ zH?kvC=B6x60D(H;q&LVz5bliA7Qf(4h1&^i)}A4`SyH!h-Q!T`Lq%l=IVNn^PiXlTh z&js73aOd9?dFchiZaF2>UF&}6XjMUjA@$oPG3TR&b^+F}_(7yHO3_Ekl>tw`g?>;B zpdB0YXo;A%s_Ks&1@ufAslP~-6e5LVA`=UU8)f|K%2UJ{9q>wD1%nQ0=BL9K#C2;5 z7=8OI4e^G9I%JI*Y@f;9O$8h(!%MvJ;;Ph~aO}@l;L0lGwb@L&SC%WCm#;CTCIKu% zd+BUEw-)Zy_i5KqMHq=N$~%g%djo8awi+{8x#VSvT>2ps_K#O3y`)~3Y&(tXBLSOM z&DXYuq~cF=xEA8w84x2SI38`}JT z`GXAK->JE&eaC~b897}O!pJv5_LNm*s7#Q3ArRyM7l!3lhcAO?^b;3VeCsM9y zA{9ivfDPaRFr#jU>##rY&~J5&vp@jD*qW`YI9BCzX&w4tkx;ki#olUr4PhPp`se2 zl$fZMXVQ#&orO`11n!O`v2)^`@+a)Tk?k1) z7?Z+5z}5TL(+BF6kC65aW5IgmoM;Nf))Z>+nWSHL2>?Vz9L|kae~sDFt{++V7k7vm zVgl`)ROBuYQ$8y>rq>DIFp`mCGON$OLL*5shz4P*dssOJsf$F|7KiiLDLU2$c{ix9 zxYNi`&s;5sL$T4aI=$QWSTPV~)W*v@8eGVqYjsnb8#mcSi%$9d_yTkhia2(+97Drg za(0%Eu_lHAv+8rnw++G>r25O@iblz4jDsS*m(g9J(Km%&LubiiDeXEx8gq%=U!;bz z3U|OP1k{m^mboN2#WI5F5nP5Vf!!ajM=X(2SZwMJrYfBz0*6Me_}zQU)K`*Oj3}|R za!z0)U!fl(HHnU_<=T?@UjQpo42&NA3N%4d?`o?^p|Xc5#w34k*{}Nr&NQ%lVINf- zsw*}yQa z^L-~a?T+2Fi}9j_t@n-yeN08mqd6biHQtfDoUN~@dm#H6?gl{krP5@?Zd+J$MXCor zl+LQT>bI6N*yh1tf1MwOFE=Ues=3Go&U?&FewjE z^_%_;d|z1yt~zR((546OhS}2|ReQ04fF7SD;4nuQ1?J>}e?R%n82GGp*rAlcHB|C> ztocp&|AYuC*F;(=F;-MFq;CJRu%WRa!xI|8G@?ySfqg3&w(Ukd2y_t*Q{H3CP3L%w{({ouD!)=w?uH^b z{7wUCOPzEo94-&}2$W;|t`7J!b_9*3%G!2b6O@lidN3$BHctDf0_`0{%g%>%p%2y# zucPp<_1M?j8Dbnu2o5=sqxi(NX5UtK9{3V!mz2UsyE} zRm)c8-aF4cu(^(t!^HC4QCljQG{uCcP$3muX(mm{>~~0!(;-R~><+%(DYDD96`030 zpIqlw1T6a4&S#DVTr7OL2N3N!Qgf~eV759T!ya`JE zvUh5PH=QWK8kYsvehJTpe~reGy8h5@P}yQ%yx8gSyH4s%!M+@!+lLaM13}*Cdz&vJ+FG~HBjp5wsP9uwB z^{MnGd7IV_6M~~$tWqJ0Ra?=#V=E|hAh5M#t)!n9sJ&Tf!4FR5{t{|m^T465jx_yQ zC%v|)iz~5-m!7}JRtmAlrj4E;x6@DI`=?}Z5<^9+2A8s^hwdE6RD9d}Z(I#ST`P({ z*O>Zz_8+wlz#*D6i+OlhGj_CN81q+dPYM@K6+Bv;EZZnCFsi)Z5`&Dd>2Ps3ive6b zCxkaJ#$vXY{odSuYJ||WblOg!Ib-4odqc*>MphnnLe^rDgR?N}c165Z*NV zHXc@GYmad_gT7zJW;}+lS&)L$cAWef?*9>XbyL-k@h!o9vopUP8Tj0!l5D6MJ;6RY z7Xh*r<6>B;uHFF|g(6?9W{fTN4l1+CLU_kPAyw5aH<+!WbvkjnnU)D$FA3b_4l)w^ zIOxpgYwh1)%Syo8;UB$4CldiM=QTu%N&NJ@&F^~Q)(dm~*?r!wnUXi@ZqsERjpRX( zT7#Qe!+ac)%%nhgaZpmOTQtKKJ(r7GhF^!6hn54V0Z${0Z@U_J{dpl#Z8ve?a)wXC zn(4wXX-94-=JaZ7TJ;=)rShW|WJ07f^&NRkyovnUPJ}c(@NZ-{w4uBogO2|WA4x$- z<0j{u^B5gs@9N!#BK#RmFd2k+55+9$QSt>gbuNtwr0CP!nD|~nO5ZY>$LDILK|H{1y5l|7(w-{^@N8ucbt{g z5c&QiHeJFhtjZ9n9yz=EtRHcQmumc&*M+)MZD&_{U&MT2B3(yn)qGg0N~c{7U<>u| zs|+D%-dx9(|4)Bkgn_vy0YLJOc6Tsm>vHwr@n&(@fg;#it$xPLJKwNF1m4*gYq454 zw+yy@`^}IfTk+LJF8uTMtMw<2%4i!%iZ#xFw>Wgi-&!sxz!Yh0)!)%#9x+BwchBHvA~e+L$?!Wgk~Q1ehSY~VQXg4kJ?pY)W| zRo@na^8cWvS8ApKc8gOZ_i;)AQ!^uoHcxYNza6~b!e-e!xOW{Tom)qe196`Wh5fN=oycAb)0|03y zCg77pevhk1zo%pmZ_P=7NH$^azNL(SmZlJ_`2`OCfG;wiP!gy$3ooob5^e&ktdP`O>E2%5eyUeC%b{CSd~)Z~^4t;Wsl)l2c3@fr`rEQ^^|(?w@j*N% z5({E_zox1rx)N_fV<$u52vb3)DH#CBD~!S$Q;t%6Cu&4EB0={4iy z!kwK9-sX&H8(YkudWjuS0C)uV$X8)1tMxX+^NmVukbVlXBrqd<0nfC*7~6-eJ-c*EL~y?I_G zTc5rEqkQRY4_T5!!Ucw&E6-PCjq5Svf;{F=B@`u@b|7imD#1+j&$j?QD>pI6ye(=T zhatxS<8R)7-PM-k91b+sn@SMO(Sod`Ebwzuth@%1|KkWLnp>&Ub!4{yNS-ciknHJ0Utj5udBaLTE4KRUyFJ5gVggvK^JuZf=J+s3pnu+YA`@vXY!98T6sPT=EaCE@@oKnBt5D%#M zm^ccJe+F3d9D+^mnkjEQ-O=wGr)Ps)Y%>vxG~Y>=QcsPZRJqT?iC%7Qo1#VdFtfRQ zT-{rI1|$H(cYp){eXCqX(~ReP+;_^Yi8GJ+@b10me~>Rii9^5B2bYBO`iEAK!pBtq zU}zg5H+Gs#Yv;m-KQirNKs5IN)7_-$Yw=xz~BKbBng^ds+qLUVppcDH2&Ndy& zKlF%O;8P4Ky6zC0ZsWSAXql#r=7b0;FC^dIj`X{tF*fM`fw|CwtdyMMaIfHVr}J_`W6_bM~ zgn$3d+ZC<%r!!-+?J9UEza$I#wCo$GfD6IgyfJ@oBmMc)cP4xS4yd#>U*3){R?it< zVH(lRfvoTmn+fWlQ*$yQhR<;Wp`{8ps@C-hJ!FO`-mCH6bOA8J)v0Uxj1=)^%d(L1 zDd}Wj%Y2n*l~&Y=;&ImfV~}%+j^RDZ`(f~3KhYC{AWadBifLuWgSyEfBG0dgl?9IY@ASpD`N}^lVca?Nb_b3-#<#T*el~a3Iv~v=CP{MStFXU{ z`D-w#PgrwdA7>pHw94N5@NeYH!^66}ttdS6hXuk<;38QjQgRqe^BL}`CIGxJnO1)ZH(4mxD;;sQ+Mc=qi zk;T#JC&RYE7s3;r?al3ycqoOkzW$Duf71!a=K2t`e_J4~yj>*9NL>oEV58Ra-4}R=8$NTA!RZLE2^x<2Gy~HOqYRMqj zH`dZ;9{VF$7xxgEl;yWmF&3q-pS)ImDULO+{G!ub{85h-o?X7zPgeIO;kDRj0#&o5CU6JQWb+v+T(N9eoeat7R*O*9u(p`+1lN3d*_1{$zLQ0dwgM zXXm^y1>~X3>Hwq+%5O7BqrVw={H-uF+lHFF)+uM7IHS#FMChJRUvO!A{K*WU8TomTTv8>1(49 zl~-~EN3Pii6Y*}}zqmNS5QQM!8ZnvofYH+eLxy(dGiK;6vhbU>-n1C5*%mibj$TQk z5YD^-%VI1vGQ$|0dsw>921Lsz1VtG9)JcLw_LQ?pJ~B0rp9}(#rvZ4_G6(-KV^W0W zTvERxq8>1CHZ6DnDr>^SKy3!Y4GQgS-r$zeTjc~SD`iJR3MT$wpaXe?r zft|rrK-ha-tA)HL00?4&y6!W^8TdZ$1PJ5CL$~3jN&#_NBwQ1Ar7MVd40%BA+yfp0 z(f@1JOg@I^b{nS>_AwDq^S}N)?z>UXH(+&`6A=3wNzjZ*{g}aR;3#u^ zMuF44gzo4%`e$3;%Ob2&?>-9q(nwk>RZ!MnCfmV}XA#mwMkWe09ClOC&Cw#O=P!V? zW3vD2%sq6rKf;l8+HzuJDHd8f_F$*)jW8Swz>O|DD)g0E!|k;R(J|qEsem@=&}SqJ zd}d>jw{-q#DQIHFro0|}N(ab$1|}jfAFqZyW~pgN7hVD1N(LV!OqqT$t?~o})B|?z zm!3K7Slj{Rctx(wIp0^0{^?Tg{)WJJv*}SZ1Jg8N;z>{&&ZKd8pa`(yD|AMeyJRt;|tFC$n z@l{k9`pDs>1`=7gast}fN&!mA;M&jx000N+C&YOZw4jZ##oHeS5%#yC4&sxBWGQJ0 zMIWV?K^NT=h@$DljGHDH9KHxCej|_x-secudQLRSbC3X9W|P?MG~X&j0ve)6icUT} z`mH^hc!RRKceG15N}opK#_`7V7`|d^@D|oC#)=7bsxl^(Oj6zAG2SDrrH)ds6HB_q%w} z>l2U^{=}c||5$5K-Lca7PeUW&_OPRPG$N!JFZfs!D>D*twPZX`-hI3D=dO;)f5_6_ zlI&rucjH$n@e>fZcxJ=+Cx5I_7C)RzZoOJR^Z1?wZArhi@@)Tk^?&5OTlRJ^N!Pf+ zxN6G0XY6H}7IB}d$3c?<#`O~o5L&kGFY3fZ&i`G7>mG<>PUJ{7VifQ?XbwpBC#b8H zX46!+m|atDaoRape~yzB`3Tqa*)Q1+_U;{B>rvlP|GeYj2M-7dXdlDH$)_sY$*uDg zi(!OVD0QhfJjH=3-tE^Fb8 zxti+Dv^&3hk_x(gPP590ST5|Vdy!87SkG@5u4;8tJrj~7Wvf_e?=HGV5aP%%WBP?z z3w--`QsPEx4}HQPv!lr}Ix`KRlY(^?u)c1`Z{f1m<~u#pY|G?aD)k zzU{%+DSw#=hZW}O<@`vt+!6*DDzQg~bZ;Pp(c_&c3@{Ms!Q4BR6R?VoHpgXoA zN*7%Nwsbn}at&R&hb-FgqNdM_N9eB5FR#Kdl(6}~>-_Z6@kD^w0bhk2us(SjBo z7f%O-b|!_+!E#?M68|(kS>5si_1i_mRSYn zU`14b%i4tn_n4eLz_M9?Z+8JNK<9r7Th%QV!e1B)*(T>{*1NkRFaFr&_=kR01H~oA zkRqOM1v(rC{$pyJl0{A%bWIi_*Y{Z~gtgej6tWo~DzA5lVpSz74s$YtsnQS?Hx{YB ztrK(^2cOuzc&^bs_z&HUfFV`q$}pIW^pEZQ?#ryqOBdKsSi_iZu zP4q-XYcs!r>`fNNriF@eGr*otGTqjOzlDX>v8=he?;nybb}=V_`5A(B_S3EFXA>kg zL5=6j=_K7+|4!0`2x`QBe{CIyc53O&e==A6y#ar23hZ_Mr!g*Odn#NDc4kIpHe~*) z$z)qC)g8p4mGGy!MM#|e8kp~3MS2nR8M2;2tXPmcd=W(CEFg`nd+BoYTsksR>2bWE zMfRyLW^#nTWEK9)z~Kzrp4$ZT8~(jMQHU{^n3@UPF_ef_WMI1%A>RX4>re}mAKOjI zL@8VZ3d1Hj+df|RRh^g~`8uCI88!sK@9mHMzW(liDFT`il4gB*mfgfIV9_8cm0h4x z_H+0_3#if|dLt>HsigN(^@+qZYMTEskm?<@>YE} z?WR#Nq4geG(gI)U!|P`BN5uTC>3XWN89Ze!-shy)c>C4*UIMlfdVd+uqM=>hVGqMR zdJ_!u+=$i$Z+^YZV^BKdZEPD(BRG~n>*e2AE8NcCV$dW7^Y}WuaUG3$ktHH(h>v zQ1%8ex?+ko9-VE+?P#|ZnwG{`49kf%v9@_C(rp>_7X1M%q|P*N`TE@zxIlj5F@zsHa#q7f;#cIqhi8vd!E(?%V!YB$sg}qg}5sj(KvJPv#$TK#I;& z@hGVX-*~m^4yaV$y;65x?V3!tZ<$+FmQwF9e;BU*x;{m2q~bm4D%Z#)Y2lLL5FV7?_2 z{99w-)vsCvRWXb?O5853uv(`wbAstoQ7-Z$(aF)1?!62JKRC7f*zCVu%p^bz;$YtK zv!T?Yd;daeLRHSpnMv6p(cH|flj4-dCpFE#=W0PrarRT7$z2MR9yI6U(XE>s0Bffs zW19EAN*XEwraOyo+c|QnJO7^ZorYn`jC<=zKpz`w;|aRJ{Y5OZMiLH5%Ezm&%;ujA zc=Ozbit;y53T8)XqUk!>zYX)omZfyR%Z?UAaqqT&@tA0(#%3JlmF#waPfXrQImOor z|HHs7HfIYvi{`8%N29=1PLHe6#>2xB2(wSno~F0mBkaA(lHDhbE}+Us{)W?kdi#5Q zqT%E)5gt?5fnzMCg56%c%fX^hNdQBswQ-}*q>nr148MdCp+oul@jqnmgSnkgGh^AF zuW`eVt7{B9{2`11cS?csr31y6?dC%Ypv$4vP04DU(BF0-0hBoB$-v=65?F(7D%PnD zH`Dt5a$E1a<_J=okrnod=pL1G{ERkUJd0Mv`9KrWwUNvI`e=9$tS26}qQW}qotxuV zpu=fvg6`H=^Jw}LMx>ncqS9$d_9y2akkACDodjw1_txLM0w50r(;X_&$pNHGM77Uy zLTY7w*BP?mGt9Y%z2u(L8>4IUD6lWV<&&E7qQ$VSChRd?d3pAbtFiBf&D_B7wz zZ8;)o4)r8OtPGpA&(mVIUB?q(!*@|G;QFY^w^K^2xu#pgJTeP0G$kM&5hzFCU;?Ud ztx9XC`G@%4O>Dq?Z0FZsBw3nEQgt1ESN2__5P^agsxKhabSqi>fw@YQD)0P6qT`GGzpawG|J%I-Ij%j3S#;XI8-yrHhfa${nBTU)Neg6UHiK#qc4vAQH|7+Zczt{2Y|g?tlk0lr7Fo4w73c`}nW;>bkTfJ7j+6Q2?-T1XJB1>Wk88BpT?tqGf0X z07ke3>eJb1C8hq{{=AB8*Rat8RQbo3egjMsE85A@YxG-_->>yxc3(WW6Jlac{8&s| z{%zXpg}taSTy21JTQ>^cqH2PE5#NbpTb`=A3f>9CDw-g=#pHUVQ%Mm++k0W&KM+Yy z?}DqWD?a^#?|n;Z`GWyY>5dqY%K!ll)qrK6t+xGOe?}v?I zu?z?>!q8xWm9kjzJ8%k-@9AZREFuf=l1g|*fk5*l;fDHYJTKk-of(08$8Ey}pvl-i zkev5jKSc{j)(0P2^qLKw_u!+y+(U32UhAZ_0LEbew-8Jk+~jfC%2ma)_iaXeVG{t} zUh}?VXa_z~=b3)FDV)#Llfei+Vc6^UIgx_iG>gRgz6$~{`|9G^jI~DBPm+ndx@_@?g@1X+PVA9@Jy-z>7 z^i|8t7v|iP`(t)=0k@s^b>Inj@A;)ShR;ZGnolUaj1@VE=#qQQ1EyfY(X&s=v$1vPt)QbZlSRniY|Clw^J7qw2#JL9M!9WJ z5*Af$>L{~p4$RtJSx4;|uU;MX*Jv?GI*LHQg6Fm)hU1@qVFg*uK}1X^f-+yAHCFHwY6QRN!E?}ns zW-aN-H@&T|pGkqHhJe9+xRc4ua+mxPzb!=jYZzAa>lIt16u93!@RNGT!wXfJG_4K# zoq%8g#|tw>h=4==`Ks8}*w!nHfuFXb`V7U@ssD|^xptpaPPj_?+5OT#@7+gxXCh*( znjYM+$@yRro|29z#;MT|5wKWGLmzlf=A3Sy%}r}Qmt<@_w|Q~B{E39@GZhnpu}hXc zZDr$7lF{Oqe$Ww^Lk>Fn{Y}*H(>NG}4dwJs$sD&RhB&A?RCC=Fc7=va1|13A1-+pN_#H6)VtXD~aD7S&u#) zaD@^XT#0E{d_p=`=;UXG!J9en-Pg$_@aXoic`r7|G6L?x`9l<)YT2G_Lb@;kz|J`N zWRi&OUpDDQh$?kd6yilW-p4TfNmdLg_>+Q+{Z}z#M{#x=K#kw7#OyhW)8RxK+y#Oo zggnfs*XTTDc}k4kP=m+zU^#|V^Ccu$+2nn<(DNgSJQD#UlCkH7{W(jTfU<3e33+Um zyF7HEe71OpZC(@^Vx6?LKA}2{N{kuH8o`&!nES&xkJrs?`_cp*ejB=)5v|$3?-4O= zKjUf8$ADZK%z;L-6*}NKQ?sf(1qNP=?(TNiz~jctSLTNEn~k0+cuuWU$G71n-sK#H z;F{ca9Lg9s#axX&*NeWHK$p~{Jr1pFFkv^uVO9iuG6}e!szjPlGbySc930r_q|?oK za@V=tdym)X|Fm=6Z%r-T8j?^2=^#y7=paR!pb|xT@4cw>&QYpB5>z}0(xfPeM0yA5 zy{dGH(u*_&snScRcj7tUIo}WWdG0@O^Xy-Co}E22YbI;fTJyedhTJBaqD_^3H&=Qz zjr89aFsu6h>eNTkdvEE{mOV%DQOc{w6W`dbzy6bv`1A*z+Zm`-MwW9KypdcdLdfz( zT2yuqsTjuNsV#vf0Y5LO7b}Q8r3(-Mw)2J6-+)+6sKyhom+9<9`R*r66FN2trFqXS z?QsMmgEe0a%M`xIf#Zn|+eMPm?p&9vU+8xTn!J4W5;Y2w`g844w^hv%V8Nq1N_LPZCjOJmsc4>urr@i^0iGSwbb!Q_Mrd=wr?e}Wr1!i{M-1T zq>$8o3dZKcCGJF?>%7cb<6-0?nYnc_cUKl2;5F+3{}&B;;) zKMk6a{9Lc)8EPGbkF+-*x2hVVl`Nm{w2XW{T1h=Jiny>eCA4v5>wD9C%REqw{~PVl zegS1p7T(}TYn#)itu=}Ze?jl<*wNX-X!3qksiQ@1PG^Jd{1sWMwA_*_0V?+{ z+)LRX$m^7SV4r;JeOtmVaMEh&)}LPgoPoI4csF@pF6_TlTlyQ7fD^w-y?B3dqp+y-ck|#iZK|<#4G`;MCdkAZQp3HX)*rE?r z5j&r2l4V_dN!=3)5>bF{^Syw-v`L3+O<0rSJAGB1>m$KH{Dga*NPcd%&Ul=>=}3Ws zM{*J#lQv4}LssPpSnazpFEV^U;T_(>x81F6Q%jc@sOG8q^tfrlqN*;T5kFo2;mo{m zc+|yTafx%j5($Ti@L*h=rLWk%&F*#vM+Xt9sL^9bs^-UVd@kZvM%70NcB<{zc zlBX&2!~szQ4lGB>itZ32;0t#2vT7xws8DA|CIzFyfhOUDGse9l~8FN35+|(sWA~G_W&24bo2}4j5f6 z6{hchTequhmypf>;{Mg3WN+Os=-kCPi64|*?bR_GdKeHkFlrixGl{~2B`(#Tahv}- z6e+WPFH*=n{;Yn{_HTMj*aDe0eY>b2G`weCt@SpfUgcE$HLV#g^=VIJ&m+wZz@RG6 zoSoZRE&Yd46SnYLKgClM*O|^m-^UGdgd*;;03tn|u%9`ft2Ne<2`?Ufwq?+_>?saC zcZo+J-$AJbKHs@pFQL4uAjd!rv0zk(jL=}&2s>>rQn09|#=3rYpg4^bU2s&)kJtPK zs8HQL8o0a&L-_FGBf<1fco7*kUThtQH!h@)Ek$FefYR7fgX|eFoOi8bLXeBttm%01 zbAXNnNH)8rRD?A%<>9xlS-147GbVJZ-X(0}`7kjaD&3SNG%>cF?|;{XIAJ;GFr$RD z-9<6}=rz*3expfsGCZBm8+Y#)w>jPQ*at|i`;J?1I|j5bEA1qCi|Cj2V8yYf<%DPd z(x#HVRFP4jlNFovuN?#jj9-0=F7aKy$SHib5}ptw!2uPv@N;o zFNxt-2qpSaIDs}V)p?S~NFG~ChAYpoi9rUQO2_gUdwtF$=<8q%sVsQZga)g2&>;ph zDX!31MyKk(;EMOewk5=#l&R%Lu1RhpXQTY zLIs)W9syWpP+scNvMXF;2L-M1K(8>{%%`q&!!iaLcn*xkG_z?TlDF(v%%v-RnjpY6 zyM-~doh>*%HM?C@t>?kb8wB>m45w)Ac%j3D!q+q+#Ji1I{4k+Jq9FhBhVp^lq{h7x z0vY+n(yo1WGRr)fINBS)24AiY)Xq~dZ`beNZ3)NSyA6s4Y8dZZ7$?jl9rJdKX(Agb^p8)ej)oC?wP`iE!1myMH*`#CWRk? z@b$FvCrx@IJv}HTi!Pb(w)K1M;;yt9nOIs5(C*gzf!b}N8VO$KF>P$#TYJV;>V0Fg z=6sk?GL*4bC)l?jwczd_LDF?J*f)eni|!51EI%%I)ig2guj-PtqGYbygbP`}{TWH; z^V6g}b&LmrKJjsFbL-T`ICISw{FdPZ%StIFVC3B9=>;D7SgmCK4sQEVgMd@FzugN2O=S=g~o3A}F_lOIPR>jSK zNizyq_So^}r})73<7jv&X`7lJ`xInnVeRS;R&5;?qcdH5hjR4$fKzX6l#?HAeH0>>kMjF|GhbELuX=pa z@Jt`c?wfz7*Hc`B_j>|vK)L+!E7%D`nH;89F7L8XRvWrsmoPL*5IwKF4#AjHiILtwRm=;hR|^TaSJaS42&}wb-UhC1o?AF%!CW z=&R99oR6g6Y>9~fu|!%?X`Gte zh38C!%^@_kS?|Y(*M$e4q+f(RXOPja|+cPI9kK9#xld3)HO-?rP-qC`YbiMQye@tqSfXR;ixS^u#DOpZQM85 zwS`NwrsfAocVoKBachz)ZP&~p6a6*3G*Gl(57wZD_{Mh&^NrPabb6MHa=!i5CEtMV zNCHeJK5*Y5UP_G}E0=9+YJT}7Dp2XH=Z;7G1Yeat{UhL$%W{h)rA~Jh#woHIvM8qS zzMc=%s|kF3XJv$2@BMC3TG);>-8Q z7*hs(cI)-Tck3Sn5vdI)W3sbJeyy)a2-03=$0FX~k7$G8%kB+TFLWZqI|+sPAgUaf zzeyX`#V;P1;w2C6aS)~$A5VxVeUIblc|1C}9*1hHfu6#@j(@1Rtm(yCkfsVsZL+iS zv(oA9Vm!DDB1s`9I7u_U^qlRB{!jxrnWNycuWyHeOP zKa-iwtF(V6qvW%g-o|w?=;bzveEG&!bTj48rJXi|&_ugl%5tf+ozc6bzY*cDUxzcj2`f(F8G^SLr_XOTk|1bhe z{oan2^eG~A{MkKw+X&M*hoEJ(i22q&Q-W`!?Fqq9^I@f{C;2bt3|fko#&FE+1PYU) zn#IzDs+19W)gButz~{>8dXuYSZm8C(fGOEi+-QrUw$lu+PC^W6Q4hja2g z?#6ReObQyW0|13a-{yB|A}8a(mt_u2ZGik!x*x}>G93{ zd#Xa~Uk@w95Im*y4<1g+)4m%tGc`F>&ExFn;r1TBJFG1ylqMFTnZ)bk9rcW2ooH%M zuu3-P((ppII;S9nfj7xvP7dL}(yO7-BQ+{AK9Eu9>6mTvxGh8Gjt80SzC>-@HiiZ8 zV_UD{jJ}2M=t+pDYsuk$d%kQnJs<>ANjbMFybma%tfQ2w+7Q)+P{nkeP@en65wH_5SxQIBFGEjv zGFo<4ddsurew=oqArjUK(MQ`UKt2E=!Hko%u2v6APr3oGSaQm7(>M*|ZWTmf)GYL( zZ;o|Z@jVDae##V4I1i}ZDS*ea5>}D9vX0??s`M0ZEECiYzpWc_%=tksS6kQDAvUn5 zcNq_FBP6UMO8mf==YcuI|9frM)AT2YSK;+Fyy7eOAA~_~xDx{b#(TV&woq5-5MvQT zDq<@JboYGXB%Q_9n-%LoP!`gu$gzb7-%yb*0a>Bu34-S>z`Gqz?lVhs2xx8tzU#>8 z+%-~@bl7z+%s~Ckouf6w6TBnc06EKPR3M`60czt1KOTc=4LOQhttc4)#9JV5bh(SO z%%!4{p%VJ`a&IlcG%b9V1>o^Uoh;M;O9N@_1!9y0y%_&~O_wF&TkR$lZCN79^Lyby z<;_WY+Ob3sNLssW9#BBMw1_V{lLsp#XA(B@;_=HzBc%i0=FHopPARt`ukoBtttUt) z>ajAKJRbDz8bzK$*ibyapjrg_U39bIh+?%uiwj)@9A?*Za>e6ktiwDYf^kjzy z335E~SE!qXM|~?5np}Vk1)hqy*wV`>?jLvOVsAog?}m>Css={(N&`W%vdcW|WSnkJ z4$#dY-Sd2Mc&Z4yvQ8wGxUPNvr;8XGNk1nBqUZ)1tdl{yc8Cg)srrxjATx|lhfqVB zcp4XvA4wlvuejXZ84+E+&h4AMam{B1Ns5-7Oq|3)y2d-}*RwvOhIVyhYHeC{6(`9qR z3KV9B=RV}I>#{~PRPg3Mp)!%xXl*n}f(a!MmR#qMGnGGKNkq3lJz3iHZy)0-Jr!%Z z@NkVM(_0>f;6h8`(@{D$T}^5|=+HaaO>A%LBo&n5qnx1{AonsAg-_FUj+im@m8y*tybtY5{Xd z?d|a00YwJ66+KJ<4N~DGZR+7cGs(y9=F{c0^i6)U-)#EEtM+6KpbvXPYIecM*)bmH zTK`)ewiR3e?TT#{#1;^GYZ+snd`R$?5hT=9J#0BufAh`~l`ULB*cGGBsSRxL71lXe z&W{OSslA8*+<5T_DgjHOEFqR9vw4rB{5Ah9Gn~(1fe)whyA~ zrQzO2;fLchPBSa1H9b4qY*%p(x_{2Vlo9DlpW=v>UoO~dB9a{`TUeb1I~tpd8O=@< zXJ0OlXONvnYT9S3uz8KOh(MpKwN4Y#e(1cYc;Gm)boPa#cS+3)kvsvU+uXsJ+WGpn z{dx4VxyAQGF(Y@^=>4D_ijBM&FFTeylk+lmXS7}tW%CmF=pXvfhSoWlTRiVs5t|$; znqY%Ok<7WXG*ih`k#qc{dGjTq8#|r^y0v|9w$ksQF`J+20DXD7Od5c^8UR5koGlch zN}wp8^Z)}FlR-7mc$7Qid4j8OT^~oYjR{3pFeq$V@s`~a#jg94BTleeD5-$uS>|sX<5__+zw0_+8eXk?m*@8$y z><17BoI;u0y5#G?RH1}LmjiA@7hb$Bx~#XCJ6%anEB*rIcQWV*wLanxP08Wd<3jg= zrtSIKTVC+#>G-j-2}HME+e4q=qD&Hd0&m+*`i+x4Ajg`^pDhUA$d@Wyh5-}lNM#}u zv3+4w>ScD1x&>{u%KL%rp(~`t3t|(@TB{y*{HjjO^{CN?8oV2JE6(AA29Vzbb}&_&SH=F;gk2&od?CR+ENJE~R;DT%s5 zNzB*7rU+&PAN^M`I)wP~=o!sJkN)JrswB}#e%thPK2&9dNyXa^?luec9{h@rMrQWy zfS3ZWENJCnKH^3NsSiWkCwizk57qFwHg_3y{S={adR|QUDLDSsq!YX)S2pa~aL!+v z)2+7_J}@s4yg>?;orgXo4sPJD9JJkI6%Q?cc%+0@?hWB-J&H%P|5~m93b9^fM)|KU zF*|cx(sOX8(&U?J`NSYTr8c{Nx~gA2JuGmB)0`^gak~S_r=BH62vr6Y?ZAM*pNEAd zVSQ!UA{U|!^4WBJncQ<)sCPe9U1a=`t5n(D%)w!Y1vO4y(0_yWkB>umyua$tQ+Iv3 zL%9mC7MhOifU3+uklk+0o^=Y#sUL{1r=QzZ)*D5GO%f#RCMS)x+OUVpZNs9pK2TMS zukYeNUbCk5KVB|7o1kosiGQQ|bBE-G!;jD@kM`9`C9u41cqnUA@?e(#w3JOSCdwuB zErc!{kZiAeyo!&`uLvP2(@I*_PSp1Q6igA7@++mB!XwVhM#~6FsCILS-EnD7@nk9 zxuaMmP%^7~a`Ayo1NyM1P%Ud&8MY(83hUvD*GxoAtZOCeMcM#6wYlPQu`q#MC*x*l!xRA()E6ARoZa?}%vMB~j%_v3U zE{1NAzfQMJI?V4h05QTE5La$3+0Up3eZXJTq=Z*2$YDGeeuF+hc+`N;vkIH?MDa*7 zaQ$Z=0QwWZ@Bhx9ls)hWNIcAm8q5@=t?M@a23-H>Qv>kT?R7^1EY=-0UC+FyYRmZQJw&8XV3NbknNbjl=@XMRq_6ApA(G$Lni%y6a2pk d{*P5Kdmdoxs=%%?fFl4Ob!DA9CAaOu{s$m&fVuzx literal 0 HcmV?d00001 diff --git a/skills/keeper-secrets/SKILL.md b/plugins/keeper-secrets/skills/keeper-secrets/SKILL.md similarity index 100% rename from skills/keeper-secrets/SKILL.md rename to plugins/keeper-secrets/skills/keeper-secrets/SKILL.md diff --git a/skills/keeper-secrets/references/keeper-notation.md b/plugins/keeper-secrets/skills/keeper-secrets/references/keeper-notation.md similarity index 100% rename from skills/keeper-secrets/references/keeper-notation.md rename to plugins/keeper-secrets/skills/keeper-secrets/references/keeper-notation.md diff --git a/skills/keeper-secrets/references/ksm-commands.md b/plugins/keeper-secrets/skills/keeper-secrets/references/ksm-commands.md similarity index 100% rename from skills/keeper-secrets/references/ksm-commands.md rename to plugins/keeper-secrets/skills/keeper-secrets/references/ksm-commands.md diff --git a/skills/keeper-secrets/references/ksm-exec-patterns.md b/plugins/keeper-secrets/skills/keeper-secrets/references/ksm-exec-patterns.md similarity index 100% rename from skills/keeper-secrets/references/ksm-exec-patterns.md rename to plugins/keeper-secrets/skills/keeper-secrets/references/ksm-exec-patterns.md diff --git a/plugins/keeper-setup/.claude-plugin/plugin.json b/plugins/keeper-setup/.claude-plugin/plugin.json new file mode 100644 index 0000000..055fc47 --- /dev/null +++ b/plugins/keeper-setup/.claude-plugin/plugin.json @@ -0,0 +1,22 @@ +{ + "name": "keeper-setup", + "version": "0.1.0", + "description": "Install and configure Keeper CLI tools (KSM CLI and Commander) for the Keeper Security agent kit, including profiles and authentication.", + "author": { + "name": "Keeper Security", + "email": "commanders@keepersecurity.com" + }, + "license": "Apache-2.0", + "keywords": [ + "setup", + "install", + "configuration", + "keeper", + "ksm", + "commander", + "cli", + "authentication", + "profiles" + ], + "repository": "https://github.com/Keeper-Security/agent-kit" +} diff --git a/plugins/keeper-setup/.cursor-plugin/plugin.json b/plugins/keeper-setup/.cursor-plugin/plugin.json new file mode 100644 index 0000000..43f7cd1 --- /dev/null +++ b/plugins/keeper-setup/.cursor-plugin/plugin.json @@ -0,0 +1,23 @@ +{ + "name": "keeper-setup", + "version": "0.1.0", + "description": "Install and configure Keeper CLI tools (KSM CLI and Commander) for the Keeper Security agent kit, including profiles and authentication.", + "author": { + "name": "Keeper Security", + "email": "commanders@keepersecurity.com" + }, + "license": "Apache-2.0", + "keywords": [ + "setup", + "install", + "configuration", + "keeper", + "ksm", + "commander", + "cli", + "authentication", + "profiles" + ], + "repository": "https://github.com/Keeper-Security/agent-kit", + "logo": "assets/logo.png" +} diff --git a/plugins/keeper-setup/assets/logo.png b/plugins/keeper-setup/assets/logo.png new file mode 100644 index 0000000000000000000000000000000000000000..d4d6f4f64bf91681d59208b0fefd119446ac06e4 GIT binary patch literal 24935 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 literal 0 HcmV?d00001 diff --git a/skills/keeper-setup/SKILL.md b/plugins/keeper-setup/skills/keeper-setup/SKILL.md similarity index 100% rename from skills/keeper-setup/SKILL.md rename to plugins/keeper-setup/skills/keeper-setup/SKILL.md diff --git a/release-please-config.json b/release-please-config.json index a4c8d6b..56f2b95 100644 --- a/release-please-config.json +++ b/release-please-config.json @@ -5,8 +5,38 @@ "changelog-path": "CHANGELOG.md", "release-type": "simple", "extra-files": [ - ".claude-plugin/plugin.json", - ".claude-plugin/marketplace.json" + { + "type": "json", + "path": ".claude-plugin/marketplace.json", + "jsonpath": "$.metadata.version" + }, + { + "type": "json", + "path": ".claude-plugin/marketplace.json", + "jsonpath": "$.plugins[*].version" + }, + { + "type": "json", + "path": ".cursor-plugin/marketplace.json", + "jsonpath": "$.metadata.version" + }, + { + "type": "json", + "path": ".cursor-plugin/marketplace.json", + "jsonpath": "$.plugins[*].version" + }, + { + "type": "json", + "path": "plugins/**/.cursor-plugin/plugin.json", + "glob": true, + "jsonpath": "$.version" + }, + { + "type": "json", + "path": "plugins/**/.claude-plugin/plugin.json", + "glob": true, + "jsonpath": "$.version" + } ] } } diff --git a/scripts/act-all.sh b/scripts/act-all.sh index 137905e..657e1cf 100755 --- a/scripts/act-all.sh +++ b/scripts/act-all.sh @@ -1,7 +1,8 @@ #!/usr/bin/env bash -# Run act against every workflow under .github/workflows (local CI smoke test). +# Run act against GitHub Actions workflows (local smoke test). # -# validate-plugin + test-skills must succeed (same as GitHub push CI). +# Plugin validation runs via pre-commit locally (`task validate`); +# this script runs it first so act matches "full local check" expectations. # semantic-pull-request and release-please call the GitHub API; act usually # cannot complete those jobs locally - we still run them to exercise YAML and # container setup; non-zero exit is expected and does not fail this script. @@ -20,9 +21,8 @@ run_list_push() { act push -W ".github/workflows/${wf}" } -echo "=== Workflows: validate-plugin, test-skills (must pass locally) ===" -run_list_push validate-plugin.yml -run_list_push test-skills.yml +echo "=== Plugin validation (pre-commit plugin-ci / task validate) — must pass locally ===" +task validate echo "" echo "=== Workflow: semantic-pull-request (pull_request; GitHub API required for success) ===" @@ -53,4 +53,4 @@ if [[ "${rp}" -ne 0 ]]; then fi echo "" -echo "=== act run finished (validate-plugin + test-skills passed above) ===" +echo "=== act run finished (plugin validation + act smoke tests above) ===" diff --git a/scripts/validate-plugins.sh b/scripts/validate-plugins.sh new file mode 100755 index 0000000..282559c --- /dev/null +++ b/scripts/validate-plugins.sh @@ -0,0 +1,574 @@ +#!/usr/bin/env bash +# Validate plugins (Claude/Cursor manifests, marketplaces, skills, references), +# skill triggers, section coverage, markdown code-block heuristics, and root docs. +# Requires bash, jq, and standard POSIX utilities (awk, grep, find, sort). +# +# Markdown example checks run in parallel. Override concurrency with VALIDATE_MAX_JOBS +# (default: min(8, CPU count)). +set -euo pipefail + +SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" +ROOT="$(cd "$SCRIPT_DIR/.." && pwd)" +export AGENT_KIT_ROOT="$ROOT" +cd "$ROOT" + +errors=() +add_err() { errors+=("$1"); } + +ok() { printf ' ✓ %s\n' "$1"; } + +if ! command -v jq >/dev/null 2>&1; then + echo "ERROR: jq is required for JSON validation (https://jqlang.org/)" >&2 + exit 2 +fi + +validate_plugin_json() { + local label="$1" json="$2" + local k + for k in name description version author license keywords repository; do + if ! jq -e ".${k} != null" "$json" >/dev/null 2>&1; then + add_err "$label: missing required field '$k'" + fi + done + if ! jq -e '.author | type == "object"' "$json" >/dev/null 2>&1; then + add_err "$label: 'author' must be an object" + else + if ! jq -e '.author.name != null and (.author.name | type == "string")' "$json" >/dev/null 2>&1; then + add_err "$label: author missing 'name'" + fi + if ! jq -e '.author.email != null and (.author.email | type == "string")' "$json" >/dev/null 2>&1; then + add_err "$label: author missing 'email'" + fi + fi + if ! jq -e '.keywords | type == "array"' "$json" >/dev/null 2>&1; then + add_err "$label: 'keywords' must be an array" + fi +} + +validate_cursor_logo() { + local plugin_root="$1" label="$2" json="$3" + if ! jq -e '.logo != null' "$json" >/dev/null 2>&1; then + return 0 + fi + if ! jq -e '.logo | type == "string"' "$json" >/dev/null 2>&1; then + add_err "$label: 'logo' must be a string" + return 0 + fi + local logo + logo=$(jq -r '.logo' "$json") + if [[ ! -f "$plugin_root/$logo" ]]; then + add_err "$label: logo file not found: $plugin_root/$logo" + fi +} + +# SKILL.md: single-line name + description in YAML frontmatter (repo convention) +validate_skill_md() { + local f="$1" rel="$2" + local first fm name_line desc_line name_val body + + first=$(head -n1 "$f" | tr -d '\r') + if [[ "$first" != '---' ]]; then + add_err "$rel: missing opening ---" + return + fi + + fm=$(awk ' + /^---$/ { if (++c == 1) next; if (c == 2) exit } + c == 1 { print } + ' "$f") + + if [[ -z "$fm" ]]; then + add_err "$rel: invalid frontmatter block" + return + fi + + name_line=$(echo "$fm" | grep -E '^name:' | head -n1 || true) + desc_line=$(echo "$fm" | grep -E '^description:' | head -n1 || true) + + if [[ -z "$name_line" ]]; then + add_err "$rel: frontmatter missing 'name'" + else + name_val=$(echo "$name_line" | sed 's/^name:[[:space:]]*//') + if ((${#name_val} > 64)); then + add_err "$rel: 'name' must be ≤64 characters" + fi + fi + + if [[ -z "$desc_line" ]]; then + add_err "$rel: frontmatter missing 'description'" + fi + + body=$(awk ' + /^---$/ { if (++c == 2) { body=1; next } } + body { print } + ' "$f") + if [[ -z "${body//[$' \t\r\n']/}" ]]; then + add_err "$rel: no body after frontmatter" + fi +} + +validate_marketplace() { + local mp="$1" + local n i name src exp got + + if [[ ! -f "$mp" ]]; then + add_err "Missing marketplace: $mp" + return + fi + + if ! jq -e . "$mp" >/dev/null 2>&1; then + add_err "$mp: invalid JSON" + return + fi + + for k in name owner metadata plugins; do + if ! jq -e ".${k} != null" "$mp" >/dev/null 2>&1; then + add_err "$mp: missing top-level key '$k'" + fi + done + + if ! jq -e '.plugins | type == "array"' "$mp" >/dev/null 2>&1; then + add_err "$mp: 'plugins' must be an array" + return + fi + + n=$(jq '.plugins | length' "$mp") + u=$(jq '.plugins | map(.name) | unique | length' "$mp") + if [[ "$n" -ne "$u" ]]; then + add_err "$mp: duplicate plugin name in plugins[]" + fi + + for ((i = 0; i < n; i++)); do + if ! jq -e ".plugins[$i] | type == \"object\"" "$mp" >/dev/null 2>&1; then + add_err "$mp: plugins[$i] must be an object" + continue + fi + for req in name source description version; do + if ! jq -e ".plugins[$i].${req} != null" "$mp" >/dev/null 2>&1; then + add_err "$mp: plugins[$i] missing '$req'" + fi + done + name=$(jq -r ".plugins[$i].name" "$mp") + src=$(jq -r ".plugins[$i].source" "$mp") + src="${src#./}" + if [[ ! -d "$ROOT/$src" ]]; then + add_err "$mp: plugin '$name' source not a directory: $src" + else + exp="$ROOT/plugins/$name" + got="$(cd "$ROOT/$src" && pwd -P)" + if [[ "$(cd "$exp" 2>/dev/null && pwd -P)" != "$got" ]]; then + add_err "$mp: plugin '$name' source path should be ./plugins/$name, got ./$src" + fi + fi + done + + mapfile -t mp_names < <(jq -r '.plugins[].name' "$mp" | sort -u) + mapfile -t want < <(printf '%s\n' "${plugin_ids[@]}" | sort -u) + + if ! cmp -s <(printf '%s\n' "${mp_names[@]}") <(printf '%s\n' "${want[@]}"); then + local miss extra + miss=$(comm -23 <(printf '%s\n' "${want[@]}") <(printf '%s\n' "${mp_names[@]}") | paste -sd, - || true) + extra=$(comm -13 <(printf '%s\n' "${want[@]}") <(printf '%s\n' "${mp_names[@]}") | paste -sd, - || true) + [[ -n "$miss" ]] && add_err "$mp: marketplace missing plugins: ${miss//,/, }" + [[ -n "$extra" ]] && add_err "$mp: marketplace has unknown plugins: ${extra//,/, }" + fi +} + +# --- Skill trigger keywords (description substring match, case-insensitive) --- +_desc_contains() { + local hay="$1" needle="$2" + local h n + h=$(printf '%s' "$hay" | tr '[:upper:]' '[:lower:]') + n=$(printf '%s' "$needle" | tr '[:upper:]' '[:lower:]') + [[ "$h" == *"$n"* ]] +} + +check_skill_triggers() { + local f skill_name fm desc total found kw + mapfile -t _skill_paths < <(find "$ROOT/plugins" -type f -path '*/skills/*/SKILL.md' | sort) + for f in "${_skill_paths[@]}"; do + skill_name=$(basename "$(dirname "$f")") + fm=$(awk ' + /^---$/ { if (++c == 1) next; if (c == 2) exit } + c == 1 { print } + ' "$f") + if [[ -z "$fm" ]]; then + echo "⚠ $skill_name: No frontmatter" + continue + fi + desc=$(echo "$fm" | grep -E '^description:' | head -n1 | sed 's/^description:[[:space:]]*//') + if [[ -z "$desc" ]]; then + echo "⚠ $skill_name: No description in frontmatter" + continue + fi + echo "" + echo "📋 Skill: $skill_name" + echo " Description length: ${#desc} chars" + total=0 + found=0 + case "$skill_name" in + keeper-secrets) + for kw in keeper ksm secret vault credentials 'keeper://' password 'api key' certificate 'ci/cd' docker kubernetes; do + ((total++)) || true + if _desc_contains "$desc" "$kw"; then + ((found++)) || true + fi + done + ;; + keeper-admin) + for kw in keeper commander admin enterprise 'user management' pam rotation session; do + ((total++)) || true + if _desc_contains "$desc" "$kw"; then + ((found++)) || true + fi + done + ;; + keeper-setup) + for kw in install keeper setup configure ksm commander cli; do + ((total++)) || true + if _desc_contains "$desc" "$kw"; then + ((found++)) || true + fi + done + ;; + *) + echo " ⚠ No expected trigger list for skill id $skill_name" + continue + ;; + esac + if [[ "$found" -gt 0 ]]; then + echo " ✓ Found $found/$total expected triggers" + else + echo " ⚠ No expected triggers found in description" + fi + done + echo "" + echo "✓ Analyzed ${#_skill_paths[@]} skills" +} + +_check_skill_sections() { + local path="$1" plugin_id="$2" + shift 2 + local secs=("$@") content missing s + if [[ ! -f "$ROOT/$path" ]]; then + add_err "Missing: $path" + return + fi + content=$(cat "$ROOT/$path") + missing=() + for s in "${secs[@]}"; do + if ! printf '%s' "$content" | grep -Fq "$s"; then + missing+=("$s") + fi + done + if [[ ${#missing[@]} -gt 0 ]]; then + echo "⚠ $plugin_id: Missing sections: ${missing[*]}" + else + echo "✓ $plugin_id: All expected sections present" + fi +} + +check_skill_content_sections() { + echo "Checking skill content coverage..." + _check_skill_sections "plugins/keeper-secrets/skills/keeper-secrets/SKILL.md" keeper-secrets \ + "When to Use KSM" "Prerequisites" "Core Commands" "ksm exec" "ksm interpolate" "Guardrails" + _check_skill_sections "plugins/keeper-admin/skills/keeper-admin/SKILL.md" keeper-admin \ + "When to Use Commander" "Prerequisites" "Vault Operations" "Enterprise Administration" "Guardrails" + _check_skill_sections "plugins/keeper-setup/skills/keeper-setup/SKILL.md" keeper-setup \ + "Quick Install" "KSM CLI" "Commander" "Troubleshooting" +} + +# Scan markdown for fenced code blocks; warn on missing language or suspicious literals. +check_md_file_examples() { + local filepath="$1" + local rel="${filepath#"$ROOT"/}" + local _fence + _fence=$(printf '\x60\x60\x60') + local line in_block=0 lang="" code="" issues=() + + while IFS= read -r line || [[ -n "${line:-}" ]]; do + if [[ "${line:0:3}" == "$_fence" ]]; then + if [[ "$in_block" -eq 1 ]]; then + # closing fence + if [[ -z "$lang" && "$code" == *$'\n'* && -n "${code//[$' \t\r\n']/}" ]]; then + issues+=("Code block without language specification") + fi + if [[ -n "$code" ]]; then + if printf '%s' "$code" | grep -qiE 'password[[:space:]]*=[[:space:]]*['\''"]' && ! printf '%s' "$code" | grep -qi 'keeper'; then + issues+=("Possible hardcoded password in example") + fi + if printf '%s' "$code" | grep -qiE 'token[[:space:]]*=[[:space:]]*['\''"]' && ! printf '%s' "$code" | grep -qi 'keeper'; then + issues+=("Possible hardcoded token in example") + fi + fi + in_block=0 + lang="" + code="" + else + # opening fence + in_block=1 + lang=$(echo "$line" | sed 's/^```//' | awk '{print $1}' | tr -d '\r') + code="" + fi + continue + fi + if [[ "$in_block" -eq 1 ]]; then + code+="$line"$'\n' + fi + done <"$filepath" + + if [[ ${#issues[@]} -gt 0 ]]; then + echo "" + echo "⚠ $rel:" + for m in "${issues[@]}"; do + echo " - $m" + done + else + echo "✓ $rel" + fi +} + +_validate_max_jobs() { + local max="${VALIDATE_MAX_JOBS:-8}" + local ncpu + ncpu=$(nproc 2>/dev/null || sysctl -n hw.ncpu 2>/dev/null || echo 4) + if ((max > ncpu)); then max=$ncpu; fi + if ((max < 1)); then max=1; fi + printf '%s' "$max" +} + +check_md_examples_all() { + echo "Checking for insecure examples..." + local max + max=$(_validate_max_jobs) + mapfile -t _md_files < <(find "$ROOT/plugins" -type f -name '*.md' | sort) + if [[ ${#_md_files[@]} -eq 0 ]]; then + return 0 + fi + local tmpdir job active + tmpdir=$(mktemp -d "${TMPDIR:-/tmp}/validate-plugins-md.XXXXXX") + trap 'rm -rf "$tmpdir"' RETURN + job=0 + for f in "${_md_files[@]}"; do + while true; do + active=$(jobs -p 2>/dev/null | wc -l | tr -d ' ') + [[ "${active:-0}" -lt "$max" ]] && break + sleep 0.02 + done + job=$((job + 1)) + ( check_md_file_examples "$f" >"$tmpdir/$(printf '%05d' "$job").out" ) & + done + wait 2>/dev/null || true + local o + mapfile -t _outs < <(find "$tmpdir" -maxdepth 1 -name '*.out' -print | sort -V) + for o in "${_outs[@]}"; do + cat "$o" + done + rm -rf "$tmpdir" + trap - RETURN +} + +check_root_docs() { + echo "Checking documentation files..." + local doc sections missing s + # (file, pipe-separated sections; empty = existence only) + for doc in README.md SECURITY.md CONTRIBUTING.md LICENSE.md; do + if [[ ! -f "$ROOT/$doc" ]]; then + echo "⚠ Missing: $doc" + continue + fi + missing=() + case "$doc" in + README.md) + for s in Installation Prerequisites Documentation Contributing License Security; do + grep -Fq "$s" "$ROOT/$doc" || missing+=("$s") + done + ;; + SECURITY.md) + for s in Reporting "Supported versions"; do + grep -Fq "$s" "$ROOT/$doc" || missing+=("$s") + done + ;; + CONTRIBUTING.md) + for s in "How to Contribute" "Testing Guidelines" "Submission Checklist"; do + grep -Fq "$s" "$ROOT/$doc" || missing+=("$s") + done + ;; + LICENSE.md) + missing=() + ;; + esac + if [[ ${#missing[@]} -gt 0 ]]; then + echo "⚠ $doc: Missing: ${missing[*]}" + else + echo "✓ $doc" + fi + done +} + +# --- discover plugins (sorted ids) --- +plugin_ids=() +while IFS= read -r line; do + [[ -n "$line" ]] && plugin_ids+=("$line") +done < <( + for d in "$ROOT"/plugins/*/; do + [[ -d "$d" ]] || continue + base=$(basename "$d") + [[ "$base" == .* ]] && continue + if [[ -f "${d}.claude-plugin/plugin.json" || -f "${d}.cursor-plugin/plugin.json" ]]; then + printf '%s\n' "$base" + fi + done | sort +) + +if [[ ! -d "$ROOT/plugins" ]]; then + echo "ERROR: plugins directory not found: $ROOT/plugins" + exit 1 +fi + +if [[ ${#plugin_ids[@]} -eq 0 ]]; then + echo "ERROR: No plugin directories under $ROOT/plugins" + exit 1 +fi + +echo "== Plugin manifests (Claude + Cursor) ==" +for pid in "${plugin_ids[@]}"; do + pr="$ROOT/plugins/$pid" + echo "" + echo "--- $pid ---" + cj="$pr/.claude-plugin/plugin.json" + uj="$pr/.cursor-plugin/plugin.json" + + if [[ ! -f "$cj" ]]; then + add_err "Missing Claude manifest: $cj" + else + if ! jq -e . "$cj" >/dev/null 2>&1; then + add_err "$cj: invalid JSON" + else + validate_plugin_json "$cj" "$cj" + pname=$(jq -r '.name' "$cj") + if [[ "$pname" != "$pid" ]]; then + add_err "$cj: 'name' must match directory ('$pid')" + else + ver=$(jq -r '.version' "$cj") + ok "Claude plugin.json ($ver)" + fi + fi + fi + + if [[ ! -f "$uj" ]]; then + add_err "Missing Cursor manifest: $uj" + else + if ! jq -e . "$uj" >/dev/null 2>&1; then + add_err "$uj: invalid JSON" + else + validate_plugin_json "$uj" "$uj" + validate_cursor_logo "$pr" "$uj" "$uj" + pname=$(jq -r '.name' "$uj") + if [[ "$pname" != "$pid" ]]; then + add_err "$uj: 'name' must match directory ('$pid')" + else + ver=$(jq -r '.version' "$uj") + ok "Cursor plugin.json ($ver)" + fi + fi + fi +done + +echo "" +echo "== Root marketplaces ==" +for mp_path in "$ROOT/.claude-plugin/marketplace.json" "$ROOT/.cursor-plugin/marketplace.json"; do + label="Claude" + [[ "$mp_path" == *cursor* ]] && label="Cursor" + echo "" + echo "--- $label ---" + n_err=${#errors[@]} + validate_marketplace "$mp_path" + if [[ ${#errors[@]} -eq "$n_err" ]]; then + n=$(jq '.plugins | length' "$mp_path") + ok "${mp_path#"$ROOT"/}: $n plugins" + fi +done + +echo "" +echo "== SKILL.md files ==" +mapfile -t skill_files < <(find "$ROOT/plugins" -type f -path '*/skills/*/SKILL.md' | sort) +if [[ ${#skill_files[@]} -eq 0 ]]; then + add_err "No SKILL.md files under $ROOT/plugins" +else + for sf in "${skill_files[@]}"; do + rel="${sf#"$ROOT"/}" + echo "" + echo "Validating $rel" + n_err=${#errors[@]} + validate_skill_md "$sf" "$rel" + if [[ ${#errors[@]} -eq "$n_err" ]]; then + ok "frontmatter and body" + fi + done +fi + +echo "" +echo "== Reference docs ==" +for rel in \ + "skills/keeper-secrets/references/ksm-commands.md" \ + "skills/keeper-secrets/references/keeper-notation.md" \ + "skills/keeper-secrets/references/ksm-exec-patterns.md"; do + p="$ROOT/plugins/keeper-secrets/$rel" + if [[ ! -f "$p" ]]; then + add_err "Missing reference: $p" + else + n=$(wc -l <"$p" | tr -d ' ') + if [[ "$n" -lt 5 ]]; then + add_err "Reference file suspiciously short ($n lines): $p" + else + ok "keeper-secrets/$rel ($n lines)" + fi + fi +done + +for rel in \ + "skills/keeper-admin/references/commander-commands.md" \ + "skills/keeper-admin/references/enterprise-mgmt.md" \ + "skills/keeper-admin/references/pam-commands.md" \ + "skills/keeper-admin/references/rotation-commands.md"; do + p="$ROOT/plugins/keeper-admin/$rel" + if [[ ! -f "$p" ]]; then + add_err "Missing reference: $p" + else + n=$(wc -l <"$p" | tr -d ' ') + if [[ "$n" -lt 5 ]]; then + add_err "Reference file suspiciously short ($n lines): $p" + else + ok "keeper-admin/$rel ($n lines)" + fi + fi +done + +echo "" +echo "== Skill trigger keywords ==" +check_skill_triggers + +echo "" +echo "== Skill content sections ==" +check_skill_content_sections + +echo "" +echo "== Skill examples (markdown code blocks) ==" +check_md_examples_all + +echo "" +echo "== Root documentation ==" +check_root_docs + +if [[ ${#errors[@]} -gt 0 ]]; then + echo "" + echo "❌ Validation failed:" + for e in "${errors[@]}"; do + echo " - $e" + done + exit 1 +fi + +echo "" +echo "✓ All validations passed" diff --git a/version.txt b/version.txt index 9084fa2..6e8bf73 100644 --- a/version.txt +++ b/version.txt @@ -1 +1 @@ -1.1.0 +0.1.0