[
  {
    "path": ".github/ISSUE_TEMPLATE/bug_report.md",
    "content": "---\nname: Bug Report\nabout: Report a bug or issue with OpenSkills\ntitle: '[BUG] '\nlabels: bug\nassignees: ''\n---\n\n**Bug Description**\nA clear and concise description of the bug.\n\n**Steps to Reproduce**\n1.\n2.\n3.\n\n**Expected Behavior**\nWhat should happen.\n\n**Actual Behavior**\nWhat actually happens.\n\n**Environment**\n- openskills version: (`openskills --version`)\n- Operating System:\n- Node.js version: (`node --version`)\n- AI agent: (Claude Code, Cursor, Windsurf, Aider, etc.)\n\n**Installation Context**\n- [ ] Global install (`npm i -g openskills`)\n- [ ] Local install via npm link\n- [ ] Installed skills: (list from `openskills list`)\n\n**Additional Context**\nAdd any other relevant information, error messages, or screenshots.\n"
  },
  {
    "path": ".github/ISSUE_TEMPLATE/config.yml",
    "content": "blank_issues_enabled: false\ncontact_links:\n  - name: Anthropic Skills Documentation\n    url: https://www.anthropic.com/engineering/equipping-agents-for-the-real-world-with-agent-skills\n    about: Learn about Anthropic's Agent Skills specification\n  - name: Anthropic Skills Repository\n    url: https://github.com/anthropics/skills\n    about: Browse official Anthropic example skills\n  - name: Discussions\n    url: https://github.com/numman-ali/openskills/discussions\n    about: Ask questions or discuss OpenSkills with the community\n"
  },
  {
    "path": ".github/ISSUE_TEMPLATE/feature_request.md",
    "content": "---\nname: Feature Request\nabout: Suggest a new feature or enhancement\ntitle: '[FEATURE] '\nlabels: enhancement\nassignees: ''\n---\n\n**Feature Description**\nA clear description of the feature you'd like to see.\n\n**Use Case**\nExplain how this feature would be used and what problem it solves.\n\n**Proposed Implementation**\nIf you have ideas about how this could be implemented, share them here.\n\n**Compatibility Consideration**\nPlease confirm:\n- [ ] This feature maintains compatibility with Anthropic's SKILL.md specification\n- [ ] This feature works across multiple AI agents (not just Claude Code)\n- [ ] This feature aligns with the simplicity goal (.claude/skills only)\n\n**Alternatives Considered**\nHave you considered any alternative solutions or workarounds?\n\n**Additional Context**\nAdd any other context, screenshots, or examples.\n"
  },
  {
    "path": ".github/maintainer/config.json",
    "content": "{\n  \"schemaVersion\": 1,\n  \"reportsDir\": \"reports\",\n  \"stateFile\": \".github/maintainer/state.json\",\n  \"noMergeExternalPRs\": true,\n  \"semantics\": {\n    \"intent\": {\n      \"bug\": [\n        \"bug\",\n        \"crash\",\n        \"error\",\n        \"exception\",\n        \"fails\",\n        \"failing\",\n        \"broken\",\n        \"regression\"\n      ],\n      \"feature\": [\n        \"feature\",\n        \"enhancement\",\n        \"feature request\",\n        \"would be nice\",\n        \"add support\",\n        \"request\"\n      ],\n      \"question\": [\n        \"how do i\",\n        \"how can i\",\n        \"is it possible\",\n        \"what does\",\n        \"question\"\n      ],\n      \"support\": [\n        \"help\",\n        \"support\",\n        \"troubleshoot\",\n        \"configure\",\n        \"configuration\",\n        \"setup\",\n        \"install\"\n      ],\n      \"meta\": [\n        \"roadmap\",\n        \"governance\",\n        \"maintainer\",\n        \"community\",\n        \"discussion\"\n      ]\n    },\n    \"needsInfo\": {\n      \"repro\": [\n        \"steps to reproduce\",\n        \"repro steps\",\n        \"reproduction\"\n      ],\n      \"expected\": [\n        \"expected behavior\",\n        \"expected result\"\n      ],\n      \"actual\": [\n        \"actual behavior\",\n        \"actual result\"\n      ],\n      \"environment\": [\n        \"environment\",\n        \"os\",\n        \"operating system\",\n        \"platform\"\n      ],\n      \"version\": [\n        \"version\",\n        \"openskills version\",\n        \"node version\"\n      ],\n      \"logs\": [\n        \"logs\",\n        \"stack trace\",\n        \"error output\"\n      ],\n      \"testPlan\": [\n        \"test plan\",\n        \"testing\",\n        \"tests run\"\n      ]\n    },\n    \"environmentTokens\": [\n      \"windows\",\n      \"win11\",\n      \"win10\",\n      \"mac\",\n      \"macos\",\n      \"linux\",\n      \"ubuntu\",\n      \"debian\",\n      \"node\",\n      \"npm\",\n      \"pnpm\",\n      \"yarn\"\n    ],\n    \"relationship\": {\n      \"linkKeywords\": [\n        \"fixes\",\n        \"closes\",\n        \"resolves\",\n        \"addresses\",\n        \"related to\",\n        \"see\",\n        \"ref\",\n        \"refs\",\n        \"linked to\"\n      ],\n      \"duplicateHints\": [\n        \"duplicate\",\n        \"same issue\",\n        \"same error\",\n        \"same problem\"\n      ]\n    },\n    \"errors\": {\n      \"signatures\": [],\n      \"keywords\": [\n        \"error\",\n        \"exception\",\n        \"failed\",\n        \"failure\",\n        \"crash\",\n        \"security error\"\n      ]\n    }\n  },\n  \"heuristics\": {\n    \"needsInfo\": {\n      \"enabled\": true,\n      \"threshold\": 2,\n      \"issueSignals\": {\n        \"missingRepro\": {\n          \"enabled\": true,\n          \"applyTo\": [\n            \"bug\",\n            \"unknown\"\n          ]\n        },\n        \"missingExpectedActual\": {\n          \"enabled\": true,\n          \"applyTo\": [\n            \"bug\",\n            \"unknown\"\n          ]\n        },\n        \"missingEnvironment\": {\n          \"enabled\": true,\n          \"applyTo\": [\n            \"bug\",\n            \"support\",\n            \"question\",\n            \"unknown\"\n          ]\n        },\n        \"missingVersion\": {\n          \"enabled\": true,\n          \"applyTo\": [\n            \"bug\",\n            \"support\",\n            \"question\",\n            \"unknown\"\n          ]\n        },\n        \"missingLogs\": {\n          \"enabled\": true,\n          \"applyTo\": [\n            \"bug\",\n            \"support\",\n            \"unknown\"\n          ]\n        }\n      },\n      \"prSignals\": {\n        \"missingTestPlan\": {\n          \"enabled\": true\n        },\n        \"missingDescription\": {\n          \"enabled\": true\n        }\n      },\n      \"weights\": {\n        \"missingRepro\": 2,\n        \"missingExpectedActual\": 1,\n        \"missingEnvironment\": 1,\n        \"missingVersion\": 1,\n        \"missingLogs\": 1,\n        \"missingTestPlan\": 1,\n        \"missingDescription\": 1\n      }\n    },\n    \"duplicates\": {\n      \"titleSimilarityThreshold\": 0.6,\n      \"overlapThreshold\": 0.35,\n      \"requireSharedError\": false\n    },\n    \"relationshipQuality\": {\n      \"strongOverlapThreshold\": 0.45,\n      \"mediumOverlapThreshold\": 0.15,\n      \"strongWhenExplicit\": true,\n      \"defaultWhenLinked\": \"medium\"\n    }\n  },\n  \"sentiment\": {\n    \"positiveWords\": [\n      \"thanks\",\n      \"thank\",\n      \"great\",\n      \"awesome\",\n      \"good\",\n      \"love\",\n      \"like\",\n      \"helpful\",\n      \"appreciate\",\n      \"nice\",\n      \"excellent\",\n      \"amazing\",\n      \"worked\",\n      \"works\",\n      \"fixed\",\n      \"resolved\",\n      \"perfect\"\n    ],\n    \"negativeWords\": [\n      \"broken\",\n      \"fail\",\n      \"fails\",\n      \"failing\",\n      \"error\",\n      \"crash\",\n      \"crashes\",\n      \"bad\",\n      \"terrible\",\n      \"awful\",\n      \"hate\",\n      \"bug\",\n      \"regression\",\n      \"doesnt\",\n      \"doesn't\",\n      \"cant\",\n      \"can't\",\n      \"worse\",\n      \"problem\",\n      \"issue\"\n    ]\n  },\n  \"staleDays\": {\n    \"issues\": 60,\n    \"prs\": 30\n  },\n  \"labels\": {\n    \"blocked\": [\n      \"blocked\",\n      \"on-hold\"\n    ],\n    \"needsInfo\": [\n      \"needs-info\",\n      \"needs-more-info\",\n      \"waiting-for-response\"\n    ],\n    \"needsDecision\": [\n      \"needs-decision\"\n    ],\n    \"closable\": [\n      \"duplicate\",\n      \"wontfix\",\n      \"invalid\",\n      \"out-of-scope\"\n    ]\n  },\n  \"typeLabels\": {\n    \"bug\": [\n      \"bug\"\n    ],\n    \"feature\": [\n      \"feature\",\n      \"enhancement\"\n    ],\n    \"question\": [\n      \"question\"\n    ],\n    \"support\": [\n      \"support\"\n    ],\n    \"meta\": [\n      \"meta\",\n      \"governance\",\n      \"roadmap\"\n    ]\n  },\n  \"priority\": {\n    \"issue\": {\n      \"commentWeight\": 2,\n      \"reactionWeights\": {\n        \"THUMBS_UP\": 3,\n        \"THUMBS_DOWN\": 2,\n        \"HEART\": 2\n      },\n      \"typeBoosts\": {\n        \"bug\": 10,\n        \"feature\": 5\n      },\n      \"stalePenalty\": {\n        \"over30\": -5,\n        \"over60\": -10\n      },\n      \"ageBoost\": {\n        \"over30AndFresh\": 5\n      }\n    },\n    \"pr\": {\n      \"commentWeight\": 2,\n      \"reviewWeight\": 3,\n      \"approvalBoost\": 8,\n      \"ciSuccessBoost\": 6,\n      \"unresolvedThreadsPenalty\": -5,\n      \"changesRequestedPenalty\": -5,\n      \"draftPenalty\": -8,\n      \"stalePenalty\": {\n        \"over14\": -5,\n        \"over30\": -10\n      }\n    },\n    \"labelBoosts\": {\n      \"security\": 40,\n      \"critical\": 25,\n      \"high-priority\": 15\n    }\n  },\n  \"relationshipScore\": {\n    \"overlapWeight\": 30,\n    \"explicitLinkBoost\": 8,\n    \"linkedIssuesWeight\": 2,\n    \"mentionedByWeight\": 2,\n    \"linkedIssuePriorityWeight\": 0.15\n  },\n  \"implementation\": {\n    \"commentWeight\": 1,\n    \"reviewWeight\": 2,\n    \"reviewCommentWeight\": 1,\n    \"reactionWeight\": 1,\n    \"linkedIssuePriorityWeight\": 0.6,\n    \"linkedIssueReactionWeight\": 0.3,\n    \"linkedIssueSentimentWeight\": 0.2,\n    \"relationshipScoreWeight\": 0.5,\n    \"relationshipQualityBoosts\": {\n      \"strong\": 6,\n      \"medium\": 3,\n      \"weak\": 0,\n      \"none\": -2\n    },\n    \"touchesTestsBoost\": 5,\n    \"ciSuccessBoost\": 6,\n    \"ciFailurePenalty\": -6,\n    \"changesRequestedPenalty\": -8,\n    \"unresolvedThreadsPenalty\": -4,\n    \"draftPenalty\": -10,\n    \"agePenalty\": {\n      \"over14\": -3,\n      \"over30\": -7,\n      \"over60\": -12\n    },\n    \"sizePenalty\": {\n      \"filesOver10\": -3,\n      \"filesOver25\": -7,\n      \"linesOver500\": -8,\n      \"linesOver1000\": -12\n    },\n    \"agentScoreWeight\": 1,\n    \"agentConfidenceMultipliers\": {\n      \"high\": 1.5,\n      \"medium\": 1,\n      \"low\": 0.5,\n      \"unset\": 0\n    },\n    \"scoreFloor\": 0,\n    \"tierThresholds\": {\n      \"strong\": 40,\n      \"medium\": 20\n    }\n  }\n}\n"
  },
  {
    "path": ".github/maintainer/context.md",
    "content": "# Project Context\n\n## Vision\nOpenSkills is a universal skills loader for AI coding agents. It enables installing and managing Anthropic SKILL.md format skills across any AI agent (Claude Code, Cursor, Windsurf, Gemini, etc.), making it easy to extend agent capabilities with reusable skill definitions.\n\n## Current Priorities\n1. **Close the loop on v1.5.0 fixes** - Ensure update workflow + Gemini usage change are confirmed and closed\n2. **Resolve PR backlog** - Decide on symlink installs, safe deletion, spinner changes, Nix flake\n3. **Improve onboarding docs** - Keep update guidance and agent-specific instructions clear\n4. **Triage feature requests** - Consolidate duplicates and keep the queue actionable\n\n## Success Metrics\n- Update command works for typical git installs, with clear recovery steps\n- Agent usage text avoids shell/tooling misunderstandings\n- PR backlog stays small and decisions are explicit\n- Issue queue is triaged and duplicates consolidated\n- Contributors receive timely, respectful responses\n\n## Areas\n\n| Area | Status | Notes |\n|------|--------|-------|\n| `src/commands/install.ts` | Healthy | Windows path handling fixed |\n| `src/cli.ts` | Healthy | Version now reads from package.json |\n| `tests/` | Good | 103 tests passing |\n| `docs/` | Improving | Update guidance and agent usage tips added |\n\n## Contribution Guidelines\n- PRs are reviewed for intent and approach but implemented by maintainers\n- Tests are required for bug fixes\n- Keep changes focused and minimal\n- Cross-platform compatibility is essential\n\n## Tone\nTechnical, respectful, and appreciative. Thank contributors for their insights even when we implement fixes ourselves. Be direct about what we're doing and why.\n\n## Out of Scope\n- Merging external PRs directly (we implement fixes ourselves using PR insights)\n- Features that add complexity without clear benefit\n- Platform-specific workarounds when cross-platform solutions exist\n"
  },
  {
    "path": ".github/maintainer/contributors.md",
    "content": "# Contributor Notes\n\n## Active Contributors\n\n### @username\n- **First seen:** 2025-12-15\n- **Contributions:** 3 PRs (2 implemented), 5 issues\n- **Strengths:** Good tests, clear descriptions\n- **Notes:** Offered to help maintain. Responsive.\n\n### @another-user\n- **First seen:** 2026-01-10\n- **Contributions:** 1 PR (pending)\n- **Notes:** First-time contributor. Needs guidance on tests.\n\n## Former Contributors\n\n### @past-contributor\n- **Active:** 2025-06 to 2025-09\n- **Notes:** Great work on CLI. Moved on to other projects.\n\n"
  },
  {
    "path": ".github/maintainer/decisions.md",
    "content": "# Decision Log\n\n## 2026-01\n\n### [RELEASE:1.5.0] Shipped update command + source tracking\n**Date:** 2026-01-17\n**Decision:** Release and publish 1.5.0\n**Reasoning:** Adds `openskills update`, source metadata tracking, multi-skill reads, and improved update UX.\n\n### [RELEASE:1.3.1] Shipped fixes for Windows install, CLI version, root SKILL.md\n**Date:** 2026-01-17\n**Decision:** Release and publish 1.3.1\n**Reasoning:** Resolved critical Windows install bug, version mismatch, and root SKILL.md detection.\n\n### [ISSUES:28,34,43,48,17,29,20,42,51] Closed - Fixed in v1.3.1\n**Date:** 2026-01-17\n**Decision:** Close as resolved\n**Reasoning:** Fixes shipped in v1.3.1; confirmed via tests and release notes.\n\n### [PRS:38,37,18,40] Closed - Implemented fix\n**Date:** 2026-01-17\n**Decision:** Closed after maintainer implementation\n**Reasoning:** Used PR insights; shipped in v1.3.1 with tests.\n\n### [ISSUE:42] Deferred - Version requirements\n**Date:** 2026-01-15\n**Decision:** Defer to post-1.0\n**Reasoning:** Good feature but adds complexity. Want to stabilize core first.\n\n### [PR:38] Closed - Implemented fix\n**Date:** 2026-01-16\n**Decision:** Closed after maintainer implementation\n**Reasoning:** Fixes critical bug affecting all Windows users. Tests pass.\n\n### [ISSUE:30] Closed - Stale\n**Date:** 2026-01-16\n**Decision:** Closed without action\n**Reasoning:** No response to info request for 60 days.\n"
  },
  {
    "path": ".github/maintainer/index/graph.json",
    "content": "{\n  \"generatedAt\": \"2026-01-17T19:26:09.772Z\",\n  \"nodes\": [\n    {\n      \"id\": \"issue:35\",\n      \"type\": \"issue\",\n      \"title\": \"[I NEED YOU!] Request for a Core Contributor!\",\n      \"priorityScore\": 9,\n      \"actionability\": \"ready\"\n    },\n    {\n      \"id\": \"issue:50\",\n      \"type\": \"issue\",\n      \"title\": \"now i have skills and model api ,but i can' use claude ,should i write workflow to let the model use skill or how can i create a project like claude to use the skills\",\n      \"priorityScore\": 7,\n      \"actionability\": \"ready\"\n    },\n    {\n      \"id\": \"issue:47\",\n      \"type\": \"issue\",\n      \"title\": \"[FEATURE]\",\n      \"priorityScore\": 5,\n      \"actionability\": \"ready\"\n    },\n    {\n      \"id\": \"issue:41\",\n      \"type\": \"issue\",\n      \"title\": \"[FEATURE] support multiple skill reads at once OR explain that they should be read sparately\",\n      \"priorityScore\": 5,\n      \"actionability\": \"ready\"\n    },\n    {\n      \"id\": \"issue:32\",\n      \"type\": \"issue\",\n      \"title\": \"[FEATURE] Project version requirements management\",\n      \"priorityScore\": 5,\n      \"actionability\": \"ready\"\n    },\n    {\n      \"id\": \"issue:24\",\n      \"type\": \"issue\",\n      \"title\": \"[FEATURE] AugmentCode\",\n      \"priorityScore\": 5,\n      \"actionability\": \"ready\"\n    },\n    {\n      \"id\": \"issue:13\",\n      \"type\": \"issue\",\n      \"title\": \"[FEATURE] Support skill switch\",\n      \"priorityScore\": 5,\n      \"actionability\": \"ready\"\n    },\n    {\n      \"id\": \"issue:19\",\n      \"type\": \"issue\",\n      \"title\": \"[FEATURE] Support gitlab\",\n      \"priorityScore\": 0,\n      \"actionability\": \"ready\"\n    },\n    {\n      \"id\": \"pr:30\",\n      \"type\": \"pr\",\n      \"title\": \"fix: switch to async spawn to unblock spinner animation and switch fr…\",\n      \"priorityScore\": 0,\n      \"implementationScore\": 8,\n      \"actionability\": \"needs-analysis\"\n    },\n    {\n      \"id\": \"pr:39\",\n      \"type\": \"pr\",\n      \"title\": \"feat: implement safe deletion with trash/recycle bin suppor\",\n      \"priorityScore\": 0,\n      \"implementationScore\": 5,\n      \"actionability\": \"needs-analysis\"\n    },\n    {\n      \"id\": \"pr:31\",\n      \"type\": \"pr\",\n      \"title\": \"feat: support installing local skills via symlinks with --symlink flag\",\n      \"priorityScore\": 0,\n      \"implementationScore\": 25,\n      \"actionability\": \"needs-analysis\"\n    },\n    {\n      \"id\": \"pr:25\",\n      \"type\": \"pr\",\n      \"title\": \"feat: add Nix flake for declarative builds and development\",\n      \"priorityScore\": 0,\n      \"implementationScore\": 20,\n      \"actionability\": \"needs-analysis\"\n    }\n  ],\n  \"edges\": [\n    {\n      \"from\": \"issue:47\",\n      \"to\": \"issue:41\",\n      \"type\": \"possible_duplicate\"\n    },\n    {\n      \"from\": \"issue:47\",\n      \"to\": \"issue:32\",\n      \"type\": \"possible_duplicate\"\n    },\n    {\n      \"from\": \"issue:47\",\n      \"to\": \"issue:24\",\n      \"type\": \"possible_duplicate\"\n    },\n    {\n      \"from\": \"issue:47\",\n      \"to\": \"issue:19\",\n      \"type\": \"possible_duplicate\"\n    },\n    {\n      \"from\": \"issue:47\",\n      \"to\": \"issue:13\",\n      \"type\": \"possible_duplicate\"\n    },\n    {\n      \"from\": \"issue:32\",\n      \"to\": \"issue:45\",\n      \"type\": \"mentions\"\n    },\n    {\n      \"from\": \"issue:32\",\n      \"to\": \"issue:38\",\n      \"type\": \"mentions\"\n    },\n    {\n      \"from\": \"issue:13\",\n      \"to\": \"issue:41\",\n      \"type\": \"possible_duplicate\"\n    },\n    {\n      \"from\": \"issue:19\",\n      \"to\": \"issue:41\",\n      \"type\": \"possible_duplicate\"\n    },\n    {\n      \"from\": \"issue:19\",\n      \"to\": \"issue:13\",\n      \"type\": \"possible_duplicate\"\n    }\n  ]\n}\n"
  },
  {
    "path": ".github/maintainer/index/items.json",
    "content": "{\n  \"generatedAt\": \"2026-01-17T19:26:09.772Z\",\n  \"count\": 32,\n  \"items\": [\n    {\n      \"id\": 6,\n      \"type\": \"issue\",\n      \"title\": \"\",\n      \"actionability\": \"done\",\n      \"priorityScore\": 17,\n      \"sentimentScore\": 0,\n      \"needsInfoScore\": 0,\n      \"needsInfoSignals\": [],\n      \"linkedIssues\": [],\n      \"labels\": [\n        \"enhancement\"\n      ],\n      \"notePath\": \".github/maintainer/notes/issues/000/ISSUE-6.md\",\n      \"lastSeenAt\": \"2026-01-17T19:10:52.058Z\"\n    },\n    {\n      \"id\": 9,\n      \"type\": \"issue\",\n      \"title\": \"\",\n      \"actionability\": \"done\",\n      \"priorityScore\": 17,\n      \"sentimentScore\": 0,\n      \"needsInfoScore\": 0,\n      \"needsInfoSignals\": [],\n      \"linkedIssues\": [],\n      \"labels\": [\n        \"enhancement\"\n      ],\n      \"notePath\": \".github/maintainer/notes/issues/000/ISSUE-9.md\",\n      \"lastSeenAt\": \"2026-01-17T19:25:46Z\"\n    },\n    {\n      \"id\": 13,\n      \"type\": \"issue\",\n      \"title\": \"[FEATURE] Support skill switch\",\n      \"actionability\": \"ready\",\n      \"priorityScore\": 5,\n      \"sentimentScore\": 0,\n      \"needsInfoScore\": 0,\n      \"needsInfoSignals\": [],\n      \"linkedIssues\": [],\n      \"labels\": [\n        \"enhancement\"\n      ],\n      \"notePath\": \".github/maintainer/notes/issues/000/ISSUE-13.md\",\n      \"lastSeenAt\": \"2026-01-17T19:26:09.772Z\"\n    },\n    {\n      \"id\": 16,\n      \"type\": \"issue\",\n      \"title\": \"\",\n      \"actionability\": \"done\",\n      \"priorityScore\": 7,\n      \"sentimentScore\": 0,\n      \"needsInfoScore\": 0,\n      \"needsInfoSignals\": [],\n      \"linkedIssues\": [],\n      \"labels\": [\n        \"bug\"\n      ],\n      \"notePath\": \".github/maintainer/notes/issues/000/ISSUE-16.md\",\n      \"lastSeenAt\": \"2026-01-17T19:25:46Z\"\n    },\n    {\n      \"id\": 17,\n      \"type\": \"issue\",\n      \"title\": \"\",\n      \"actionability\": \"ready\",\n      \"priorityScore\": 7,\n      \"sentimentScore\": 0,\n      \"needsInfoScore\": 0,\n      \"needsInfoSignals\": [],\n      \"linkedIssues\": [],\n      \"labels\": [],\n      \"notePath\": \".github/maintainer/notes/issues/000/ISSUE-17.md\",\n      \"lastSeenAt\": \"2026-01-17T13:39:40.172Z\"\n    },\n    {\n      \"id\": 19,\n      \"type\": \"issue\",\n      \"title\": \"[FEATURE] Support gitlab\",\n      \"actionability\": \"ready\",\n      \"priorityScore\": 0,\n      \"sentimentScore\": 0,\n      \"needsInfoScore\": 0,\n      \"needsInfoSignals\": [],\n      \"linkedIssues\": [],\n      \"labels\": [\n        \"enhancement\"\n      ],\n      \"notePath\": \".github/maintainer/notes/issues/000/ISSUE-19.md\",\n      \"lastSeenAt\": \"2026-01-17T19:26:09.772Z\"\n    },\n    {\n      \"id\": 20,\n      \"type\": \"issue\",\n      \"title\": \"\",\n      \"actionability\": \"ready\",\n      \"priorityScore\": 26,\n      \"sentimentScore\": 0,\n      \"needsInfoScore\": 0,\n      \"needsInfoSignals\": [],\n      \"linkedIssues\": [],\n      \"labels\": [\n        \"bug\"\n      ],\n      \"notePath\": \".github/maintainer/notes/issues/000/ISSUE-20.md\",\n      \"lastSeenAt\": \"2026-01-17T13:39:40.172Z\"\n    },\n    {\n      \"id\": 24,\n      \"type\": \"issue\",\n      \"title\": \"[FEATURE] AugmentCode\",\n      \"actionability\": \"ready\",\n      \"priorityScore\": 5,\n      \"sentimentScore\": 0,\n      \"needsInfoScore\": 0,\n      \"needsInfoSignals\": [],\n      \"linkedIssues\": [],\n      \"labels\": [\n        \"enhancement\"\n      ],\n      \"notePath\": \".github/maintainer/notes/issues/000/ISSUE-24.md\",\n      \"lastSeenAt\": \"2026-01-17T19:26:09.772Z\"\n    },\n    {\n      \"id\": 28,\n      \"type\": \"issue\",\n      \"title\": \"\",\n      \"actionability\": \"ready\",\n      \"priorityScore\": 50,\n      \"agentScore\": 100,\n      \"agentConfidence\": \"high\",\n      \"sentimentScore\": 0,\n      \"needsInfoScore\": 0,\n      \"needsInfoSignals\": [],\n      \"linkedIssues\": [],\n      \"labels\": [\n        \"bug\"\n      ],\n      \"notePath\": \".github/maintainer/notes/issues/000/ISSUE-28.md\",\n      \"lastSeenAt\": \"2026-01-17T13:39:40.172Z\"\n    },\n    {\n      \"id\": 29,\n      \"type\": \"issue\",\n      \"title\": \"\",\n      \"actionability\": \"needs-info\",\n      \"priorityScore\": 18,\n      \"sentimentScore\": 0,\n      \"needsInfoScore\": 5,\n      \"needsInfoSignals\": [\n        \"missing-repro\",\n        \"missing-expected-actual\",\n        \"missing-environment\",\n        \"missing-version\"\n      ],\n      \"linkedIssues\": [],\n      \"labels\": [\n        \"bug\"\n      ],\n      \"notePath\": \".github/maintainer/notes/issues/000/ISSUE-29.md\",\n      \"lastSeenAt\": \"2026-01-17T13:39:40.172Z\"\n    },\n    {\n      \"id\": 32,\n      \"type\": \"issue\",\n      \"title\": \"[FEATURE] Project version requirements management\",\n      \"actionability\": \"ready\",\n      \"priorityScore\": 5,\n      \"sentimentScore\": 0,\n      \"needsInfoScore\": 0,\n      \"needsInfoSignals\": [],\n      \"linkedIssues\": [],\n      \"labels\": [\n        \"enhancement\"\n      ],\n      \"notePath\": \".github/maintainer/notes/issues/000/ISSUE-32.md\",\n      \"lastSeenAt\": \"2026-01-17T19:26:09.772Z\"\n    },\n    {\n      \"id\": 34,\n      \"type\": \"issue\",\n      \"title\": \"\",\n      \"actionability\": \"ready\",\n      \"priorityScore\": 27,\n      \"sentimentScore\": 0,\n      \"needsInfoScore\": 0,\n      \"needsInfoSignals\": [],\n      \"linkedIssues\": [],\n      \"labels\": [],\n      \"notePath\": \".github/maintainer/notes/issues/000/ISSUE-34.md\",\n      \"lastSeenAt\": \"2026-01-17T13:39:40.172Z\"\n    },\n    {\n      \"id\": 35,\n      \"type\": \"issue\",\n      \"title\": \"[I NEED YOU!] Request for a Core Contributor!\",\n      \"actionability\": \"ready\",\n      \"priorityScore\": 9,\n      \"sentimentScore\": 0,\n      \"needsInfoScore\": 0,\n      \"needsInfoSignals\": [],\n      \"linkedIssues\": [],\n      \"labels\": [\n        \"enhancement\"\n      ],\n      \"notePath\": \".github/maintainer/notes/issues/000/ISSUE-35.md\",\n      \"lastSeenAt\": \"2026-01-17T19:26:09.772Z\"\n    },\n    {\n      \"id\": 41,\n      \"type\": \"issue\",\n      \"title\": \"[FEATURE] support multiple skill reads at once OR explain that they should be read sparately\",\n      \"actionability\": \"ready\",\n      \"priorityScore\": 5,\n      \"sentimentScore\": 0,\n      \"needsInfoScore\": 0,\n      \"needsInfoSignals\": [],\n      \"linkedIssues\": [],\n      \"labels\": [\n        \"enhancement\"\n      ],\n      \"notePath\": \".github/maintainer/notes/issues/000/ISSUE-41.md\",\n      \"lastSeenAt\": \"2026-01-17T19:26:09.772Z\"\n    },\n    {\n      \"id\": 42,\n      \"type\": \"issue\",\n      \"title\": \"\",\n      \"actionability\": \"ready\",\n      \"priorityScore\": 12,\n      \"sentimentScore\": 0,\n      \"needsInfoScore\": 0,\n      \"needsInfoSignals\": [],\n      \"linkedIssues\": [],\n      \"labels\": [\n        \"bug\"\n      ],\n      \"notePath\": \".github/maintainer/notes/issues/000/ISSUE-42.md\",\n      \"lastSeenAt\": \"2026-01-17T13:39:40.172Z\"\n    },\n    {\n      \"id\": 43,\n      \"type\": \"issue\",\n      \"title\": \"\",\n      \"actionability\": \"ready\",\n      \"priorityScore\": 20,\n      \"sentimentScore\": 0,\n      \"needsInfoScore\": 0,\n      \"needsInfoSignals\": [],\n      \"linkedIssues\": [],\n      \"labels\": [\n        \"bug\"\n      ],\n      \"notePath\": \".github/maintainer/notes/issues/000/ISSUE-43.md\",\n      \"lastSeenAt\": \"2026-01-17T13:39:40.172Z\"\n    },\n    {\n      \"id\": 47,\n      \"type\": \"issue\",\n      \"title\": \"[FEATURE]\",\n      \"actionability\": \"ready\",\n      \"priorityScore\": 5,\n      \"sentimentScore\": 0,\n      \"needsInfoScore\": 0,\n      \"needsInfoSignals\": [],\n      \"linkedIssues\": [],\n      \"labels\": [\n        \"enhancement\"\n      ],\n      \"notePath\": \".github/maintainer/notes/issues/000/ISSUE-47.md\",\n      \"lastSeenAt\": \"2026-01-17T19:26:09.772Z\"\n    },\n    {\n      \"id\": 48,\n      \"type\": \"issue\",\n      \"title\": \"\",\n      \"actionability\": \"ready\",\n      \"priorityScore\": 10,\n      \"sentimentScore\": 0,\n      \"needsInfoScore\": 0,\n      \"needsInfoSignals\": [],\n      \"linkedIssues\": [],\n      \"labels\": [\n        \"bug\"\n      ],\n      \"notePath\": \".github/maintainer/notes/issues/000/ISSUE-48.md\",\n      \"lastSeenAt\": \"2026-01-17T13:39:40.172Z\"\n    },\n    {\n      \"id\": 50,\n      \"type\": \"issue\",\n      \"title\": \"now i have skills and model api ,but i can' use claude ,should i write workflow to let the model use skill or how can i create a project like claude to use the skills\",\n      \"actionability\": \"ready\",\n      \"priorityScore\": 7,\n      \"sentimentScore\": 0,\n      \"needsInfoScore\": 0,\n      \"needsInfoSignals\": [],\n      \"linkedIssues\": [],\n      \"labels\": [\n        \"enhancement\"\n      ],\n      \"notePath\": \".github/maintainer/notes/issues/000/ISSUE-50.md\",\n      \"lastSeenAt\": \"2026-01-17T19:26:09.772Z\"\n    },\n    {\n      \"id\": 51,\n      \"type\": \"issue\",\n      \"title\": \"\",\n      \"actionability\": \"ready\",\n      \"priorityScore\": 12,\n      \"sentimentScore\": 0,\n      \"needsInfoScore\": 0,\n      \"needsInfoSignals\": [],\n      \"linkedIssues\": [],\n      \"labels\": [\n        \"bug\"\n      ],\n      \"notePath\": \".github/maintainer/notes/issues/000/ISSUE-51.md\",\n      \"lastSeenAt\": \"2026-01-17T13:39:40.172Z\"\n    },\n    {\n      \"id\": 18,\n      \"type\": \"pr\",\n      \"title\": \"\",\n      \"actionability\": \"needs-analysis\",\n      \"priorityScore\": 8,\n      \"implementationScoreAuto\": 18,\n      \"implementationScoreFinal\": 18,\n      \"implementationTierAuto\": \"weak\",\n      \"implementationTierFinal\": \"weak\",\n      \"agentScore\": 0,\n      \"agentConfidence\": \"unset\",\n      \"relationshipScore\": 9,\n      \"relationshipOverlap\": 0,\n      \"relationshipQualityAuto\": \"medium\",\n      \"relationshipQualityFinal\": \"medium\",\n      \"sentimentScore\": 0,\n      \"needsInfoScore\": 1,\n      \"needsInfoSignals\": [\n        \"missing-test-plan\"\n      ],\n      \"linkedIssues\": [\n        17\n      ],\n      \"labels\": [],\n      \"notePath\": \".github/maintainer/notes/prs/000/PR-18.md\",\n      \"lastSeenAt\": \"2026-01-17T13:39:40.172Z\"\n    },\n    {\n      \"id\": 23,\n      \"type\": \"pr\",\n      \"title\": \"\",\n      \"actionability\": \"done\",\n      \"priorityScore\": 0,\n      \"implementationScoreAuto\": 0,\n      \"implementationScoreFinal\": 5,\n      \"implementationTierAuto\": \"weak\",\n      \"implementationTierFinal\": \"weak\",\n      \"agentScore\": 5,\n      \"agentConfidence\": \"medium\",\n      \"relationshipScore\": 0,\n      \"relationshipOverlap\": 0,\n      \"relationshipQualityAuto\": \"none\",\n      \"relationshipQualityFinal\": \"none\",\n      \"sentimentScore\": 0,\n      \"needsInfoScore\": 1,\n      \"needsInfoSignals\": [\n        \"missing-test-plan\"\n      ],\n      \"linkedIssues\": [],\n      \"labels\": [],\n      \"notePath\": \".github/maintainer/notes/prs/000/PR-23.md\",\n      \"lastSeenAt\": \"2026-01-17T18:12:49.3NZ\"\n    },\n    {\n      \"id\": 25,\n      \"type\": \"pr\",\n      \"title\": \"feat: add Nix flake for declarative builds and development\",\n      \"actionability\": \"needs-analysis\",\n      \"priorityScore\": 0,\n      \"implementationScoreAuto\": 0,\n      \"implementationScoreFinal\": 20,\n      \"implementationTierAuto\": \"weak\",\n      \"implementationTierFinal\": \"medium\",\n      \"agentScore\": 20,\n      \"agentConfidence\": \"medium\",\n      \"relationshipScore\": 0,\n      \"relationshipOverlap\": 0,\n      \"relationshipQualityAuto\": \"none\",\n      \"relationshipQualityFinal\": \"none\",\n      \"sentimentScore\": 0,\n      \"needsInfoScore\": 1,\n      \"needsInfoSignals\": [\n        \"missing-test-plan\"\n      ],\n      \"linkedIssues\": [],\n      \"labels\": [],\n      \"notePath\": \".github/maintainer/notes/prs/000/PR-25.md\",\n      \"lastSeenAt\": \"2026-01-17T19:26:09.772Z\"\n    },\n    {\n      \"id\": 26,\n      \"type\": \"pr\",\n      \"title\": \"\",\n      \"actionability\": \"needs-analysis\",\n      \"priorityScore\": 0,\n      \"implementationScoreAuto\": 0,\n      \"implementationScoreFinal\": 0,\n      \"implementationTierAuto\": \"weak\",\n      \"implementationTierFinal\": \"weak\",\n      \"agentScore\": 5,\n      \"agentConfidence\": \"medium\",\n      \"relationshipScore\": 0,\n      \"relationshipOverlap\": 0,\n      \"relationshipQualityAuto\": \"none\",\n      \"relationshipQualityFinal\": \"none\",\n      \"sentimentScore\": 0,\n      \"needsInfoScore\": 1,\n      \"needsInfoSignals\": [\n        \"missing-test-plan\"\n      ],\n      \"linkedIssues\": [],\n      \"labels\": [],\n      \"notePath\": \".github/maintainer/notes/prs/000/PR-26.md\",\n      \"lastSeenAt\": \"2026-01-17T14:59:03.417Z\"\n    },\n    {\n      \"id\": 27,\n      \"type\": \"pr\",\n      \"title\": \"\",\n      \"actionability\": \"needs-analysis\",\n      \"priorityScore\": 0,\n      \"implementationScoreAuto\": 0,\n      \"implementationScoreFinal\": 0,\n      \"implementationTierAuto\": \"weak\",\n      \"implementationTierFinal\": \"weak\",\n      \"agentScore\": 0,\n      \"agentConfidence\": \"high\",\n      \"relationshipScore\": 0,\n      \"relationshipOverlap\": 0,\n      \"relationshipQualityAuto\": \"none\",\n      \"relationshipQualityFinal\": \"none\",\n      \"sentimentScore\": 0,\n      \"needsInfoScore\": 1,\n      \"needsInfoSignals\": [\n        \"missing-test-plan\"\n      ],\n      \"linkedIssues\": [],\n      \"labels\": [],\n      \"notePath\": \".github/maintainer/notes/prs/000/PR-27.md\",\n      \"lastSeenAt\": \"2026-01-17T14:59:03.417Z\"\n    },\n    {\n      \"id\": 30,\n      \"type\": \"pr\",\n      \"title\": \"fix: switch to async spawn to unblock spinner animation and switch fr…\",\n      \"actionability\": \"needs-analysis\",\n      \"priorityScore\": 0,\n      \"implementationScoreAuto\": 0,\n      \"implementationScoreFinal\": 8,\n      \"implementationTierAuto\": \"weak\",\n      \"implementationTierFinal\": \"weak\",\n      \"agentScore\": 15,\n      \"agentConfidence\": \"low\",\n      \"relationshipScore\": 0,\n      \"relationshipOverlap\": 0,\n      \"relationshipQualityAuto\": \"none\",\n      \"relationshipQualityFinal\": \"none\",\n      \"sentimentScore\": 0,\n      \"needsInfoScore\": 1,\n      \"needsInfoSignals\": [\n        \"missing-test-plan\"\n      ],\n      \"linkedIssues\": [],\n      \"labels\": [],\n      \"notePath\": \".github/maintainer/notes/prs/000/PR-30.md\",\n      \"lastSeenAt\": \"2026-01-17T19:26:09.772Z\"\n    },\n    {\n      \"id\": 31,\n      \"type\": \"pr\",\n      \"title\": \"feat: support installing local skills via symlinks with --symlink flag\",\n      \"actionability\": \"needs-analysis\",\n      \"priorityScore\": 0,\n      \"implementationScoreAuto\": 0,\n      \"implementationScoreFinal\": 25,\n      \"implementationTierAuto\": \"weak\",\n      \"implementationTierFinal\": \"medium\",\n      \"agentScore\": 25,\n      \"agentConfidence\": \"medium\",\n      \"relationshipScore\": 0,\n      \"relationshipOverlap\": 0,\n      \"relationshipQualityAuto\": \"none\",\n      \"relationshipQualityFinal\": \"none\",\n      \"sentimentScore\": 0,\n      \"needsInfoScore\": 1,\n      \"needsInfoSignals\": [\n        \"missing-test-plan\"\n      ],\n      \"linkedIssues\": [],\n      \"labels\": [],\n      \"notePath\": \".github/maintainer/notes/prs/000/PR-31.md\",\n      \"lastSeenAt\": \"2026-01-17T19:26:09.772Z\"\n    },\n    {\n      \"id\": 37,\n      \"type\": \"pr\",\n      \"title\": \"\",\n      \"actionability\": \"needs-analysis\",\n      \"priorityScore\": 4,\n      \"implementationScoreAuto\": 116,\n      \"implementationScoreFinal\": 116,\n      \"implementationTierAuto\": \"strong\",\n      \"implementationTierFinal\": \"strong\",\n      \"agentScore\": 70,\n      \"agentConfidence\": \"medium\",\n      \"relationshipScore\": 37,\n      \"relationshipOverlap\": 0,\n      \"relationshipQualityAuto\": \"strong\",\n      \"relationshipQualityFinal\": \"strong\",\n      \"sentimentScore\": 0,\n      \"needsInfoScore\": 1,\n      \"needsInfoSignals\": [\n        \"missing-test-plan\"\n      ],\n      \"linkedIssues\": [\n        20,\n        17,\n        28,\n        29,\n        34\n      ],\n      \"labels\": [],\n      \"notePath\": \".github/maintainer/notes/prs/000/PR-37.md\",\n      \"lastSeenAt\": \"2026-01-17T13:39:40.172Z\"\n    },\n    {\n      \"id\": 38,\n      \"type\": \"pr\",\n      \"title\": \"\",\n      \"actionability\": \"needs-analysis\",\n      \"priorityScore\": 8,\n      \"implementationScoreAuto\": 119,\n      \"implementationScoreFinal\": 262,\n      \"implementationTierAuto\": \"strong\",\n      \"implementationTierFinal\": \"strong\",\n      \"agentScore\": 95,\n      \"agentConfidence\": \"high\",\n      \"relationshipScore\": 39,\n      \"relationshipOverlap\": 0,\n      \"relationshipQualityAuto\": \"strong\",\n      \"relationshipQualityFinal\": \"strong\",\n      \"sentimentScore\": 0,\n      \"needsInfoScore\": 0,\n      \"needsInfoSignals\": [],\n      \"linkedIssues\": [\n        34,\n        28,\n        29,\n        17,\n        20\n      ],\n      \"labels\": [],\n      \"notePath\": \".github/maintainer/notes/prs/000/PR-38.md\",\n      \"lastSeenAt\": \"2026-01-17T13:39:40.172Z\"\n    },\n    {\n      \"id\": 39,\n      \"type\": \"pr\",\n      \"title\": \"feat: implement safe deletion with trash/recycle bin suppor\",\n      \"actionability\": \"needs-analysis\",\n      \"priorityScore\": 0,\n      \"implementationScoreAuto\": 0,\n      \"implementationScoreFinal\": 5,\n      \"implementationTierAuto\": \"weak\",\n      \"implementationTierFinal\": \"weak\",\n      \"agentScore\": 10,\n      \"agentConfidence\": \"low\",\n      \"relationshipScore\": 0,\n      \"relationshipOverlap\": 0,\n      \"relationshipQualityAuto\": \"none\",\n      \"relationshipQualityFinal\": \"none\",\n      \"sentimentScore\": 0,\n      \"needsInfoScore\": 0,\n      \"needsInfoSignals\": [],\n      \"linkedIssues\": [],\n      \"labels\": [],\n      \"notePath\": \".github/maintainer/notes/prs/000/PR-39.md\",\n      \"lastSeenAt\": \"2026-01-17T19:26:09.772Z\"\n    },\n    {\n      \"id\": 40,\n      \"type\": \"pr\",\n      \"title\": \"\",\n      \"actionability\": \"needs-analysis\",\n      \"priorityScore\": 0,\n      \"implementationScoreAuto\": 3,\n      \"implementationScoreFinal\": 3,\n      \"implementationTierAuto\": \"weak\",\n      \"implementationTierFinal\": \"weak\",\n      \"agentScore\": 0,\n      \"agentConfidence\": \"unset\",\n      \"relationshipScore\": 0,\n      \"relationshipOverlap\": 0,\n      \"relationshipQualityAuto\": \"none\",\n      \"relationshipQualityFinal\": \"none\",\n      \"sentimentScore\": 0,\n      \"needsInfoScore\": 1,\n      \"needsInfoSignals\": [\n        \"missing-test-plan\"\n      ],\n      \"linkedIssues\": [],\n      \"labels\": [],\n      \"notePath\": \".github/maintainer/notes/prs/000/PR-40.md\",\n      \"lastSeenAt\": \"2026-01-17T13:39:40.172Z\"\n    },\n    {\n      \"id\": 49,\n      \"type\": \"pr\",\n      \"title\": \"\",\n      \"actionability\": \"done\",\n      \"priorityScore\": 0,\n      \"implementationScoreAuto\": 0,\n      \"implementationScoreFinal\": 20,\n      \"implementationTierAuto\": \"weak\",\n      \"implementationTierFinal\": \"medium\",\n      \"agentScore\": 20,\n      \"agentConfidence\": \"medium\",\n      \"relationshipScore\": 0,\n      \"relationshipOverlap\": 0,\n      \"relationshipQualityAuto\": \"none\",\n      \"relationshipQualityFinal\": \"none\",\n      \"sentimentScore\": 0,\n      \"needsInfoScore\": 1,\n      \"needsInfoSignals\": [\n        \"missing-test-plan\"\n      ],\n      \"linkedIssues\": [],\n      \"labels\": [],\n      \"notePath\": \".github/maintainer/notes/prs/000/PR-49.md\",\n      \"lastSeenAt\": \"2026-01-17T19:10:52.058Z\"\n    }\n  ]\n}\n"
  },
  {
    "path": ".github/maintainer/notes/issues/000/ISSUE-13.md",
    "content": "---\nid: 13\ntype: issue\nstatus: open\nactionability: ready\npriority_score: 5\nsentiment_score: 0\nneeds_info_score: 0\nneeds_info_signals: []\nlabels: [enhancement]\nlast_seen_at: 2026-01-17T19:26:09.772Z\n---\n## Intent\n- TODO: Summarize the reporter intent and underlying need.\n\n## Analysis\n- TODO: Root cause, severity, scope.\n\n## Proposed Action\n- TODO: Implement, ask for info, close, defer.\n\n## Draft Response (requires approval)\n- TODO: Draft the public response.\n"
  },
  {
    "path": ".github/maintainer/notes/issues/000/ISSUE-16.md",
    "content": "---\nid: 16\ntype: issue\nstatus: closed\nactionability: done\npriority_score: 7\nsentiment_score: 0\nneeds_info_score: 0\nneeds_info_signals: []\nlabels: [bug]\nlast_seen_at: 2026-01-17T19:25:46Z\n---\n## Intent\nPrevent AGENTS usage text from triggering a `bash` call in Gemini CLI.\n\n## Analysis\nThe `Bash(\"...\")` example is misinterpreted by Gemini CLI. Updated generated usage text to plain shell invocation.\n\n## Proposed Action\nRespond noting the fix in v1.5.0; close as resolved.\n\n## Draft Response (requires approval)\nPosted and closed after v1.5.0 release.\n\n## Resolution\nClosed as resolved in v1.5.0; usage text updated to plain shell invocation.\n"
  },
  {
    "path": ".github/maintainer/notes/issues/000/ISSUE-17.md",
    "content": "---\nid: 17\ntype: issue\nstatus: open\nactionability: ready\npriority_score: 7\nsentiment_score: 0\nneeds_info_score: 0\nneeds_info_signals: []\nlabels: []\nlast_seen_at: 2026-01-17T13:39:40.172Z\n---\n## Intent\n- TODO: Summarize the reporter intent and underlying need.\n\n## Analysis\n- TODO: Root cause, severity, scope.\n\n## Proposed Action\n- TODO: Implement, ask for info, close, defer.\n\n## Draft Response (requires approval)\n- TODO: Draft the public response.\n"
  },
  {
    "path": ".github/maintainer/notes/issues/000/ISSUE-19.md",
    "content": "---\nid: 19\ntype: issue\nstatus: open\nactionability: ready\npriority_score: 0\nsentiment_score: 0\nneeds_info_score: 0\nneeds_info_signals: []\nlabels: [enhancement]\nlast_seen_at: 2026-01-17T19:26:09.772Z\n---\n## Intent\n- TODO: Summarize the reporter intent and underlying need.\n\n## Analysis\n- TODO: Root cause, severity, scope.\n\n## Proposed Action\n- TODO: Implement, ask for info, close, defer.\n\n## Draft Response (requires approval)\n- TODO: Draft the public response.\n"
  },
  {
    "path": ".github/maintainer/notes/issues/000/ISSUE-20.md",
    "content": "---\nid: 20\ntype: issue\nstatus: open\nactionability: ready\npriority_score: 26\nsentiment_score: 0\nneeds_info_score: 0\nneeds_info_signals: []\nlabels: [bug]\nlast_seen_at: 2026-01-17T13:39:40.172Z\n---\n## Intent\nUser installed `openskills@1.3.0` but CLI reports `1.2.1` and installs fail on Windows with “Security error: Installation path outside target directory.” They need correct version reporting and Windows installs to work.\n\n## Analysis\nTwo issues are bundled here:\n- **Version mismatch**: CLI version is hardcoded to `1.2.1` in `src/cli.ts`, so `dist/cli.js` ships the wrong version string.\n- **Windows install failure**: path validation uses a hardcoded `/` separator, which fails on Windows backslashes.\nSeverity is high: Windows installs are blocked; version mismatch affects all users on 1.3.0.\n\n## Proposed Action\nImplement the Windows path separator fix (`path.sep`) and read CLI version dynamically from `package.json`. Ship a patch release and close duplicates.\n\n## Draft Response (requires approval)\nThanks for the detailed report! You’ve hit two separate bugs: the CLI version string is hardcoded to `1.2.1`, and the Windows install path check used `/` so it always failed on backslashes. I’ve fixed both (dynamic version reading + Windows path separator handling). We’ll ship a patch release and close the related duplicates once it’s published.\n"
  },
  {
    "path": ".github/maintainer/notes/issues/000/ISSUE-24.md",
    "content": "---\nid: 24\ntype: issue\nstatus: open\nactionability: ready\npriority_score: 5\nsentiment_score: 0\nneeds_info_score: 0\nneeds_info_signals: []\nlabels: [enhancement]\nlast_seen_at: 2026-01-17T19:26:09.772Z\n---\n## Intent\n- TODO: Summarize the reporter intent and underlying need.\n\n## Analysis\n- TODO: Root cause, severity, scope.\n\n## Proposed Action\n- TODO: Implement, ask for info, close, defer.\n\n## Draft Response (requires approval)\n- TODO: Draft the public response.\n"
  },
  {
    "path": ".github/maintainer/notes/issues/000/ISSUE-28.md",
    "content": "---\nid: 28\ntype: issue\nstatus: open\nactionability: ready\npriority_score: 50\nagent_score: 100\nagent_confidence: high\nagent_rationale: Critical bug blocking all Windows users. Clear root cause identified. Multiple PRs with working solutions.\nsentiment_score: 0\nneeds_info_score: 0\nneeds_info_signals: []\nlabels: [bug]\nlast_seen_at: 2026-01-17T13:39:40.172Z\n---\n## Intent\nReporter cannot install ANY skills on Windows. Every skill installation fails with \"Security error: Installation path outside target directory\". This is a critical bug blocking Windows platform entirely.\n\n## Analysis\n**Root Cause**: Path security check in `src/commands/install.ts` uses hardcoded forward slash `/`:\n```typescript\nif (!resolvedTargetPath.startsWith(resolvedTargetDir + '/')) {\n```\n\nOn Windows:\n- `path.resolve()` returns paths with backslash `\\` (e.g., `D:\\Desktop\\project\\.claude\\skills`)\n- The check appends forward slash `/` creating mixed separators\n- `startsWith()` always fails because `D:\\...\\skills\\my-skill` doesn't start with `D:\\...\\skills/`\n\n**Severity**: Critical - blocks ALL Windows installations\n**Scope**: 3 locations in install.ts (lines ~197, ~247, ~373)\n**Duplicates**: #34, #20, #43, #48, #17, #29 (7 total issues)\n**PRs with fixes**: #38 (best), #37, #18, #40, #26\n\n## Proposed Action\n1. Implement fix using `path.sep` from Node.js (approach from PR #38)\n2. Update tests to use platform-agnostic path handling\n3. Add Windows-specific test cases\n4. Close all duplicate issues with consolidated response\n5. Close related PRs with thanks to contributors\n\n## Draft Response (requires approval)\n```\nThis is now fixed! The issue was that our path security check used a hardcoded forward slash `/` which doesn't match Windows backslashes `\\`.\n\nThe fix uses Node.js `path.sep` which automatically uses the correct separator for each platform.\n\nThank you @giggitygi for the excellent bug report with root cause analysis, and thanks to everyone who reported duplicates and contributed PRs - especially @didierhk, @letsgitcracking, and @cuiyiming007 whose PRs helped guide the implementation.\n\nThe fix will be in the next release.\n```\n"
  },
  {
    "path": ".github/maintainer/notes/issues/000/ISSUE-29.md",
    "content": "---\nid: 29\ntype: issue\nstatus: open\nactionability: needs-info\npriority_score: 18\nsentiment_score: 0\nneeds_info_score: 5\nneeds_info_signals: [missing-repro, missing-expected-actual, missing-environment, missing-version]\nlabels: [bug]\nlast_seen_at: 2026-01-17T13:39:40.172Z\n---\n## Intent\n- TODO: Summarize the reporter intent and underlying need.\n\n## Analysis\n- TODO: Root cause, severity, scope.\n\n## Proposed Action\n- TODO: Implement, ask for info, close, defer.\n\n## Draft Response (requires approval)\n- TODO: Draft the public response.\n"
  },
  {
    "path": ".github/maintainer/notes/issues/000/ISSUE-32.md",
    "content": "---\nid: 32\ntype: issue\nstatus: open\nactionability: ready\npriority_score: 5\nsentiment_score: 0\nneeds_info_score: 0\nneeds_info_signals: []\nlabels: [enhancement]\nlast_seen_at: 2026-01-17T19:26:09.772Z\n---\n## Intent\n- TODO: Summarize the reporter intent and underlying need.\n\n## Analysis\n- TODO: Root cause, severity, scope.\n\n## Proposed Action\n- TODO: Implement, ask for info, close, defer.\n\n## Draft Response (requires approval)\n- TODO: Draft the public response.\n"
  },
  {
    "path": ".github/maintainer/notes/issues/000/ISSUE-34.md",
    "content": "---\nid: 34\ntype: issue\nstatus: open\nactionability: ready\npriority_score: 27\nsentiment_score: 0\nneeds_info_score: 0\nneeds_info_signals: []\nlabels: []\nlast_seen_at: 2026-01-17T13:39:40.172Z\n---\n## Intent\n- TODO: Summarize the reporter intent and underlying need.\n\n## Analysis\n- TODO: Root cause, severity, scope.\n\n## Proposed Action\n- TODO: Implement, ask for info, close, defer.\n\n## Draft Response (requires approval)\n- TODO: Draft the public response.\n"
  },
  {
    "path": ".github/maintainer/notes/issues/000/ISSUE-35.md",
    "content": "---\nid: 35\ntype: issue\nstatus: open\nactionability: ready\npriority_score: 9\nsentiment_score: 0\nneeds_info_score: 0\nneeds_info_signals: []\nlabels: [enhancement]\nlast_seen_at: 2026-01-17T19:26:09.772Z\n---\n## Intent\n- TODO: Summarize the reporter intent and underlying need.\n\n## Analysis\n- TODO: Root cause, severity, scope.\n\n## Proposed Action\n- TODO: Implement, ask for info, close, defer.\n\n## Draft Response (requires approval)\n- TODO: Draft the public response.\n"
  },
  {
    "path": ".github/maintainer/notes/issues/000/ISSUE-41.md",
    "content": "---\nid: 41\ntype: issue\nstatus: open\nactionability: ready\npriority_score: 5\nsentiment_score: 0\nneeds_info_score: 0\nneeds_info_signals: []\nlabels: [enhancement]\nlast_seen_at: 2026-01-17T19:26:09.772Z\n---\n## Intent\n- TODO: Summarize the reporter intent and underlying need.\n\n## Analysis\n- TODO: Root cause, severity, scope.\n\n## Proposed Action\n- TODO: Implement, ask for info, close, defer.\n\n## Draft Response (requires approval)\n- TODO: Draft the public response.\n"
  },
  {
    "path": ".github/maintainer/notes/issues/000/ISSUE-42.md",
    "content": "---\nid: 42\ntype: issue\nstatus: open\nactionability: ready\npriority_score: 12\nsentiment_score: 0\nneeds_info_score: 0\nneeds_info_signals: []\nlabels: [bug]\nlast_seen_at: 2026-01-17T13:39:40.172Z\n---\n## Intent\nUser installed `openskills@1.3.0` but `openskills --version` still prints `1.2.1`. They want the CLI to report the actual package version and the build/publish process corrected.\n\n## Analysis\nRoot cause: CLI version is hardcoded in `src/cli.ts`, so `dist/cli.js` ships with a stale version string. Scope: all installs of 1.3.0; severity: medium (confusing but not blocking).\n\n## Proposed Action\nRead the version dynamically from `package.json`, rebuild, and ensure release artifacts are updated.\n\n## Draft Response (requires approval)\nConfirmed—`dist/cli.js` is shipping a hardcoded version string. I’ll update the CLI to read the version from `package.json` and publish a patch release so `openskills --version` matches the installed package.\n"
  },
  {
    "path": ".github/maintainer/notes/issues/000/ISSUE-43.md",
    "content": "---\nid: 43\ntype: issue\nstatus: open\nactionability: ready\npriority_score: 20\nsentiment_score: 0\nneeds_info_score: 0\nneeds_info_signals: []\nlabels: [bug]\nlast_seen_at: 2026-01-17T13:39:40.172Z\n---\n## Intent\n- TODO: Summarize the reporter intent and underlying need.\n\n## Analysis\n- TODO: Root cause, severity, scope.\n\n## Proposed Action\n- TODO: Implement, ask for info, close, defer.\n\n## Draft Response (requires approval)\n- TODO: Draft the public response.\n"
  },
  {
    "path": ".github/maintainer/notes/issues/000/ISSUE-47.md",
    "content": "---\nid: 47\ntype: issue\nstatus: open\nactionability: ready\npriority_score: 5\nsentiment_score: 0\nneeds_info_score: 0\nneeds_info_signals: []\nlabels: [enhancement]\nlast_seen_at: 2026-01-17T19:26:09.772Z\n---\n## Intent\n- TODO: Summarize the reporter intent and underlying need.\n\n## Analysis\n- TODO: Root cause, severity, scope.\n\n## Proposed Action\n- TODO: Implement, ask for info, close, defer.\n\n## Draft Response (requires approval)\n- TODO: Draft the public response.\n"
  },
  {
    "path": ".github/maintainer/notes/issues/000/ISSUE-48.md",
    "content": "---\nid: 48\ntype: issue\nstatus: open\nactionability: ready\npriority_score: 10\nsentiment_score: 0\nneeds_info_score: 0\nneeds_info_signals: []\nlabels: [bug]\nlast_seen_at: 2026-01-17T13:39:40.172Z\n---\n## Intent\n- TODO: Summarize the reporter intent and underlying need.\n\n## Analysis\n- TODO: Root cause, severity, scope.\n\n## Proposed Action\n- TODO: Implement, ask for info, close, defer.\n\n## Draft Response (requires approval)\n- TODO: Draft the public response.\n"
  },
  {
    "path": ".github/maintainer/notes/issues/000/ISSUE-50.md",
    "content": "---\nid: 50\ntype: issue\nstatus: open\nactionability: ready\npriority_score: 7\nsentiment_score: 0\nneeds_info_score: 0\nneeds_info_signals: []\nlabels: [enhancement]\nlast_seen_at: 2026-01-17T19:26:09.772Z\n---\n## Intent\n- TODO: Summarize the reporter intent and underlying need.\n\n## Analysis\n- TODO: Root cause, severity, scope.\n\n## Proposed Action\n- TODO: Implement, ask for info, close, defer.\n\n## Draft Response (requires approval)\n- TODO: Draft the public response.\n"
  },
  {
    "path": ".github/maintainer/notes/issues/000/ISSUE-51.md",
    "content": "---\nid: 51\ntype: issue\nstatus: open\nactionability: ready\npriority_score: 12\nsentiment_score: 0\nneeds_info_score: 0\nneeds_info_signals: []\nlabels: [bug]\nlast_seen_at: 2026-01-17T13:39:40.172Z\n---\n## Intent\nUser tried installing a repo with a root-level `SKILL.md` and got “No SKILL.md files found in repository.” They need installs to work when a repo is a single skill at the root.\n\n## Analysis\nRoot cause: `installFromRepo` only searches subdirectories for `SKILL.md` and ignores a root-level `SKILL.md`. This breaks single-skill repos like `dpconde/claude-android-skill`.\n\n## Proposed Action\nTreat root `SKILL.md` as a valid single-skill repo during git installs and use the frontmatter name (fallback to repo name) for the install folder.\n\n## Draft Response (requires approval)\nThanks for the report! The installer only searched subdirectories and missed `SKILL.md` at repo root, which is why your single-skill repo failed. I’ve updated the installer to treat root `SKILL.md` as a valid skill and to install it using the skill’s frontmatter name. This will ship in the next patch release.\n"
  },
  {
    "path": ".github/maintainer/notes/issues/000/ISSUE-6.md",
    "content": "---\nid: 6\ntype: issue\nstatus: closed\nactionability: done\npriority_score: 17\nsentiment_score: 0\nneeds_info_score: 0\nneeds_info_signals: []\nlabels: [enhancement]\nlast_seen_at: 2026-01-17T19:10:52.058Z\n---\n## Intent\nEnable fully non-interactive installs where `--yes` also accepts overwrite prompts.\n\n## Analysis\nShipped in v1.3.0 with non-interactive overwrite behavior. Reporter confirmed working on 2026-01-07.\n\n## Proposed Action\nClose as resolved.\n\n## Draft Response (requires approval)\nPosted and closed after confirmation (v1.3.0/v1.3.1).\n\n## Resolution\nClosed as resolved after reporter confirmation and release fix.\n"
  },
  {
    "path": ".github/maintainer/notes/issues/000/ISSUE-9.md",
    "content": "---\nid: 9\ntype: issue\nstatus: closed\nactionability: done\npriority_score: 17\nsentiment_score: 0\nneeds_info_score: 0\nneeds_info_signals: []\nlabels: [enhancement]\nlast_seen_at: 2026-01-17T19:25:46Z\n---\n## Intent\nProvide a clear workflow for updating skills installed from upstream git repos.\n\n## Analysis\nDocs gap caused confusion for users who expect a direct “update” path.\n\n## Proposed Action\nRespond with v1.5.0 update command + README guidance; close as resolved.\n\n## Draft Response (requires approval)\nPosted with step-by-step update instructions and closed after v1.5.0 release.\n\n## Resolution\nClosed as resolved in v1.5.0; comment includes explicit update steps and re-install guidance.\n"
  },
  {
    "path": ".github/maintainer/notes/prs/000/PR-18.md",
    "content": "---\nid: 18\ntype: pr\nstatus: open\nactionability: needs-analysis\npriority_score: 8\nimplementation_score_auto: 18\nimplementation_score_final: 18\nimplementation_tier_auto: weak\nimplementation_tier_final: weak\nagent_score: 0\nagent_confidence: unset\nagent_rationale: \nrelationship_score: 9\nrelationship_overlap: 0\nrelationship_quality_auto: medium\nrelationship_quality_final: medium\nsentiment_score: 0\nneeds_info_score: 1\nneeds_info_signals: [missing-test-plan]\nlinked_issues: [17]\nlabels: []\nlast_seen_at: 2026-01-17T13:39:40.172Z\n---\n## Intent\n- TODO: Summarize what the PR tries to solve and how.\n\n## Relationship Quality\n- TODO: Does this solve linked issues? Any mismatch? Update relationship_quality_final if needed.\n\n## Agent Score Adjustment\n- TODO: Adjust score based on your judgment. Update agent_score, agent_confidence, agent_rationale.\n\n## Implementation Plan\n- TODO: How you will implement the fix directly.\n\n## Draft Response (requires approval)\n- TODO: Draft the closing response with credit.\n"
  },
  {
    "path": ".github/maintainer/notes/prs/000/PR-23.md",
    "content": "---\nid: 23\ntype: pr\nstatus: closed\nactionability: done\npriority_score: 0\nimplementation_score_auto: 0\nimplementation_score_final: 5\nimplementation_tier_auto: weak\nimplementation_tier_final: weak\nagent_score: 5\nagent_confidence: medium\nagent_rationale: Small README clarification; likely superseded by current README rewrite.\nrelationship_score: 0\nrelationship_overlap: 0\nrelationship_quality_auto: none\nrelationship_quality_final: none\nsentiment_score: 0\nneeds_info_score: 1\nneeds_info_signals: [missing-test-plan]\nlinked_issues: []\nlabels: []\nlast_seen_at: 2026-01-17T18:12:49.3NZ\n---\n## Intent\nClarify that installs are project-local by default so non‑Claude users don’t assume global install behavior.\n\n## Relationship Quality\nNo linked issues; doc clarification only.\n\n## Agent Score Adjustment\nLow priority; the README was recently rewritten and may already address this.\n\n## Implementation Plan\nReview current README and add a short line if the “project install by default” point is still unclear.\n\n## Resolution\nClosed after v1.4.0 clarified project-local default installs in README and installer messaging.\n\n## Draft Response (requires approval)\nThanks for the PR! I recently refreshed the README, so I’ll check if this clarification is still needed and incorporate it if not already covered.\n"
  },
  {
    "path": ".github/maintainer/notes/prs/000/PR-25.md",
    "content": "---\nid: 25\ntype: pr\nstatus: open\nactionability: needs-analysis\npriority_score: 0\nimplementation_score_auto: 0\nimplementation_score_final: 20\nimplementation_tier_auto: weak\nimplementation_tier_final: medium\nagent_score: 20\nagent_confidence: medium\nagent_rationale: Nice-to-have dev ergonomics; no linked issue and adds maintenance surface (flake updates).\nrelationship_score: 0\nrelationship_overlap: 0\nrelationship_quality_auto: none\nrelationship_quality_final: none\nsentiment_score: 0\nneeds_info_score: 1\nneeds_info_signals: [missing-test-plan]\nlinked_issues: []\nlabels: []\nlast_seen_at: 2026-01-17T19:26:09.772Z\n---\n## Intent\nAdd Nix flake support for reproducible builds and dev shells (`nix build`, `nix run`, `nix develop`).\n\n## Relationship Quality\nNo linked issues; relationship quality remains none.\n\n## Agent Score Adjustment\nModerate value for Nix users; low urgency and adds maintenance overhead for lockfile updates.\n\n## Implementation Plan\n1. Decide if we want to support Nix officially.\n2. If yes, add `flake.nix`/`flake.lock` and a brief README note.\n3. Add `.gitignore` entries for `result` and `.direnv`.\n\n## Draft Response (requires approval)\nThanks for the PR! I like the Nix support, but I need to decide if we want to maintain flake files in core. I’ll follow up after that call.\n"
  },
  {
    "path": ".github/maintainer/notes/prs/000/PR-26.md",
    "content": "---\nid: 26\ntype: pr\nstatus: open\nactionability: needs-analysis\npriority_score: 0\nimplementation_score_auto: 0\nimplementation_score_final: 0\nimplementation_tier_auto: weak\nimplementation_tier_final: weak\nagent_score: 5\nagent_confidence: medium\nagent_rationale: Solves the Windows path issue with a different approach and adds extensive tests, but the bug is already fixed in v1.3.1.\nrelationship_score: 0\nrelationship_overlap: 0\nrelationship_quality_auto: none\nrelationship_quality_final: none\nsentiment_score: 0\nneeds_info_score: 1\nneeds_info_signals: [missing-test-plan]\nlinked_issues: []\nlabels: []\nlast_seen_at: 2026-01-17T14:59:03.417Z\n---\n## Intent\nReplace string-based path checks with `path.relative()` for cross-platform safety and add extensive test coverage.\n\n## Relationship Quality\nNot explicitly linked, but effectively addresses the Windows path-validation bug cluster already resolved in v1.3.1.\n\n## Agent Score Adjustment\nGood idea and solid test coverage, but redundant after the shipped fix.\n\n## Implementation Plan\nNo new implementation needed; the shipped fix uses `path.sep` with additional Windows tests.\n\n## Draft Response (requires approval)\nThanks for the thorough PR and test suite! We shipped a fix for Windows path validation in v1.3.1, so I’m closing this as resolved. Your analysis helped validate the root cause and edge cases—appreciate it.\n"
  },
  {
    "path": ".github/maintainer/notes/prs/000/PR-27.md",
    "content": "---\nid: 27\ntype: pr\nstatus: open\nactionability: needs-analysis\npriority_score: 0\nimplementation_score_auto: 0\nimplementation_score_final: 0\nimplementation_tier_auto: weak\nimplementation_tier_final: weak\nagent_score: 0\nagent_confidence: high\nagent_rationale: Obsolete; version is now dynamic and beyond 1.3.0.\nrelationship_score: 0\nrelationship_overlap: 0\nrelationship_quality_auto: none\nrelationship_quality_final: none\nsentiment_score: 0\nneeds_info_score: 1\nneeds_info_signals: [missing-test-plan]\nlinked_issues: []\nlabels: []\nlast_seen_at: 2026-01-17T14:59:03.417Z\n---\n## Intent\nBump the CLI version string to 1.3.0.\n\n## Relationship Quality\nNo linked issues; superseded by the dynamic version fix in v1.3.1.\n\n## Agent Score Adjustment\nNo longer relevant; version is read from package.json now.\n\n## Implementation Plan\nNone needed; already solved by dynamic versioning.\n\n## Draft Response (requires approval)\nThanks for the PR! We’ve switched to reading the version directly from package.json (v1.3.1), so this manual bump is no longer needed. Closing with appreciation for the help.\n"
  },
  {
    "path": ".github/maintainer/notes/prs/000/PR-30.md",
    "content": "---\nid: 30\ntype: pr\nstatus: open\nactionability: needs-analysis\npriority_score: 0\nimplementation_score_auto: 0\nimplementation_score_final: 8\nimplementation_tier_auto: weak\nimplementation_tier_final: weak\nagent_score: 15\nagent_confidence: low\nagent_rationale: Improves UX but swaps dependencies and changes process execution; not tied to an issue and overlaps with recent fixes.\nrelationship_score: 0\nrelationship_overlap: 0\nrelationship_quality_auto: none\nrelationship_quality_final: none\nsentiment_score: 0\nneeds_info_score: 1\nneeds_info_signals: [missing-test-plan]\nlinked_issues: []\nlabels: []\nlast_seen_at: 2026-01-17T19:26:09.772Z\n---\n## Intent\nImprove UX during git clone by using async spawn (so the spinner animates) and swap `ora` for `nanospinner`. Also claims to fix a JSON import warning.\n\n## Relationship Quality\nNo linked issues; relationship quality remains none.\n\n## Agent Score Adjustment\nLow confidence because it introduces a dependency change and touches core install flow without a strong demand signal.\n\n## Implementation Plan\n1. Decide if spinner UX is a priority.\n2. If yes, switch clone to `spawn` while keeping `ora` (avoid dependency swap unless needed).\n3. Verify no regression in error handling for git clone failures.\n4. Update tests if necessary (none currently).\n\n## Draft Response (requires approval)\nThanks for the PR! I need to decide whether we want to change the spinner/dependency and async clone behavior. I’ll circle back after reviewing UX priorities and risk.\n"
  },
  {
    "path": ".github/maintainer/notes/prs/000/PR-31.md",
    "content": "---\nid: 31\ntype: pr\nstatus: open\nactionability: needs-analysis\npriority_score: 0\nimplementation_score_auto: 0\nimplementation_score_final: 25\nimplementation_tier_auto: weak\nimplementation_tier_final: medium\nagent_score: 25\nagent_confidence: medium\nagent_rationale: Useful dev workflow enhancement, but overlaps with existing manual symlink guidance; needs decision on CLI support.\nrelationship_score: 0\nrelationship_overlap: 0\nrelationship_quality_auto: none\nrelationship_quality_final: none\nsentiment_score: 0\nneeds_info_score: 1\nneeds_info_signals: [missing-test-plan]\nlinked_issues: []\nlabels: []\nlast_seen_at: 2026-01-17T19:26:09.772Z\n---\n## Intent\nAdd a `--symlink` flag to install local skills by creating symlinks instead of copying, enabling live updates from a dev repo.\n\n## Relationship Quality\nNo linked issues. Relationship quality remains none.\n\n## Agent Score Adjustment\nThis is a quality-of-life improvement for local development. Medium confidence, pending a decision on supporting symlink installs in core.\n\n## Implementation Plan\n1. Add `--symlink` option to `install` command and `InstallOptions`.\n2. In local installs, create symlink instead of `cpSync` when flag is set.\n3. Add tests for symlink behavior (happy path + broken link handling).\n4. Update README with short example.\n\n## Draft Response (requires approval)\nThanks for the PR! The symlink workflow is helpful. I’m deciding whether we want to support this via a flag (vs the current manual symlink guidance). I’ll follow up once that decision is made.\n"
  },
  {
    "path": ".github/maintainer/notes/prs/000/PR-37.md",
    "content": "---\nid: 37\ntype: pr\nstatus: open\nactionability: needs-analysis\npriority_score: 4\nimplementation_score_auto: 116\nimplementation_score_final: 116\nimplementation_tier_auto: strong\nimplementation_tier_final: strong\nagent_score: 70\nagent_confidence: medium\nagent_rationale: Fixes both Windows path validation and CLI version reporting, but overlaps with PR #38 and has weaker test coverage/detail.\nrelationship_score: 37\nrelationship_overlap: 0\nrelationship_quality_auto: strong\nrelationship_quality_final: strong\nsentiment_score: 0\nneeds_info_score: 1\nneeds_info_signals: [missing-test-plan]\nlinked_issues: [20, 17, 28, 29, 34]\nlabels: []\nlast_seen_at: 2026-01-17T13:39:40.172Z\n---\n## Intent\nFix Windows install failures by replacing hardcoded `/` with `path.relative()`-based validation, and fix CLI version output by reading from `package.json`.\n\n## Relationship Quality\nStrong — addresses the Windows path error and the version mismatch reported in #20/#42, plus duplicates #17/#28/#29/#34.\n\n## Agent Score Adjustment\nSolid approach with two fixes, but PR #38 is cleaner for the path check and includes clearer tests. Keep this as a reference for the version fix.\n\n## Implementation Plan\n1. Apply the Windows path fix using `path.sep` (per PR #38) in `src/commands/install.ts`.\n2. Update tests to exercise cross-platform separators.\n3. Pull in the dynamic version read from this PR (`src/cli.ts`).\n\n## Draft Response (requires approval)\nThanks for the PR! I’m implementing the fixes directly: using a cross-platform path check to fix Windows installs, and reading the CLI version from `package.json`. I’ll credit this PR in the release notes when the patch ships.\n"
  },
  {
    "path": ".github/maintainer/notes/prs/000/PR-38.md",
    "content": "---\nid: 38\ntype: pr\nstatus: open\nactionability: needs-analysis\npriority_score: 8\nimplementation_score_auto: 119\nimplementation_score_final: 262\nimplementation_tier_auto: strong\nimplementation_tier_final: strong\nagent_score: 95\nagent_confidence: high\nagent_rationale: Best quality Windows fix PR. Clean, focused changes. Well-documented. Excellent test coverage. Use this approach.\nrelationship_score: 39\nrelationship_overlap: 0\nrelationship_quality_auto: strong\nrelationship_quality_final: strong\nsentiment_score: 0\nneeds_info_score: 0\nneeds_info_signals: []\nlinked_issues: [34, 28, 29, 17, 20]\nlabels: []\nlast_seen_at: 2026-01-17T13:39:40.172Z\n---\n## Intent\nFix Windows path security check by using `path.sep` instead of hardcoded `/`. The PR correctly identifies the root cause and provides a clean, minimal fix.\n\n**What it does**:\n1. Imports `sep` from `path` module\n2. Replaces 3 occurrences of `'/'` with `sep` in security checks\n3. Updates tests to use `sep` and `resolve()` for cross-platform compatibility\n4. Adds Windows-specific test cases\n\n## Relationship Quality\n**Strong** - This PR directly solves all linked issues (#28, #34, #29, #17, #20). The approach is correct and maintains security guarantees on all platforms.\n\n## Agent Score Adjustment\n**Score: 95** (high confidence)\n\nRationale:\n- Best quality of all Windows fix PRs\n- Clean, focused changes (+35/-12 lines)\n- Well-documented with clear root cause analysis\n- Includes proper test coverage\n- Uses the correct Node.js approach (`path.sep`)\n\n## Implementation Plan\n1. Apply the `path.sep` changes from this PR to `src/commands/install.ts`\n2. Update `tests/commands/install.test.ts` with the test improvements\n3. Also include version fix from PR #37 (read from package.json)\n4. Run tests to verify\n5. Close this PR and related PRs with thanks\n\n## Draft Response (requires approval)\n```\nThank you @didierhk for this excellent PR! Your root cause analysis was spot-on, and the `path.sep` solution is exactly the right approach.\n\nI've implemented the fix based on your approach:\n- Using `path.sep` for cross-platform path validation in all 3 security check locations\n- Updating tests to use `sep` and `resolve()` for proper cross-platform testing\n- Adding the Windows-specific test cases you outlined\n\nThis fix will unblock all Windows users. Really appreciate your contribution to making openskills work across all platforms!\n\nClosing this PR as the fix has been merged.\n```\n"
  },
  {
    "path": ".github/maintainer/notes/prs/000/PR-39.md",
    "content": "---\nid: 39\ntype: pr\nstatus: open\nactionability: needs-analysis\npriority_score: 0\nimplementation_score_auto: 0\nimplementation_score_final: 5\nimplementation_tier_auto: weak\nimplementation_tier_final: weak\nagent_score: 10\nagent_confidence: low\nagent_rationale: Feature is useful but introduces a new dependency and changes deletion semantics; needs maintainer decision due to platform risk (Linux trash support).\nrelationship_score: 0\nrelationship_overlap: 0\nrelationship_quality_auto: none\nrelationship_quality_final: none\nsentiment_score: 0\nneeds_info_score: 0\nneeds_info_signals: []\nlinked_issues: []\nlabels: []\nlast_seen_at: 2026-01-17T19:26:09.772Z\n---\n## Intent\nPrevent accidental permanent deletions by moving skills to the system trash/recycle bin by default. Adds a `--force` flag for permanent deletes and introduces a `trash` helper.\n\n## Relationship Quality\nNo linked issue; behavior change proposal. Relationship quality remains none.\n\n## Agent Score Adjustment\nLow confidence due to dependency risk and behavior change (Linux support caveats). Needs maintainer decision before implementation.\n\n## Implementation Plan\n1. Decide whether default deletion should be “trash” or remain permanent.\n2. If accepted, add `trash` dependency and a small wrapper (or optional dependency) with graceful fallback on Linux.\n3. Add `--force` (or `--permanent`) flag to `remove` and `manage`.\n4. Add tests around deletion mode selection (avoid actual trash in tests).\n5. Update README + changelog for behavior change.\n\n## Draft Response (requires approval)\nThanks for the PR! I like the safety angle, but this changes deletion semantics and adds a dependency with Linux caveats. I need to decide if we want this behavior in core. I’ll follow up once I make that call.\n"
  },
  {
    "path": ".github/maintainer/notes/prs/000/PR-40.md",
    "content": "---\nid: 40\ntype: pr\nstatus: open\nactionability: needs-analysis\npriority_score: 0\nimplementation_score_auto: 3\nimplementation_score_final: 3\nimplementation_tier_auto: weak\nimplementation_tier_final: weak\nagent_score: 0\nagent_confidence: unset\nagent_rationale: \nrelationship_score: 0\nrelationship_overlap: 0\nrelationship_quality_auto: none\nrelationship_quality_final: none\nsentiment_score: 0\nneeds_info_score: 1\nneeds_info_signals: [missing-test-plan]\nlinked_issues: []\nlabels: []\nlast_seen_at: 2026-01-17T13:39:40.172Z\n---\n## Intent\n- TODO: Summarize what the PR tries to solve and how.\n\n## Relationship Quality\n- TODO: Does this solve linked issues? Any mismatch? Update relationship_quality_final if needed.\n\n## Agent Score Adjustment\n- TODO: Adjust score based on your judgment. Update agent_score, agent_confidence, agent_rationale.\n\n## Implementation Plan\n- TODO: How you will implement the fix directly.\n\n## Draft Response (requires approval)\n- TODO: Draft the closing response with credit.\n"
  },
  {
    "path": ".github/maintainer/notes/prs/000/PR-49.md",
    "content": "---\nid: 49\ntype: pr\nstatus: closed\nactionability: done\npriority_score: 0\nimplementation_score_auto: 0\nimplementation_score_final: 20\nimplementation_tier_auto: weak\nimplementation_tier_final: medium\nagent_score: 20\nagent_confidence: medium\nagent_rationale: Small doc-quality improvement that reduces onboarding friction; low risk but not tied to a concrete issue.\nrelationship_score: 0\nrelationship_overlap: 0\nrelationship_quality_auto: none\nrelationship_quality_final: none\nsentiment_score: 0\nneeds_info_score: 1\nneeds_info_signals: [missing-test-plan]\nlinked_issues: []\nlabels: []\nlast_seen_at: 2026-01-17T19:10:52.058Z\n---\n## Intent\nAdd a line to AGENTS.md usage instructions telling users to install OpenSkills if it’s missing. This is a documentation nudge to help new developers in repos where AGENTS.md already exists.\n\n## Relationship Quality\nNo linked issues; doc-only improvement. Relationship quality remains none.\n\n## Agent Score Adjustment\nLow-impact doc change. Could be accepted with slight wording to avoid implying global install is required for all environments.\n\n## Implementation Plan\n1. Update `src/utils/agents-md.ts` usage block with a neutral note: “If openskills isn’t available, install it (e.g., `npm i -g openskills` or `npx openskills`).”\n2. Regenerate AGENTS.md via `openskills sync` if needed.\n3. No tests required.\n\n## Resolution\nClosed after v1.5.0. README now clarifies `npx` usage and that install is optional; AGENTS.md not updated.\n\n## Draft Response (requires approval)\nThanks for the PR! We standardized on `npx` examples and want install to remain optional (no requirement for global install). I added a small README tip for human usage and closed this out in v1.5.0.\n"
  },
  {
    "path": ".github/maintainer/patterns.md",
    "content": "# Observed Patterns\n\n## Recurring Issues\n\n### Windows Path Handling\n- **First seen:** 2025-12-15\n- **Frequency:** 8 duplicate reports\n- **Root cause:** Hardcoded `/` instead of `path.sep`\n- **Resolution:** Fixed in v1.3.1\n- **Prevention:** Added Windows CI\n\n### Version Display Mismatch\n- **First seen:** 2025-12-19\n- **Cause:** Hardcoded version string not updated\n- **Resolution:** Read from package.json dynamically\n\n### Root SKILL.md Not Detected\n- **First seen:** 2026-01-17\n- **Cause:** Installer only searched subdirectories for SKILL.md\n- **Resolution:** Treat repo-root SKILL.md as a single-skill repo (v1.3.1)\n\n## Contributor Patterns\n\n- Chinese-speaking user base is significant (consider i18n)\n- Contributors often submit multiple PRs for same issue (need faster review)\n\n## Codebase Patterns\n\n- Most bugs cluster in `src/cli/` (needs refactoring)\n- Test coverage gaps in error handling paths\n"
  },
  {
    "path": ".github/maintainer/release-checklist.md",
    "content": "# Release Checklist\n\nUse this when shipping a new OpenSkills version so we do not repeat the same guidance every time.\n\n## 1) Scope + Approval\n- Confirm the release type (patch/minor/major) and get approval for public actions.\n- Verify the change list and whether any docs or messages need updates.\n\n## 2) Versioning + Changelog\n- Bump `package.json` + `package-lock.json` (use `npm version X.Y.Z --no-git-tag-version`).\n- Update `CHANGELOG.md` with date + bullet list.\n\n## 3) Build + Tests\n- Run `npm run build`.\n- Run `npm test` (or `npm run prepublishOnly`).\n\n## 4) Commit + Push\n- Stage only release-related files.\n- Commit message: `Release vX.Y.Z`.\n- Push branch.\n\n## 5) Release + Publish\n- Tag: `git tag vX.Y.Z` then `git push origin vX.Y.Z`.\n- GitHub release notes from `CHANGELOG.md`.\n- Publish to npm: `npm publish`.\n\n## 6) Post-Release\n- Close related issues/PRs with confirmation.\n- Update `.github/maintainer/decisions.md` and any relevant notes.\n"
  },
  {
    "path": ".github/maintainer/runs.md",
    "content": "# Run Ledger\n\n| Date | Report Path | Summary |\n|------|-------------|---------|\n\n- 2026-01-17T13:12:54.389Z | reports/2026-01-17T13-12-54 | issues:20 prs:12\n- 2026-01-17T13:39:40.172Z | reports/2026-01-17T13-39-40 | issues:20 prs:12\n- 2026-01-17T14:59:03.417Z | reports/2026-01-17T14-59-03 | issues:11 prs:8\n- 2026-01-17T17:32:44.575Z | reports/2026-01-17T17-32-44 | issues:11 prs:6\n- 2026-01-17T18:18:54.518Z | reports/2026-01-17T18-18-54 | issues:11 prs:5\n- 2026-01-17T19:10:52.058Z | reports/2026-01-17T19-10-52 | issues:10 prs:4\n- 2026-01-17T19:26:09.772Z | reports/2026-01-17T19-26-09 | issues:8 prs:4\n"
  },
  {
    "path": ".github/maintainer/semantics.generated.json",
    "content": "{\n  \"schemaVersion\": 1,\n  \"generatedAt\": \"2026-01-17T13:12:54.700Z\",\n  \"sources\": [\n    \"/Users/numman/Repos/openskills/.github/ISSUE_TEMPLATE/bug_report.md\",\n    \"/Users/numman/Repos/openskills/.github/ISSUE_TEMPLATE/config.yml\",\n    \"/Users/numman/Repos/openskills/.github/ISSUE_TEMPLATE/feature_request.md\",\n    \"/Users/numman/Repos/openskills/CONTRIBUTING.md\"\n  ],\n  \"overrides\": {\n    \"semantics\": {\n      \"intent\": {\n        \"bug\": [\n          \"bug description\"\n        ],\n        \"feature\": [\n          \"feature description\",\n          \"feature requests\"\n        ],\n        \"question\": [\n          \"questions?\"\n        ],\n        \"support\": [],\n        \"meta\": [\n          \"contributing: open a discussion thread\"\n        ]\n      },\n      \"needsInfo\": {\n        \"repro\": [\n          \"steps to reproduce\",\n          \"provide clear reproduction steps\"\n        ],\n        \"expected\": [\n          \"expected behavior\"\n        ],\n        \"actual\": [\n          \"actual behavior\"\n        ],\n        \"environment\": [\n          \"environment\",\n          \"operating system\"\n        ],\n        \"version\": [\n          \"openskills version\",\n          \"node.js version\",\n          \"include version information\"\n        ],\n        \"logs\": [],\n        \"testPlan\": [\n          \"testing: include tests for new functionality\",\n          \"link locally for testing\",\n          \"testing changes\"\n        ]\n      },\n      \"environmentTokens\": [\n        \"windows\",\n        \"win11\",\n        \"win10\",\n        \"mac\",\n        \"macos\",\n        \"linux\",\n        \"ubuntu\",\n        \"debian\",\n        \"node\",\n        \"npm\",\n        \"pnpm\",\n        \"yarn\"\n      ],\n      \"relationship\": {\n        \"linkKeywords\": [],\n        \"duplicateHints\": []\n      },\n      \"errors\": {\n        \"signatures\": [],\n        \"keywords\": []\n      }\n    }\n  }\n}\n"
  },
  {
    "path": ".github/maintainer/standing-rules.md",
    "content": "# Standing Rules\n\n## Stale Policy\n\n| Condition | Days | Action |\n|-----------|------|--------|\n| Issue waiting on reporter | 30 | Comment asking for update |\n| Issue waiting on reporter | 60 | Close as stale |\n| PR waiting on author | 30 | Close as stale |\n\n## Auto-Labels\n\n| Condition | Label |\n|-----------|-------|\n| PR touches `src/core/` | `core` |\n| Issue mentions \"windows\" | `platform:windows` |\n| First-time contributor | `first-contribution` |\n\n## External PR Handling\n\n- Never merge external PRs\n- Extract intent and implement fixes directly\n- Close PRs with explanation and credit\n\n"
  },
  {
    "path": ".github/maintainer/state.json",
    "content": "{\n  \"schemaVersion\": 1,\n  \"lastRunAt\": \"2026-01-17T19:26:11.736Z\",\n  \"lastReportDir\": \"reports/2026-01-17T19-26-09\",\n  \"issueHashes\": {\n    \"13\": \"4efd4ad0\",\n    \"19\": \"66c95529\",\n    \"24\": \"6c7eeae3\",\n    \"32\": \"7d02d549\",\n    \"35\": \"235f4981\",\n    \"41\": \"6ad719fb\",\n    \"47\": \"519321cb\",\n    \"50\": \"689f74c9\"\n  },\n  \"prHashes\": {\n    \"25\": \"3e79e440\",\n    \"30\": \"4bcb7033\",\n    \"31\": \"49a3992\",\n    \"39\": \"7652e623\"\n  }\n}\n"
  },
  {
    "path": ".github/maintainer/work/agent-briefs.md",
    "content": "# Agent Briefs\n\nGenerated: 2026-01-17\nRepository: numman-ali/openskills\n\n## Brief 1: Decide on Safe Deletion (PR #39)\n\n**Intent**: Evaluate whether `remove`/`manage` should trash by default (with `--force` for permanent delete) and whether adding the `trash` dependency is acceptable.\n\n**Context**:\n- PR #39 adds `trash` dependency + `--force` flag\n- Linux support caveats noted by dependency author\n\n**Acceptance Criteria** (if accepted):\n- Default deletion is explicit and documented\n- `--force` provides permanent deletion\n- Behavior is consistent across OSes (with graceful fallback)\n\n**Constraints**:\n- Avoid breaking workflows unexpectedly\n- Prefer minimal dependency risk\n\n**Approval needed**: Yes\n\n---\n\n## Brief 2: Decide on Symlink Install Flag (PR #31)\n\n**Intent**: Decide whether to support `--symlink` for local installs to enable live updates.\n\n**Context**:\n- Manual symlink workflow already documented\n- PR adds flag + tests\n\n**Acceptance Criteria** (if accepted):\n- `openskills install ./path --symlink` creates symlink\n- Default behavior remains copy\n- Tests cover symlink and broken link scenarios\n\n**Approval needed**: Yes\n\n---\n\n## Brief 3: Decide on Nix Flake Support (PR #25)\n\n**Intent**: Decide if the repo should ship a Nix flake for dev/build ergonomics.\n\n**Context**:\n- Adds `flake.nix`, `flake.lock`, README snippet\n- Requires ongoing maintenance\n\n**Approval needed**: Yes\n\n---\n\n## Brief 4: Triage Gemini CLI Behavior (ISSUE #16)\n\n**Intent**: Understand why `Bash(\"openskills read <skill-name>\")` affects Gemini CLI behavior.\n\n**Information Needed**:\n- Exact repro steps\n- Agent/CLI versions\n- Any logs or stderr output\n\n**Approval needed**: Yes (response)\n\n---\n\n## Brief 5: Guidance for Non‑Claude Users (ISSUE #50)\n\n**Intent**: Provide clear steps for using OpenSkills with other agents.\n\n**Draft Guidance**:\n- Install skills (`openskills install ...`)\n- Sync to AGENTS.md (`openskills sync`)\n- Ensure the agent reads AGENTS.md and can run `openskills read`\n\n**Approval needed**: Yes (response)\n\n---\n\n## Brief 6: Clarify Skill Update Workflow (ISSUE #9)\n\n**Intent**: Determine if we should document an upstream sync workflow or provide a script.\n\n**Questions**:\n- Are they asking for auto‑sync or a manual update recipe?\n- Should this live in README or a separate doc?\n\n**Approval needed**: Yes\n"
  },
  {
    "path": ".github/maintainer/work/agent-prompts.md",
    "content": "# Agent Prompts\n\nUse this file to draft executable prompts for each brief.\n"
  },
  {
    "path": ".github/maintainer/work/opportunities.md",
    "content": "# Opportunity Backlog\n\n- Add a short FAQ/troubleshooting section (install modes, AGENTS.md, “skill not found”).\n- Add a “default install is project-local” clarification in README (if still ambiguous).\n- Consider a brief guide for non-Claude agents (Cursor/Windsurf/Aider/Codex).\n"
  },
  {
    "path": ".github/maintainer/work/queue.md",
    "content": "# Maintainer Queue\n\n## Recently Resolved (v1.3.1)\n\n- Windows install path validation (issues #28, #34, #43, #48, #17, #29)\n- CLI version mismatch (#20, #42)\n- Root SKILL.md install failure (#51)\n\n---\n\n## Priority 1: Triage/Support Backlog\n\n| Issue | Title | Recommended Action |\n|-------|-------|--------------------|\n| #9 | Document workflow/create script for updating skills | Ask for clarity + consider docs brief |\n| #6 | --yes should overwrite | Close as resolved by current behavior or clarify scope |\n| #35 | Core contributor request | Respond with contributor guidelines |\n| #16 | Gemini CLI behavior | Investigate + request repro |\n| #50 | How to use skills outside Claude | Provide guidance (AGENTS.md + OpenSkills flow) |\n\n---\n\n## Priority 2: Open PRs (Analysis Summary)\n\n| PR | Title | Recommendation |\n|----|-------|----------------|\n| #49 | Auto-install note in AGENTS.md | Acceptable doc tweak; implement with neutral wording |\n| #39 | Safe deletion with trash | Needs maintainer decision (dependency + behavior change) |\n| #31 | Symlink install flag | Nice-to-have; decide if CLI should support symlink installs |\n| #30 | Async spawn + spinner swap | Low priority; reconsider dependency swap |\n| #27 | Version update to 1.3.0 | Obsolete; close with thanks |\n| #26 | Path.relative + tests | Redundant; close with thanks (fixed in v1.3.1) |\n| #25 | Nix flake | Optional; decide if we want official Nix support |\n| #23 | README clarification | Likely superseded by recent README rewrite |\n\n---\n\n## Priority 3: Opportunities\n\n- Add a short “Troubleshooting/FAQ” section (install modes, AGENTS.md, agent compatibility).\n- Clarify default install scope (“project” vs “global”) in README if still confusing.\n"
  },
  {
    "path": ".github/workflows/ci.yml",
    "content": "name: CI\n\non:\n  push:\n    branches: [main]\n  pull_request:\n    branches: [main]\n\njobs:\n  test:\n    name: Test on Node.js ${{ matrix.node-version }}\n    runs-on: ubuntu-latest\n\n    strategy:\n      matrix:\n        node-version: [20.x, 22.x]\n\n    steps:\n      - name: Checkout code\n        uses: actions/checkout@v4\n\n      - name: Setup Node.js ${{ matrix.node-version }}\n        uses: actions/setup-node@v4\n        with:\n          node-version: ${{ matrix.node-version }}\n          cache: 'npm'\n\n      - name: Install dependencies\n        run: npm ci\n\n      - name: Run type check\n        run: npm run typecheck\n\n      - name: Build\n        run: npm run build\n\n      - name: Run tests\n        run: npm test\n\n      - name: Test CLI\n        run: |\n          npm link\n          openskills --version\n          openskills list || true\n"
  },
  {
    "path": ".gitignore",
    "content": "node_modules/\ndist/\n.DS_Store\n*.log\n.openskills-temp/\n.claude/\nskills/\n.tmp/\n.venv-skill/\nreports/\n"
  },
  {
    "path": ".npmignore",
    "content": ".git\n.gitignore\n.DS_Store\nnode_modules/\nsrc/\ntests/\n.github/\n*.log\n.vscode/\n.idea/\ntsconfig.json\ntsup.config.ts\nvitest.config.ts\n.claude/\nAGENTS.md\n"
  },
  {
    "path": "AGENTS.md",
    "content": "# AGENTS\n\n<skills_system priority=\"1\">\n\n## Available Skills\n\n<!-- SKILLS_TABLE_START -->\n<usage>\nWhen users ask you to perform tasks, check if any of the available skills below can help complete the task more effectively. Skills provide specialized capabilities and domain knowledge.\n\nHow to use skills:\n- Invoke: Bash(\"npx openskills read <skill-name>\")\n- The skill content will load with detailed instructions on how to complete the task\n- Base directory provided in output for resolving bundled resources (references/, scripts/, assets/)\n\nUsage notes:\n- Only use skills listed in <available_skills> below\n- Do not invoke a skill that is already loaded in your context\n- Each skill invocation is stateless\n</usage>\n\n<available_skills>\n\n<skill>\n<name>open-source-maintainer</name>\n<description>End-to-end GitHub repository maintenance for open-source projects. Use when asked to triage issues, review PRs, analyze contributor activity, generate maintenance reports, or maintain a repository. Triggers include \"triage\", \"maintain\", \"review PRs\", \"analyze issues\", \"repo maintenance\", \"what needs attention\", \"open source maintenance\", or any request to understand and act on GitHub issues/PRs. Supports human-in-the-loop workflows with persistent memory across sessions.</description>\n<location>project</location>\n</skill>\n\n</available_skills>\n<!-- SKILLS_TABLE_END -->\n\n</skills_system>\n"
  },
  {
    "path": "CHANGELOG.md",
    "content": "# Changelog\n\nAll notable changes to this project will be documented in this file.\n\nThe format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/),\nand this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).\n\n## [1.5.0] - 2026-01-17\n\n### Added\n\n- **`openskills update`** - Refresh installed skills from their recorded source (default: all)\n- **Source metadata tracking** - Install now records origin info for reliable updates\n\n### Changed\n\n- **Multi-skill read** - `openskills read` supports comma-separated names\n- **Generated usage text** - Clarified read invocation for shell usage\n- **README** - Added update guidance and human usage tips\n\n### Fixed\n\n- **Update UX** - Skips skills without source metadata and lists them for re-install\n\n## [1.3.0] - 2025-12-14\n\n### Added\n\n- **Symlink support** - Skills can now be symlinked into the skills directory ([#3](https://github.com/numman-ali/openskills/issues/3))\n  - Enables git-based skill updates by symlinking from a cloned repo\n  - Supports local skill development workflows\n  - Broken symlinks are gracefully skipped\n\n- **Configurable output path** - New `--output` / `-o` flag for sync command ([#5](https://github.com/numman-ali/openskills/issues/5))\n  - Sync to any `.md` file (e.g., `.ruler/AGENTS.md`)\n  - Auto-creates file with heading if it doesn't exist\n  - Auto-creates nested directories if needed\n\n- **Local path installation** - Install skills from local directories ([#10](https://github.com/numman-ali/openskills/issues/10))\n  - Supports absolute paths (`/path/to/skill`)\n  - Supports relative paths (`./skill`, `../skill`)\n  - Supports tilde expansion (`~/my-skills/skill`)\n\n- **Private git repo support** - Install from private repositories ([#10](https://github.com/numman-ali/openskills/issues/10))\n  - SSH URLs (`git@github.com:org/private-skills.git`)\n  - HTTPS with authentication\n  - Uses system SSH keys automatically\n\n- **Comprehensive test suite** - 88 tests across 6 test files\n  - Unit tests for symlink detection, YAML parsing\n  - Integration tests for install, sync commands\n  - E2E tests for full CLI workflows\n\n### Changed\n\n- **`--yes` flag now skips all prompts** - Fully non-interactive mode for CI/CD ([#6](https://github.com/numman-ali/openskills/issues/6))\n  - Overwrites existing skills without prompting\n  - Shows `Overwriting: <skill-name>` message when skipping prompt\n  - All commands now work in headless environments\n\n- **CI workflow reordered** - Build step now runs before tests\n  - Ensures `dist/cli.js` exists for E2E tests\n\n### Security\n\n- **Path traversal protection** - Validates installation paths stay within target directory\n- **Symlink dereference** - `cpSync` uses `dereference: true` to safely copy symlink targets\n- **Non-greedy YAML regex** - Prevents potential ReDoS in frontmatter parsing\n\n## [1.3.1] - 2026-01-17\n\n### Fixed\n\n- **Windows installs** - Path validation now works on Windows (`Security error: Installation path outside target directory`)\n- **CLI version** - `npx openskills --version` reads from package.json\n- **Root SKILL.md** - Single-skill repos with SKILL.md in repo root now install correctly\n\n## [1.4.0] - 2026-01-17\n\n### Changed\n\n- **README** - Clarify project-local default installs and remove redundant sync note\n- **Installer messages** - Call out project-local default vs `--global` explicitly\n\n## [1.3.2] - 2026-01-17\n\n### Changed\n\n- **Docs & AGENTS.md guidance** - Use `npx openskills` for all command examples and generated usage text\n\n## [1.2.1] - 2025-10-27\n\n### Fixed\n\n- README documentation cleanup - removed duplicate sections and incorrect flags\n\n## [1.2.0] - 2025-10-27\n\n### Added\n\n- `--universal` flag to install skills to `.agent/skills/` instead of `.claude/skills/`\n  - For multi-agent setups (Claude Code + Cursor/Windsurf/Aider)\n  - Avoids conflicts with Claude Code's native marketplace plugins\n\n### Changed\n\n- Project install is now the default (was global)\n- Skills install to `./.claude/skills/` by default\n\n## [1.1.0] - 2025-10-27\n\n### Added\n\n- Comprehensive single-page README with technical deep dive\n- Side-by-side comparison with Claude Code\n\n### Fixed\n\n- Location tag now correctly shows `project` or `global` based on install location\n\n## [1.0.0] - 2025-10-26\n\n### Added\n\n- Initial release\n- `npx openskills install <source>` - Install skills from GitHub repos\n- `npx openskills sync` - Generate `<available_skills>` XML for AGENTS.md\n- `npx openskills list` - Show installed skills\n- `npx openskills read <name>` - Load skill content for agents\n- `npx openskills manage` - Interactive skill removal\n- `npx openskills remove <name>` - Remove specific skill\n- Interactive TUI for all commands\n- Support for Anthropic's SKILL.md format\n- Progressive disclosure (load skills on demand)\n- Bundled resources support (references/, scripts/, assets/)\n\n[1.3.0]: https://github.com/numman-ali/openskills/compare/v1.2.1...v1.3.0\n[1.3.1]: https://github.com/numman-ali/openskills/compare/v1.3.0...v1.3.1\n[1.3.2]: https://github.com/numman-ali/openskills/compare/v1.3.1...v1.3.2\n[1.4.0]: https://github.com/numman-ali/openskills/compare/v1.3.2...v1.4.0\n[1.5.0]: https://github.com/numman-ali/openskills/compare/v1.4.0...v1.5.0\n[1.2.1]: https://github.com/numman-ali/openskills/compare/v1.2.0...v1.2.1\n[1.2.0]: https://github.com/numman-ali/openskills/compare/v1.1.0...v1.2.0\n[1.1.0]: https://github.com/numman-ali/openskills/compare/v1.0.0...v1.1.0\n[1.0.0]: https://github.com/numman-ali/openskills/releases/tag/v1.0.0\n"
  },
  {
    "path": "CLAUDE.md",
    "content": "@AGENTS.md"
  },
  {
    "path": "CONTRIBUTING.md",
    "content": "# Contributing to OpenSkills\n\nThank you for your interest in contributing to OpenSkills!\n\n## Code Standards\n\n- **TypeScript:** All code must be TypeScript with strict type checking\n- **Testing:** Include tests for new functionality (we use vitest)\n- **Documentation:** Update README.md for user-facing changes\n- **Modular design:** Keep functions focused and under 50 lines\n- **Minimal dependencies:** Avoid adding dependencies unless necessary\n\n## Pull Request Process\n\n1. **Fork the repository** and create a feature branch\n2. **Write clear commit messages** explaining the \"why\" not just \"what\"\n3. **Include tests** for new functionality\n4. **Update documentation** (README.md, docs/)\n5. **Ensure all checks pass** (typecheck, test, build)\n6. **Submit PR** with clear description of changes\n\n## Development Setup\n\n```bash\n# Clone your fork\ngit clone https://github.com/your-username/openskills\ncd openskills\n\n# Install dependencies\nnpm install\n\n# Run tests\nnpm test\n\n# Build\nnpm run build\n\n# Link locally for testing\nnpm link\nnpx openskills list\n```\n\n## Testing Changes\n\n```bash\n# Type check\nnpm run typecheck\n\n# Run tests\nnpm test\n\n# Watch mode\nnpm run test:watch\n\n# Coverage\nnpm run test:coverage\n\n# Test CLI locally\nnpm link\nnpx openskills install anthropics/skills/pdf-editor --project\nnpx openskills sync\nnpx openskills read pdf-editor\n```\n\n## Project Structure\n\n```\nsrc/\n├── cli.ts              # Main entry point\n├── commands/           # Command implementations\n├── utils/              # Shared utilities\n└── types.ts            # TypeScript interfaces\n\ntests/\n└── utils/              # Unit tests\n```\n\n## Reporting Issues\n\nWhen reporting issues, please:\n\n- **Check existing issues** to avoid duplicates\n- **Provide clear reproduction steps**\n- **Include version information** (`npx openskills --version`, `node --version`)\n- **Use issue templates** (bug report or feature request)\n\n## Feature Requests\n\nWe welcome feature requests that:\n- Improve usability for AI coding agents\n- Enhance AGENTS.md integration\n- Maintain compatibility with Anthropic's SKILL.md spec\n- Work universally across agents (Claude Code, Cursor, Windsurf, Aider)\n\n## Questions?\n\nFor questions about:\n- **Usage:** Open a GitHub issue\n- **Contributing:** Open a discussion thread\n- **Anthropic Skills spec:** See [Anthropic's documentation](https://www.anthropic.com/engineering/equipping-agents-for-the-real-world-with-agent-skills)\n\n## License\n\nBy contributing, you agree that your contributions will be licensed under the Apache 2.0 License.\n\n---\n\nThank you for helping make OpenSkills better!\n"
  },
  {
    "path": "LICENSE",
    "content": "Apache License\nVersion 2.0, January 2004\nhttp://www.apache.org/licenses/\n\nCopyright 2025 OpenSkills Contributors\n\nLicensed under the Apache License, Version 2.0 (the \"License\");\nyou may not use this file except in compliance with the License.\nYou may obtain a copy of the License at\n\n    http://www.apache.org/licenses/LICENSE-2.0\n\nUnless required by applicable law or agreed to in writing, software\ndistributed under the License is distributed on an \"AS IS\" BASIS,\nWITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\nSee the License for the specific language governing permissions and\nlimitations under the License.\n"
  },
  {
    "path": "README.md",
    "content": "<div align=\"center\">\n\n<img src=\"./assets/logo.svg\" alt=\"OpenSkills\" width=\"420\" />\n\n<br/>\n<br/>\n\n**Universal skills loader for AI coding agents**\n\nOne CLI. Every agent. Same format as Claude Code.\n\n[![npm version](https://img.shields.io/npm/v/openskills.svg)](https://www.npmjs.com/package/openskills)\n[![npm downloads](https://img.shields.io/npm/dm/openskills.svg)](https://www.npmjs.com/package/openskills)\n[![License](https://img.shields.io/badge/License-Apache%202.0-blue.svg)](https://opensource.org/licenses/Apache-2.0)\n\n[Quick Start](#-quick-start) · [How It Works](#-how-it-works) · [Commands](#-commands) · [Create Skills](#-creating-your-own-skills) · [FAQ](#-faq)\n\n</div>\n\n---\n\n## ✨ What Is OpenSkills?\n\nOpenSkills brings **Anthropic's skills system** to every AI coding agent — Claude Code, Cursor, Windsurf, Aider, Codex, and anything that can read `AGENTS.md`.\n\n**Think of it as the universal installer for SKILL.md.**\n\n---\n\n## 🚀 Quick Start\n\n```bash\nnpx openskills install anthropics/skills\nnpx openskills sync\n```\n\nBy default, installs are project-local (`./.claude/skills`, or `./.agent/skills` with `--universal`). Use `--global` for `~/.claude/skills`.\n\n---\n\n## ✅ Why OpenSkills\n\n- **Exact Claude Code compatibility** — same prompt format, same marketplace, same folder structure\n- **Universal** — works with Claude Code, Cursor, Windsurf, Aider, Codex, and more\n- **Progressive disclosure** — load skills only when needed (keeps context clean)\n- **Repo-friendly** — skills live in your project and can be versioned\n- **Private friendly** — install from local paths or private git repos\n\n---\n\n## 🧠 How It Works\n\n### Claude Code System Prompt (Skills)\n\nClaude Code ships skills as **SKILL.md files** and exposes them inside a `<available_skills>` block. When the user asks for a task, Claude dynamically loads the matching skill.\n\n```xml\n<available_skills>\n<skill>\n<name>pdf</name>\n<description>Comprehensive PDF manipulation toolkit for extracting text and tables...</description>\n<location>plugin</location>\n</skill>\n</available_skills>\n```\n\n### OpenSkills: Same Format, Universal Loader\n\nOpenSkills generates the **exact same `<available_skills>` XML** in your `AGENTS.md` and loads skills via:\n\n```bash\nnpx openskills read <skill-name>\n```\n\nSo any agent that can read `AGENTS.md` can use Claude Code skills without needing Claude Code itself.\n\n### Side-by-Side\n\n| Aspect | Claude Code | OpenSkills |\n|--------|-------------|------------|\n| **Prompt Format** | `<available_skills>` XML | Same XML | \n| **Skill Storage** | `.claude/skills/` | `.claude/skills/` (default) | \n| **Invocation** | `Skill(\"name\")` tool | `npx openskills read <name>` | \n| **Marketplace** | Anthropic marketplace | GitHub (anthropics/skills) | \n| **Progressive Disclosure** | ✅ | ✅ |\n\n<details>\n<summary><strong>Show the exact AGENTS.md format OpenSkills writes</strong></summary>\n\n```xml\n<skills_system priority=\"1\">\n\n## Available Skills\n\n<!-- SKILLS_TABLE_START -->\n<usage>\nWhen users ask you to perform tasks, check if any of the available skills below can help complete the task more effectively.\n\nHow to use skills:\n- Invoke: `npx openskills read <skill-name>` (run in your shell)\n- The skill content will load with detailed instructions\n- Base directory provided in output for resolving bundled resources\n\nUsage notes:\n- Only use skills listed in <available_skills> below\n- Do not invoke a skill that is already loaded in your context\n</usage>\n\n<available_skills>\n\n<skill>\n<name>pdf</name>\n<description>Comprehensive PDF manipulation toolkit for extracting text and tables, creating new PDFs, merging/splitting documents, and handling forms...</description>\n<location>project</location>\n</skill>\n\n</available_skills>\n<!-- SKILLS_TABLE_END -->\n\n</skills_system>\n```\n\n</details>\n\n---\n\n## 🔧 Install Skills\n\n### From Anthropic Marketplace\n\n```bash\nnpx openskills install anthropics/skills\n```\n\n### From Any GitHub Repo\n\n```bash\nnpx openskills install your-org/your-skills\n```\n\n### From a Local Path\n\n```bash\nnpx openskills install ./local-skills/my-skill\n```\n\n### From Private Git Repos\n\n```bash\nnpx openskills install git@github.com:your-org/private-skills.git\n```\n\n---\n\n## 🌍 Universal Mode (Multi-Agent Setups)\n\nIf you use Claude Code **and** other agents with one `AGENTS.md`, install to `.agent/skills/` to avoid conflicts with Claude's plugin marketplace:\n\n```bash\nnpx openskills install anthropics/skills --universal\n```\n\n**Priority order (highest wins):**\n1. `./.agent/skills/`\n2. `~/.agent/skills/`\n3. `./.claude/skills/`\n4. `~/.claude/skills/`\n\n---\n\n## 🧰 Commands\n\n```bash\nnpx openskills install <source> [options]  # Install from GitHub, local path, or private repo\nnpx openskills sync [-y] [-o <path>]       # Update AGENTS.md (or custom output)\nnpx openskills list                        # Show installed skills\nnpx openskills read <name>                 # Load skill (for agents)\nnpx openskills update [name...]            # Update installed skills (default: all)\nnpx openskills manage                      # Remove skills (interactive)\nnpx openskills remove <name>               # Remove specific skill\n```\n\n### Flags\n\n- `--global` — Install globally to `~/.claude/skills` (default: project install)\n- `--universal` — Install to `.agent/skills/` instead of `.claude/skills/`\n- `-y, --yes` — Skip prompts (useful for CI)\n- `-o, --output <path>` — Output file for sync (default: `AGENTS.md`)\n\n---\n\n## 🧬 The SKILL.md Format\n\nOpenSkills uses Anthropic's exact format:\n\n```markdown\n---\nname: pdf\ndescription: Comprehensive PDF manipulation toolkit for extracting text and tables, creating new PDFs, merging/splitting documents, and handling forms.\n---\n\n# PDF Skill Instructions\n\nWhen the user asks you to work with PDFs, follow these steps:\n1. Install dependencies: `pip install pypdf2`\n2. Extract text using scripts/extract_text.py\n3. Use references/api-docs.md for details\n```\n\nSkills are **loaded on demand**, keeping your agent's context clean and focused.\n\n---\n\n## 🧪 Creating Your Own Skills\n\n### Minimal Structure\n\n```\nmy-skill/\n└── SKILL.md\n```\n\n### With Resources\n\n```\nmy-skill/\n├── SKILL.md\n├── references/\n├── scripts/\n└── assets/\n```\n\nInstall your own skill:\n\n```bash\nnpx openskills install ./my-skill\n```\n\n### Local Development with Symlinks\n\n```bash\ngit clone git@github.com:your-org/my-skills.git ~/dev/my-skills\nmkdir -p .claude/skills\nln -s ~/dev/my-skills/my-skill .claude/skills/my-skill\n```\n\n### Authoring Guide\n\n```bash\nnpx openskills install anthropics/skills\nnpx openskills read skill-creator\n```\n\n---\n\n## 🔄 Updating Skills\n\nIf you installed skills from a git repo, you can refresh them anytime:\n\n```bash\nnpx openskills update\n```\n\nTo update specific skills, pass a comma-separated list:\n\n```bash\nnpx openskills update git-workflow,check-branch-first\n```\n\nIf a skill was installed before updates were tracked, re-install it once to record its source.\n\n---\n\n## ✅ Tips\n\n- You can always run OpenSkills via `npx`; a global install is optional.\n- For multiple reads, prefer comma-separated names: `npx openskills read foo,bar`.\n\n---\n\n## ❓ FAQ\n\n### Why CLI instead of MCP?\n\n**MCP is for dynamic tools.** Skills are static instructions + resources.\n\n- Skills are just files → no server required\n- Works with every agent → no MCP support needed\n- Matches Anthropic's design → SKILL.md is the spec\n\nMCP and skills solve different problems. OpenSkills keeps skills lightweight and universal.\n\n---\n\n## 📋 Requirements\n\n- **Node.js** 20.6+\n- **Git** (for cloning repositories)\n\n---\n\n## 📜 License\n\nApache 2.0\n\n## Attribution\n\nImplements [Anthropic's Agent Skills](https://www.anthropic.com/engineering/equipping-agents-for-the-real-world-with-agent-skills) specification.\n\n**Not affiliated with Anthropic.** Claude, Claude Code, and Agent Skills are trademarks of Anthropic, PBC.\n"
  },
  {
    "path": "SECURITY.md",
    "content": "# Security Policy\n\n## Supported Versions\n\nWe provide security updates for the latest version.\n\n| Version | Supported          |\n| ------- | ------------------ |\n| Latest  | ✅ Active support |\n| < 1.0   | ❌ No longer supported |\n\n## Security Considerations\n\n### Git Credentials\n\nOpenSkills clones repositories from GitHub. To protect your security:\n\n✅ **What we do:**\n- Use `git clone` with HTTPS (no credentials required for public repos)\n- Clean up temporary directories after installation\n- Only install from public repositories by default\n\n⚠️ **What you should do:**\n- Only install skills from trusted sources\n- Review SKILL.md content before loading in AI agents\n- Be cautious with skills that include executable scripts\n- Verify repository ownership before installing\n\n### Reporting a Vulnerability\n\nIf you discover a security vulnerability:\n\n1. **DO NOT open a public issue**\n2. Email: security@[check GitHub profile for contact]\n3. Include:\n   - Description of the vulnerability\n   - Steps to reproduce\n   - Potential impact\n   - Suggested fix (if any)\n\nWe aim to respond to security reports within 48 hours.\n\n### Responsible Disclosure\n\nWe follow responsible disclosure practices:\n- Security issues are patched before public disclosure\n- Reporter receives credit (unless anonymity is requested)\n- Timeline for disclosure is coordinated with reporter\n\n### Security Best Practices\n\nWhen using OpenSkills:\n\n- **Verify sources:** Only install skills from trusted repositories\n- **Review content:** Check SKILL.md before loading in agents\n- **Inspect scripts:** Review any executable scripts in skills/scripts/\n- **Keep updated:** Use the latest version for security patches\n- **Report issues:** If you find malicious skills, report them\n\n### Out of Scope\n\nThe following are **not** security vulnerabilities:\n- Skills with poor quality or incorrect instructions\n- Git clone failures due to network issues\n- Skills that don't work as described\n- Repository not found errors\n\n### Dependencies\n\nOpenSkills minimizes dependencies for security:\n- **Only dependency:** `commander` (CLI framework)\n- Regular dependency updates for security patches\n- No network requests except `git clone`\n- No telemetry or analytics\n\n## Questions?\n\nFor security questions that are not vulnerabilities, open a discussion thread on GitHub.\n\n---\n\n**Note:** OpenSkills is not affiliated with Anthropic. For Anthropic security concerns, contact Anthropic directly.\n"
  },
  {
    "path": "examples/my-first-skill/SKILL.md",
    "content": "---\nname: my-first-skill\ndescription: Example skill demonstrating Anthropic SKILL.md format. Load when learning to create skills or testing the OpenSkills loader.\n---\n\n# My First Skill\n\nThis is an example skill demonstrating the Anthropic SKILL.md format.\n\n## Purpose\n\nThis skill shows how to structure procedural guidance for AI coding agents using progressive disclosure.\n\n## When to Use\n\nLoad this skill when:\n- Learning how skills work\n- Testing the OpenSkills loader\n- Understanding the SKILL.md format\n\n## Instructions\n\nTo create a skill:\n\n1. Create a directory: `mkdir my-skill/`\n2. Add SKILL.md with YAML frontmatter:\n   ```yaml\n   ---\n   name: my-skill\n   description: When to use this skill\n   ---\n   ```\n3. Write instructions in imperative form (not second person)\n4. Reference bundled resources as needed\n\n## Bundled Resources\n\nFor detailed information about the SKILL.md specification:\n\nSee `references/skill-format.md`\n\n## Best Practices\n\n- Write in imperative/infinitive form: \"To do X, execute Y\"\n- NOT second person: avoid \"You should...\"\n- Keep SKILL.md under 5,000 words\n- Move detailed content to references/\n- Use scripts/ for executable code\n- Use assets/ for templates and output files\n\n## Resource Resolution\n\nWhen this skill is loaded, the base directory is provided:\n\n```\nBase directory: /path/to/my-first-skill\n```\n\nRelative paths resolve from base directory:\n- `references/skill-format.md` → `/path/to/my-first-skill/references/skill-format.md`\n- `scripts/helper.sh` → `/path/to/my-first-skill/scripts/helper.sh`\n"
  },
  {
    "path": "examples/my-first-skill/references/skill-format.md",
    "content": "# SKILL.md Format Reference\n\n## YAML Frontmatter (Required)\n\nEvery SKILL.md must start with YAML frontmatter:\n\n```yaml\n---\nname: skill-name           # Required: hyphen-case identifier\ndescription: When to use   # Required: 1-2 sentences, third-person\n---\n```\n\n## Markdown Body (Required)\n\nAfter frontmatter, write instructions in **imperative/infinitive form**:\n\n**Good:**\n- \"To accomplish X, execute Y\"\n- \"Load this skill when Z\"\n- \"See references/guide.md for details\"\n\n**Avoid:**\n- \"You should do X\"\n- \"If you need Y\"\n- \"When you want Z\"\n\n## Progressive Disclosure\n\nSkills load in three levels:\n\n1. **Metadata** (always in context): name + description\n2. **SKILL.md** (loaded when relevant): core instructions\n3. **Resources** (loaded as needed): detailed documentation\n\n## Bundled Resources (Optional)\n\n### references/\n\nDocumentation loaded into context as needed:\n- API documentation\n- Database schemas\n- Detailed guides\n\n### scripts/\n\nExecutable code (Python/Bash/etc.):\n- Can be run without loading to context\n- Use for deterministic, repeatable tasks\n\n### assets/\n\nFiles used in output (not loaded to context):\n- Templates\n- Images\n- Boilerplate code\n\n## File Size Guidelines\n\n- **SKILL.md**: Under 5,000 words\n- **references/**: Unlimited (loaded selectively)\n- **scripts/**: Executable, not counted\n- **assets/**: Not loaded to context\n\n## Example Structure\n\n```\npdf-editor/\n├── SKILL.md              (~2,000 words)\n├── references/\n│   └── pdf-api.md        (detailed API docs)\n├── scripts/\n│   ├── rotate.py         (executable)\n│   └── merge.py\n└── assets/\n    └── template.pdf      (boilerplate)\n```\n"
  },
  {
    "path": "package.json",
    "content": "{\n  \"name\": \"openskills\",\n  \"version\": \"1.5.0\",\n  \"description\": \"Universal skills loader for AI coding agents - install and load Anthropic SKILL.md format skills in any agent\",\n  \"type\": \"module\",\n  \"main\": \"./dist/cli.js\",\n  \"bin\": {\n    \"openskills\": \"dist/cli.js\"\n  },\n  \"files\": [\n    \"dist\",\n    \"examples\"\n  ],\n  \"scripts\": {\n    \"build\": \"tsup\",\n    \"dev\": \"tsup --watch\",\n    \"test\": \"vitest run\",\n    \"test:watch\": \"vitest\",\n    \"test:coverage\": \"vitest run --coverage\",\n    \"typecheck\": \"tsc --noEmit\",\n    \"prepublishOnly\": \"npm run typecheck && npm run build && npm test\"\n  },\n  \"keywords\": [\n    \"anthropic\",\n    \"claude\",\n    \"claude-code\",\n    \"skills\",\n    \"ai\",\n    \"agents\",\n    \"coding-agent\",\n    \"cursor\",\n    \"windsurf\",\n    \"aider\",\n    \"progressive-disclosure\",\n    \"automation\",\n    \"developer-tools\"\n  ],\n  \"author\": \"OpenSkills Contributors\",\n  \"license\": \"Apache-2.0\",\n  \"repository\": {\n    \"type\": \"git\",\n    \"url\": \"git+https://github.com/numman-ali/openskills.git\"\n  },\n  \"homepage\": \"https://github.com/numman-ali/openskills#readme\",\n  \"engines\": {\n    \"node\": \">=20.6.0\"\n  },\n  \"dependencies\": {\n    \"@inquirer/prompts\": \"^7.9.0\",\n    \"chalk\": \"^5.6.2\",\n    \"commander\": \"^12.1.0\",\n    \"ora\": \"^9.0.0\"\n  },\n  \"devDependencies\": {\n    \"@types/node\": \"^24.9.1\",\n    \"tsup\": \"^8.5.0\",\n    \"typescript\": \"^5.9.3\",\n    \"vitest\": \"^4.0.3\"\n  }\n}\n"
  },
  {
    "path": "src/cli.ts",
    "content": "#!/usr/bin/env node\n\nimport { Command } from 'commander';\nimport { listSkills } from './commands/list.js';\nimport { installSkill } from './commands/install.js';\nimport { readSkill } from './commands/read.js';\nimport { removeSkill } from './commands/remove.js';\nimport { manageSkills } from './commands/manage.js';\nimport { syncAgentsMd } from './commands/sync.js';\nimport { updateSkills } from './commands/update.js';\nimport { createRequire } from 'module';\n\nconst program = new Command();\nconst require = createRequire(import.meta.url);\nconst { version } = require('../package.json') as { version: string };\n\nprogram\n  .name('openskills')\n  .description('Universal skills loader for AI coding agents')\n  .version(version)\n  .showHelpAfterError(false)\n  .exitOverride((err) => {\n    // Handle all commander errors gracefully (no stack traces)\n    if (err.code === 'commander.helpDisplayed' || err.code === 'commander.help' || err.code === 'commander.version') {\n      process.exit(0);\n    }\n    if (err.code === 'commander.missingArgument' || err.code === 'commander.missingMandatoryOptionValue') {\n      // Error already displayed by commander\n      process.exit(1);\n    }\n    if (err.code === 'commander.unknownOption' || err.code === 'commander.invalidArgument') {\n      // Error already displayed by commander\n      process.exit(1);\n    }\n    // Other errors\n    process.exit(err.exitCode || 1);\n  });\n\nprogram\n  .command('list')\n  .description('List all installed skills')\n  .action(listSkills);\n\nprogram\n  .command('install <source>')\n  .description('Install skill from GitHub or Git URL')\n  .option('-g, --global', 'Install globally (default: project install)')\n  .option('-u, --universal', 'Install to .agent/skills/ (for universal AGENTS.md usage)')\n  .option('-y, --yes', 'Skip interactive selection, install all skills found')\n  .action(installSkill);\n\nprogram\n  .command('read <skill-names...>')\n  .description('Read skill(s) to stdout (for AI agents)')\n  .action(readSkill);\n\nprogram\n  .command('update [skill-names...]')\n  .description('Update installed skills from their source (default: all)')\n  .action(updateSkills);\n\nprogram\n  .command('sync')\n  .description('Update AGENTS.md with installed skills (interactive, pre-selects current state)')\n  .option('-y, --yes', 'Skip interactive selection, sync all skills')\n  .option('-o, --output <path>', 'Output file path (default: AGENTS.md)')\n  .action(syncAgentsMd);\n\nprogram\n  .command('manage')\n  .description('Interactively manage (remove) installed skills')\n  .action(manageSkills);\n\nprogram\n  .command('remove <skill-name>')\n  .alias('rm')\n  .description('Remove specific skill (for scripts, use manage for interactive)')\n  .action(removeSkill);\n\nprogram.parse();\n"
  },
  {
    "path": "src/commands/install.ts",
    "content": "import { readFileSync, readdirSync, existsSync, mkdirSync, rmSync, cpSync, statSync } from 'fs';\nimport { join, basename, resolve, sep, relative } from 'path';\nimport { homedir } from 'os';\nimport { execSync } from 'child_process';\nimport chalk from 'chalk';\nimport ora from 'ora';\nimport { checkbox, confirm } from '@inquirer/prompts';\nimport { ExitPromptError } from '@inquirer/core';\nimport { hasValidFrontmatter, extractYamlField } from '../utils/yaml.js';\nimport { ANTHROPIC_MARKETPLACE_SKILLS } from '../utils/marketplace-skills.js';\nimport { writeSkillMetadata } from '../utils/skill-metadata.js';\nimport type { InstallOptions } from '../types.js';\nimport type { SkillSourceMetadata, SkillSourceType } from '../utils/skill-metadata.js';\n\ninterface InstallSourceInfo {\n  source: string;\n  sourceType: SkillSourceType;\n  repoUrl?: string;\n  localRoot?: string;\n}\n\n/**\n * Check if source is a local path\n */\nfunction isLocalPath(source: string): boolean {\n  return (\n    source.startsWith('/') ||\n    source.startsWith('./') ||\n    source.startsWith('../') ||\n    source.startsWith('~/')\n  );\n}\n\n/**\n * Check if source is a git URL (SSH, git://, or HTTPS)\n */\nfunction isGitUrl(source: string): boolean {\n  return (\n    source.startsWith('git@') ||\n    source.startsWith('git://') ||\n    source.startsWith('http://') ||\n    source.startsWith('https://') ||\n    source.endsWith('.git')\n  );\n}\n\n/**\n * Extract repo name from a git URL\n */\nfunction getRepoName(repoUrl: string): string | null {\n  const cleaned = repoUrl.replace(/\\.git$/, '');\n  const lastPart = cleaned.split('/').pop();\n  if (!lastPart) return null;\n  const maybeRepo = lastPart.includes(':') ? lastPart.split(':').pop() : lastPart;\n  return maybeRepo || null;\n}\n\n/**\n * Expand ~ to home directory\n */\nfunction expandPath(source: string): string {\n  if (source.startsWith('~/')) {\n    return join(homedir(), source.slice(2));\n  }\n  return resolve(source);\n}\n\n/**\n * Ensure target path stays within target directory\n */\nfunction isPathInside(targetPath: string, targetDir: string): boolean {\n  const resolvedTargetPath = resolve(targetPath);\n  const resolvedTargetDir = resolve(targetDir);\n  const resolvedTargetDirWithSep = resolvedTargetDir.endsWith(sep)\n    ? resolvedTargetDir\n    : resolvedTargetDir + sep;\n  return resolvedTargetPath.startsWith(resolvedTargetDirWithSep);\n}\n\n/**\n * Install skill from local path, GitHub, or Git URL\n */\nexport async function installSkill(source: string, options: InstallOptions): Promise<void> {\n  const folder = options.universal ? '.agent/skills' : '.claude/skills';\n  const isProject = !options.global; // Default to project unless --global specified\n  const targetDir = isProject\n    ? join(process.cwd(), folder)\n    : join(homedir(), folder);\n\n  const location = isProject\n    ? chalk.blue(`project (${folder})`)\n    : chalk.dim(`global (~/${folder})`);\n\n  const projectLocation = `./${folder}`;\n  const globalLocation = `~/${folder}`;\n\n  console.log(`Installing from: ${chalk.cyan(source)}`);\n  console.log(`Location: ${location}`);\n  if (isProject) {\n    console.log(\n      chalk.dim(`Default install is project-local (${projectLocation}). Use --global for ${globalLocation}.`)\n    );\n  } else {\n    console.log(\n      chalk.dim(`Global install selected (${globalLocation}). Omit --global for ${projectLocation}.`)\n    );\n  }\n  console.log('');\n\n  // Handle local path installation\n  if (isLocalPath(source)) {\n    const localPath = expandPath(source);\n    const sourceInfo: InstallSourceInfo = {\n      source,\n      sourceType: 'local',\n      localRoot: localPath,\n    };\n    await installFromLocal(localPath, targetDir, options, sourceInfo);\n    printPostInstallHints(isProject);\n    return;\n  }\n\n  // Parse git source\n  let repoUrl: string;\n  let skillSubpath: string = '';\n\n  if (isGitUrl(source)) {\n    // Full git URL (SSH, HTTPS, git://)\n    repoUrl = source;\n  } else {\n    // GitHub shorthand: owner/repo or owner/repo/skill-path\n    const parts = source.split('/');\n    if (parts.length === 2) {\n      repoUrl = `https://github.com/${source}`;\n    } else if (parts.length > 2) {\n      repoUrl = `https://github.com/${parts[0]}/${parts[1]}`;\n      skillSubpath = parts.slice(2).join('/');\n    } else {\n      console.error(chalk.red('Error: Invalid source format'));\n      console.error('Expected: owner/repo, owner/repo/skill-name, git URL, or local path');\n      process.exit(1);\n    }\n  }\n\n  // Clone and install from git\n  const tempDir = join(homedir(), `.openskills-temp-${Date.now()}`);\n  mkdirSync(tempDir, { recursive: true });\n  const sourceInfo: InstallSourceInfo = {\n    source,\n    sourceType: 'git',\n    repoUrl,\n  };\n\n  try {\n    const spinner = ora('Cloning repository...').start();\n    try {\n      execSync(`git clone --depth 1 --quiet \"${repoUrl}\" \"${tempDir}/repo\"`, {\n        stdio: 'pipe',\n      });\n      spinner.succeed('Repository cloned');\n    } catch (error) {\n      spinner.fail('Failed to clone repository');\n      const err = error as { stderr?: Buffer };\n      if (err.stderr) {\n        console.error(chalk.dim(err.stderr.toString().trim()));\n      }\n      console.error(chalk.yellow('\\nTip: For private repos, ensure git SSH keys or credentials are configured'));\n      process.exit(1);\n    }\n\n    const repoDir = join(tempDir, 'repo');\n\n    if (skillSubpath) {\n      await installSpecificSkill(repoDir, skillSubpath, targetDir, isProject, options, sourceInfo);\n    } else {\n      const repoName = getRepoName(repoUrl);\n      await installFromRepo(repoDir, targetDir, options, repoName || undefined, sourceInfo);\n    }\n  } finally {\n    rmSync(tempDir, { recursive: true, force: true });\n  }\n\n  printPostInstallHints(isProject);\n}\n\n/**\n * Print post-install hints\n */\nfunction printPostInstallHints(isProject: boolean): void {\n  console.log(`\\n${chalk.dim('Read skill:')} ${chalk.cyan('npx openskills read <skill-name>')}`);\n  if (isProject) {\n    console.log(`${chalk.dim('Sync to AGENTS.md:')} ${chalk.cyan('npx openskills sync')}`);\n  }\n}\n\n/**\n * Install from local path (directory containing skills or single skill)\n */\nasync function installFromLocal(\n  localPath: string,\n  targetDir: string,\n  options: InstallOptions,\n  sourceInfo: InstallSourceInfo\n): Promise<void> {\n  if (!existsSync(localPath)) {\n    console.error(chalk.red(`Error: Path does not exist: ${localPath}`));\n    process.exit(1);\n  }\n\n  const stats = statSync(localPath);\n  if (!stats.isDirectory()) {\n    console.error(chalk.red('Error: Path must be a directory'));\n    process.exit(1);\n  }\n\n  // Check if this is a single skill (has SKILL.md) or a directory of skills\n  const skillMdPath = join(localPath, 'SKILL.md');\n  if (existsSync(skillMdPath)) {\n    // Single skill directory\n    const isProject = targetDir.includes(process.cwd());\n    await installSingleLocalSkill(localPath, targetDir, isProject, options, sourceInfo);\n  } else {\n    // Directory containing multiple skills\n    await installFromRepo(localPath, targetDir, options, undefined, sourceInfo);\n  }\n}\n\n/**\n * Install a single local skill directory\n */\nasync function installSingleLocalSkill(\n  skillDir: string,\n  targetDir: string,\n  isProject: boolean,\n  options: InstallOptions,\n  sourceInfo: InstallSourceInfo\n): Promise<void> {\n  const skillMdPath = join(skillDir, 'SKILL.md');\n  const content = readFileSync(skillMdPath, 'utf-8');\n\n  if (!hasValidFrontmatter(content)) {\n    console.error(chalk.red('Error: Invalid SKILL.md (missing YAML frontmatter)'));\n    process.exit(1);\n  }\n\n  const skillName = basename(skillDir);\n  const targetPath = join(targetDir, skillName);\n\n  const shouldInstall = await warnIfConflict(skillName, targetPath, isProject, options.yes);\n  if (!shouldInstall) {\n    console.log(chalk.yellow(`Skipped: ${skillName}`));\n    return;\n  }\n\n  mkdirSync(targetDir, { recursive: true });\n  // Security: ensure target path stays within target directory\n  if (!isPathInside(targetPath, targetDir)) {\n    console.error(chalk.red(`Security error: Installation path outside target directory`));\n    process.exit(1);\n  }\n\n  cpSync(skillDir, targetPath, { recursive: true, dereference: true });\n  writeSkillMetadata(targetPath, buildLocalMetadata(sourceInfo, skillDir));\n\n  console.log(chalk.green(`✅ Installed: ${skillName}`));\n  console.log(`   Location: ${targetPath}`);\n}\n\n/**\n * Install specific skill from subpath (no interaction needed)\n */\nasync function installSpecificSkill(\n  repoDir: string,\n  skillSubpath: string,\n  targetDir: string,\n  isProject: boolean,\n  options: InstallOptions,\n  sourceInfo: InstallSourceInfo\n): Promise<void> {\n  const skillDir = join(repoDir, skillSubpath);\n  const skillMdPath = join(skillDir, 'SKILL.md');\n\n  if (!existsSync(skillMdPath)) {\n    console.error(chalk.red(`Error: SKILL.md not found at ${skillSubpath}`));\n    process.exit(1);\n  }\n\n  // Validate\n  const content = readFileSync(skillMdPath, 'utf-8');\n  if (!hasValidFrontmatter(content)) {\n    console.error(chalk.red('Error: Invalid SKILL.md (missing YAML frontmatter)'));\n    process.exit(1);\n  }\n\n  const skillName = basename(skillSubpath);\n  const targetPath = join(targetDir, skillName);\n\n  // Warn about potential conflicts\n  const shouldInstall = await warnIfConflict(skillName, targetPath, isProject, options.yes);\n  if (!shouldInstall) {\n    console.log(chalk.yellow(`Skipped: ${skillName}`));\n    return;\n  }\n\n  mkdirSync(targetDir, { recursive: true });\n  // Security: ensure target path stays within target directory\n  if (!isPathInside(targetPath, targetDir)) {\n    console.error(chalk.red(`Security error: Installation path outside target directory`));\n    process.exit(1);\n  }\n  cpSync(skillDir, targetPath, { recursive: true, dereference: true });\n  writeSkillMetadata(targetPath, buildGitMetadata(sourceInfo, skillSubpath));\n\n  console.log(chalk.green(`✅ Installed: ${skillName}`));\n  console.log(`   Location: ${targetPath}`);\n}\n\n/**\n * Install from repository (with interactive selection unless -y flag)\n */\nasync function installFromRepo(\n  repoDir: string,\n  targetDir: string,\n  options: InstallOptions,\n  repoName: string | undefined,\n  sourceInfo: InstallSourceInfo\n): Promise<void> {\n  const rootSkillPath = join(repoDir, 'SKILL.md');\n  let skillInfos: Array<{\n    skillDir: string;\n    skillName: string;\n    description: string;\n    targetPath: string;\n    size: number;\n  }> = [];\n\n  if (existsSync(rootSkillPath)) {\n    const content = readFileSync(rootSkillPath, 'utf-8');\n    if (!hasValidFrontmatter(content)) {\n      console.error(chalk.red('Error: Invalid SKILL.md (missing YAML frontmatter)'));\n      process.exit(1);\n    }\n\n    const frontmatterName = extractYamlField(content, 'name');\n    const skillName = frontmatterName || repoName || basename(repoDir);\n    skillInfos = [\n      {\n        skillDir: repoDir,\n        skillName,\n        description: extractYamlField(content, 'description'),\n        targetPath: join(targetDir, skillName),\n        size: getDirectorySize(repoDir),\n      },\n    ];\n  }\n\n  // Find all skills\n  const findSkills = (dir: string): string[] => {\n    const skills: string[] = [];\n    const entries = readdirSync(dir, { withFileTypes: true });\n\n    for (const entry of entries) {\n      const fullPath = join(dir, entry.name);\n      if (entry.isDirectory()) {\n        if (existsSync(join(fullPath, 'SKILL.md'))) {\n          skills.push(fullPath);\n        } else {\n          skills.push(...findSkills(fullPath));\n        }\n      }\n    }\n    return skills;\n  };\n\n  if (skillInfos.length === 0) {\n    const skillDirs = findSkills(repoDir);\n\n    if (skillDirs.length === 0) {\n      console.error(chalk.red('Error: No SKILL.md files found in repository'));\n      process.exit(1);\n    }\n\n    // Build skill info list\n    skillInfos = skillDirs\n      .map((skillDir) => {\n        const skillMdPath = join(skillDir, 'SKILL.md');\n        const content = readFileSync(skillMdPath, 'utf-8');\n\n        if (!hasValidFrontmatter(content)) {\n          return null;\n        }\n\n        const skillName = basename(skillDir);\n        const description = extractYamlField(content, 'description');\n        const targetPath = join(targetDir, skillName);\n\n        // Get size\n        const size = getDirectorySize(skillDir);\n\n        return {\n          skillDir,\n          skillName,\n          description,\n          targetPath,\n          size,\n        };\n      })\n      .filter((info) => info !== null) as Array<{\n      skillDir: string;\n      skillName: string;\n      description: string;\n      targetPath: string;\n      size: number;\n    }>;\n\n    if (skillInfos.length === 0) {\n      console.error(chalk.red('Error: No valid SKILL.md files found'));\n      process.exit(1);\n    }\n  }\n\n  console.log(chalk.dim(`Found ${skillInfos.length} skill(s)\\n`));\n\n  // Interactive selection (unless -y flag or single skill)\n  let skillsToInstall = skillInfos;\n\n  if (!options.yes && skillInfos.length > 1) {\n    try {\n      const choices = skillInfos.map((info) => ({\n        name: `${chalk.bold(info.skillName.padEnd(25))} ${chalk.dim(formatSize(info.size))}`,\n        value: info.skillName,\n        description: info.description.slice(0, 80),\n        checked: true, // Check all by default\n      }));\n\n      const selected = await checkbox({\n        message: 'Select skills to install',\n        choices,\n        pageSize: 15,\n      });\n\n      if (selected.length === 0) {\n        console.log(chalk.yellow('No skills selected. Installation cancelled.'));\n        return;\n      }\n\n      skillsToInstall = skillInfos.filter((info) => selected.includes(info.skillName));\n    } catch (error) {\n      if (error instanceof ExitPromptError) {\n        console.log(chalk.yellow('\\n\\nCancelled by user'));\n        process.exit(0);\n      }\n      throw error;\n    }\n  }\n\n  // Install selected skills\n  const isProject = targetDir.startsWith(process.cwd());\n  let installedCount = 0;\n\n  for (const info of skillsToInstall) {\n    // Warn about conflicts\n    const shouldInstall = await warnIfConflict(info.skillName, info.targetPath, isProject, options.yes);\n    if (!shouldInstall) {\n      console.log(chalk.yellow(`Skipped: ${info.skillName}`));\n      continue; // Skip this skill, continue with next\n    }\n\n    mkdirSync(targetDir, { recursive: true });\n    // Security: ensure target path stays within target directory\n    if (!isPathInside(info.targetPath, targetDir)) {\n      console.error(chalk.red(`Security error: Installation path outside target directory`));\n      continue;\n    }\n    cpSync(info.skillDir, info.targetPath, { recursive: true, dereference: true });\n    writeSkillMetadata(info.targetPath, buildMetadataFromSource(sourceInfo, info.skillDir, repoDir));\n\n    console.log(chalk.green(`✅ Installed: ${info.skillName}`));\n    installedCount++;\n  }\n\n  console.log(chalk.green(`\\n✅ Installation complete: ${installedCount} skill(s) installed`));\n}\n\nfunction buildMetadataFromSource(\n  sourceInfo: InstallSourceInfo,\n  skillDir: string,\n  repoDir: string\n): SkillSourceMetadata {\n  if (sourceInfo.sourceType === 'local') {\n    return buildLocalMetadata(sourceInfo, skillDir);\n  }\n  const subpath = relative(repoDir, skillDir);\n  const normalizedSubpath = subpath === '' ? '' : subpath;\n  return buildGitMetadata(sourceInfo, normalizedSubpath);\n}\n\nfunction buildGitMetadata(sourceInfo: InstallSourceInfo, subpath: string): SkillSourceMetadata {\n  return {\n    source: sourceInfo.source,\n    sourceType: 'git',\n    repoUrl: sourceInfo.repoUrl,\n    subpath,\n    installedAt: new Date().toISOString(),\n  };\n}\n\nfunction buildLocalMetadata(sourceInfo: InstallSourceInfo, skillDir: string): SkillSourceMetadata {\n  return {\n    source: sourceInfo.source,\n    sourceType: 'local',\n    localPath: skillDir,\n    installedAt: new Date().toISOString(),\n  };\n}\n\n/**\n * Warn if installing could conflict with Claude Code marketplace\n * Returns true if should proceed, false if should skip\n */\nasync function warnIfConflict(skillName: string, targetPath: string, isProject: boolean, skipPrompt = false): Promise<boolean> {\n  // Check if overwriting existing skill\n  if (existsSync(targetPath)) {\n    if (skipPrompt) {\n      // Auto-overwrite in non-interactive mode\n      console.log(chalk.dim(`Overwriting: ${skillName}`));\n      return true;\n    }\n    try {\n      const shouldOverwrite = await confirm({\n        message: chalk.yellow(`Skill '${skillName}' already exists. Overwrite?`),\n        default: false,\n      });\n\n      if (!shouldOverwrite) {\n        return false; // Skip this skill, continue with others\n      }\n    } catch (error) {\n      if (error instanceof ExitPromptError) {\n        console.log(chalk.yellow('\\n\\nCancelled by user'));\n        process.exit(0);\n      }\n      throw error;\n    }\n  }\n\n  // Warn about marketplace conflicts (global install only)\n  if (!isProject && ANTHROPIC_MARKETPLACE_SKILLS.includes(skillName)) {\n    console.warn(chalk.yellow(`\\n⚠️  Warning: '${skillName}' matches an Anthropic marketplace skill`));\n    console.warn(chalk.dim('   Installing globally may conflict with Claude Code plugins.'));\n    console.warn(chalk.dim('   If you re-enable Claude plugins, this will be overwritten.'));\n    console.warn(chalk.dim('   Recommend: Use --project flag for conflict-free installation.\\n'));\n  }\n\n  return true; // OK to proceed\n}\n\n/**\n * Get directory size in bytes\n */\nfunction getDirectorySize(dirPath: string): number {\n  let size = 0;\n\n  const entries = readdirSync(dirPath, { withFileTypes: true });\n  for (const entry of entries) {\n    const fullPath = join(dirPath, entry.name);\n    if (entry.isFile()) {\n      size += statSync(fullPath).size;\n    } else if (entry.isDirectory()) {\n      size += getDirectorySize(fullPath);\n    }\n  }\n\n  return size;\n}\n\n/**\n * Format bytes to human-readable size\n */\nfunction formatSize(bytes: number): string {\n  if (bytes < 1024) return `${bytes}B`;\n  if (bytes < 1024 * 1024) return `${(bytes / 1024).toFixed(1)}KB`;\n  return `${(bytes / (1024 * 1024)).toFixed(1)}MB`;\n}\n"
  },
  {
    "path": "src/commands/list.ts",
    "content": "import chalk from 'chalk';\nimport { findAllSkills } from '../utils/skills.js';\n\n/**\n * List all installed skills\n */\nexport function listSkills(): void {\n  console.log(chalk.bold('Available Skills:\\n'));\n\n  const skills = findAllSkills();\n\n  if (skills.length === 0) {\n    console.log('No skills installed.\\n');\n    console.log('Install skills:');\n    console.log(`  ${chalk.cyan('npx openskills install anthropics/skills')}         ${chalk.dim('# Project (default)')}`);\n    console.log(`  ${chalk.cyan('npx openskills install owner/skill --global')}     ${chalk.dim('# Global (advanced)')}`);\n    return;\n  }\n\n  // Sort: project skills first, then global, alphabetically within each\n  const sorted = skills.sort((a, b) => {\n    if (a.location !== b.location) {\n      return a.location === 'project' ? -1 : 1;\n    }\n    return a.name.localeCompare(b.name);\n  });\n\n  // Display with inline location labels\n  for (const skill of sorted) {\n    const locationLabel = skill.location === 'project'\n      ? chalk.blue('(project)')\n      : chalk.dim('(global)');\n\n    console.log(`  ${chalk.bold(skill.name.padEnd(25))} ${locationLabel}`);\n    console.log(`    ${chalk.dim(skill.description)}\\n`);\n  }\n\n  // Summary\n  const projectCount = skills.filter(s => s.location === 'project').length;\n  const globalCount = skills.filter(s => s.location === 'global').length;\n\n  console.log(chalk.dim(`Summary: ${projectCount} project, ${globalCount} global (${skills.length} total)`));\n}\n"
  },
  {
    "path": "src/commands/manage.ts",
    "content": "import { rmSync } from 'fs';\nimport chalk from 'chalk';\nimport { checkbox } from '@inquirer/prompts';\nimport { ExitPromptError } from '@inquirer/core';\nimport { findAllSkills, findSkill } from '../utils/skills.js';\n\n/**\n * Interactively manage (remove) installed skills\n */\nexport async function manageSkills(): Promise<void> {\n  const skills = findAllSkills();\n\n  if (skills.length === 0) {\n    console.log('No skills installed.');\n    return;\n  }\n\n  try {\n    // Sort: project first\n    const sorted = skills.sort((a, b) => {\n      if (a.location !== b.location) {\n        return a.location === 'project' ? -1 : 1;\n      }\n      return a.name.localeCompare(b.name);\n    });\n\n    const choices = sorted.map((skill) => ({\n      name: `${chalk.bold(skill.name.padEnd(25))} ${skill.location === 'project' ? chalk.blue('(project)') : chalk.dim('(global)')}`,\n      value: skill.name,\n      checked: false, // Nothing checked by default\n    }));\n\n    const toRemove = await checkbox({\n      message: 'Select skills to remove',\n      choices,\n      pageSize: 15,\n    });\n\n    if (toRemove.length === 0) {\n      console.log(chalk.yellow('No skills selected for removal.'));\n      return;\n    }\n\n    // Remove selected skills\n    for (const skillName of toRemove) {\n      const skill = findSkill(skillName);\n      if (skill) {\n        rmSync(skill.baseDir, { recursive: true, force: true });\n        const location = skill.source.includes(process.cwd()) ? 'project' : 'global';\n        console.log(chalk.green(`✅ Removed: ${skillName} (${location})`));\n      }\n    }\n\n    console.log(chalk.green(`\\n✅ Removed ${toRemove.length} skill(s)`));\n  } catch (error) {\n    if (error instanceof ExitPromptError) {\n      console.log(chalk.yellow('\\n\\nCancelled by user'));\n      process.exit(0);\n    }\n    throw error;\n  }\n}\n"
  },
  {
    "path": "src/commands/read.ts",
    "content": "import { readFileSync } from 'fs';\nimport { findSkill } from '../utils/skills.js';\nimport { normalizeSkillNames } from '../utils/skill-names.js';\n\n/**\n * Read skill to stdout (for AI agents)\n */\nexport function readSkill(skillNames: string[] | string): void {\n  const names = normalizeSkillNames(skillNames);\n  if (names.length === 0) {\n    console.error('Error: No skill names provided');\n    process.exit(1);\n  }\n  const resolved = [];\n  const missing = [];\n\n  for (const name of names) {\n    const skill = findSkill(name);\n    if (!skill) {\n      missing.push(name);\n      continue;\n    }\n    resolved.push({ name, skill });\n  }\n\n  if (missing.length > 0) {\n    console.error(`Error: Skill(s) not found: ${missing.join(', ')}`);\n    console.error('\\nSearched:');\n    console.error('  .agent/skills/ (project universal)');\n    console.error('  ~/.agent/skills/ (global universal)');\n    console.error('  .claude/skills/ (project)');\n    console.error('  ~/.claude/skills/ (global)');\n    console.error('\\nInstall skills: npx openskills install owner/repo');\n    process.exit(1);\n  }\n\n  for (const { name, skill } of resolved) {\n    const content = readFileSync(skill.path, 'utf-8');\n\n    // Output in Claude Code format\n    console.log(`Reading: ${name}`);\n    console.log(`Base directory: ${skill.baseDir}`);\n    console.log('');\n    console.log(content);\n    console.log('');\n    console.log(`Skill read: ${name}`);\n  }\n}\n"
  },
  {
    "path": "src/commands/remove.ts",
    "content": "import { rmSync } from 'fs';\nimport { homedir } from 'os';\nimport { findSkill } from '../utils/skills.js';\n\n/**\n * Remove installed skill\n */\nexport function removeSkill(skillName: string): void {\n  const skill = findSkill(skillName);\n\n  if (!skill) {\n    console.error(`Error: Skill '${skillName}' not found`);\n    process.exit(1);\n  }\n\n  rmSync(skill.baseDir, { recursive: true, force: true });\n\n  const location = skill.source.includes(homedir()) ? 'global' : 'project';\n  console.log(`✅ Removed: ${skillName}`);\n  console.log(`   From: ${location} (${skill.source})`);\n}\n"
  },
  {
    "path": "src/commands/sync.ts",
    "content": "import { existsSync, readFileSync, writeFileSync, mkdirSync } from 'fs';\nimport { dirname, basename } from 'path';\nimport chalk from 'chalk';\nimport { checkbox } from '@inquirer/prompts';\nimport { ExitPromptError } from '@inquirer/core';\nimport { findAllSkills } from '../utils/skills.js';\nimport { generateSkillsXml, replaceSkillsSection, parseCurrentSkills, removeSkillsSection } from '../utils/agents-md.js';\nimport type { Skill } from '../types.js';\n\nexport interface SyncOptions {\n  yes?: boolean;\n  output?: string;\n}\n\n/**\n * Sync installed skills to a markdown file\n */\nexport async function syncAgentsMd(options: SyncOptions = {}): Promise<void> {\n  const outputPath = options.output || 'AGENTS.md';\n  const outputName = basename(outputPath);\n\n  // Validate output file is markdown\n  if (!outputPath.endsWith('.md')) {\n    console.error(chalk.red('Error: Output file must be a markdown file (.md)'));\n    process.exit(1);\n  }\n\n  // Create file if it doesn't exist\n  if (!existsSync(outputPath)) {\n    const dir = dirname(outputPath);\n    if (dir && dir !== '.' && !existsSync(dir)) {\n      mkdirSync(dir, { recursive: true });\n    }\n    writeFileSync(outputPath, `# ${outputName.replace('.md', '')}\\n\\n`);\n    console.log(chalk.dim(`Created ${outputPath}`));\n  }\n\n  let skills = findAllSkills();\n\n  if (skills.length === 0) {\n    console.log('No skills installed. Install skills first:');\n    console.log(`  ${chalk.cyan('npx openskills install anthropics/skills --project')}`);\n    return;\n  }\n\n  // Interactive mode by default (unless -y flag)\n  if (!options.yes) {\n    try {\n      // Parse what's currently in output file\n      const content = readFileSync(outputPath, 'utf-8');\n      const currentSkills = parseCurrentSkills(content);\n\n      // Sort: project first\n      const sorted = skills.sort((a, b) => {\n        if (a.location !== b.location) {\n          return a.location === 'project' ? -1 : 1;\n        }\n        return a.name.localeCompare(b.name);\n      });\n\n      const choices = sorted.map((skill) => ({\n        name: `${chalk.bold(skill.name.padEnd(25))} ${skill.location === 'project' ? chalk.blue('(project)') : chalk.dim('(global)')}`,\n        value: skill.name,\n        description: skill.description.slice(0, 70),\n        // Pre-select if currently in file, otherwise default to project skills\n        checked: currentSkills.includes(skill.name) || (currentSkills.length === 0 && skill.location === 'project'),\n      }));\n\n      const selected = await checkbox({\n        message: `Select skills to sync to ${outputName}`,\n        choices,\n        pageSize: 15,\n      });\n\n      if (selected.length === 0) {\n        // User unchecked everything - remove skills section\n        const content = readFileSync(outputPath, 'utf-8');\n        const updated = removeSkillsSection(content);\n        writeFileSync(outputPath, updated);\n        console.log(chalk.green(`✅ Removed all skills from ${outputName}`));\n        return;\n      }\n\n      // Filter skills to selected ones\n      skills = skills.filter((s) => selected.includes(s.name));\n    } catch (error) {\n      if (error instanceof ExitPromptError) {\n        console.log(chalk.yellow('\\n\\nCancelled by user'));\n        process.exit(0);\n      }\n      throw error;\n    }\n  }\n\n  const xml = generateSkillsXml(skills);\n  const content = readFileSync(outputPath, 'utf-8');\n  const updated = replaceSkillsSection(content, xml);\n\n  writeFileSync(outputPath, updated);\n\n  const hadMarkers =\n    content.includes('<skills_system') || content.includes('<!-- SKILLS_TABLE_START -->');\n\n  if (hadMarkers) {\n    console.log(chalk.green(`✅ Synced ${skills.length} skill(s) to ${outputName}`));\n  } else {\n    console.log(chalk.green(`✅ Added skills section to ${outputName} (${skills.length} skill(s))`));\n  }\n}\n"
  },
  {
    "path": "src/commands/update.ts",
    "content": "import { cpSync, existsSync, mkdirSync, rmSync } from 'fs';\nimport { dirname, join, resolve, sep } from 'path';\nimport { homedir } from 'os';\nimport { execSync } from 'child_process';\nimport chalk from 'chalk';\nimport ora from 'ora';\nimport { findAllSkills } from '../utils/skills.js';\nimport { normalizeSkillNames } from '../utils/skill-names.js';\nimport { readSkillMetadata, writeSkillMetadata } from '../utils/skill-metadata.js';\n\n/**\n * Update installed skills from their recorded source metadata.\n */\nexport async function updateSkills(skillNames: string[] | string | undefined): Promise<void> {\n  const requested = normalizeSkillNames(skillNames);\n  const skills = findAllSkills();\n\n  if (skills.length === 0) {\n    console.log('No skills installed.\\n');\n    console.log('Install skills:');\n    console.log(`  ${chalk.cyan('npx openskills install anthropics/skills')}         ${chalk.dim('# Project (default)')}`);\n    console.log(`  ${chalk.cyan('npx openskills install owner/skill --global')}     ${chalk.dim('# Global (advanced)')}`);\n    return;\n  }\n\n  let targets = skills;\n\n  if (requested.length > 0) {\n    const requestedSet = new Set(requested);\n    targets = skills.filter((skill) => requestedSet.has(skill.name));\n\n    const missing = requested.filter((name) => !skills.some((skill) => skill.name === name));\n    if (missing.length > 0) {\n      console.log(chalk.yellow(`Skipping missing skills: ${missing.join(', ')}`));\n    }\n  } else {\n    // Default to updating all installed skills\n    targets = skills;\n  }\n\n  if (targets.length === 0) {\n    console.log(chalk.yellow('No matching skills to update.'));\n    return;\n  }\n\n  let updated = 0;\n  let skipped = 0;\n  const missingMetadata: string[] = [];\n  const missingLocalSource: string[] = [];\n  const missingLocalSkillFile: string[] = [];\n  const missingRepoUrl: string[] = [];\n  const missingRepoSkillFile: Array<{ name: string; subpath: string }> = [];\n  const cloneFailures: string[] = [];\n\n  for (const skill of targets) {\n    const metadata = readSkillMetadata(skill.path);\n    if (!metadata) {\n      console.log(chalk.yellow(`Skipped: ${skill.name} (no source metadata; re-install once to enable updates)`));\n      missingMetadata.push(skill.name);\n      skipped++;\n      continue;\n    }\n\n    if (metadata.sourceType === 'local') {\n      const localPath = metadata.localPath;\n      if (!localPath || !existsSync(localPath)) {\n        console.log(chalk.yellow(`Skipped: ${skill.name} (local source missing)`));\n        missingLocalSource.push(skill.name);\n        skipped++;\n        continue;\n      }\n      if (!existsSync(join(localPath, 'SKILL.md'))) {\n        console.log(chalk.yellow(`Skipped: ${skill.name} (SKILL.md missing at local source)`));\n        missingLocalSkillFile.push(skill.name);\n        skipped++;\n        continue;\n      }\n      updateSkillFromDir(skill.path, localPath);\n      writeSkillMetadata(skill.path, { ...metadata, installedAt: new Date().toISOString() });\n      console.log(chalk.green(`✅ Updated: ${skill.name}`));\n      updated++;\n      continue;\n    }\n\n    if (!metadata.repoUrl) {\n      console.log(chalk.yellow(`Skipped: ${skill.name} (missing repo URL metadata)`));\n      missingRepoUrl.push(skill.name);\n      skipped++;\n      continue;\n    }\n\n    const tempDir = join(homedir(), `.openskills-temp-${Date.now()}`);\n    mkdirSync(tempDir, { recursive: true });\n\n    const spinner = ora(`Updating ${skill.name}...`).start();\n    try {\n      execSync(`git clone --depth 1 --quiet \"${metadata.repoUrl}\" \"${tempDir}/repo\"`, { stdio: 'pipe' });\n      const repoDir = join(tempDir, 'repo');\n      const subpath = metadata.subpath && metadata.subpath !== '.' ? metadata.subpath : '';\n      const sourceDir = subpath ? join(repoDir, subpath) : repoDir;\n\n      if (!existsSync(join(sourceDir, 'SKILL.md'))) {\n        spinner.fail(`SKILL.md missing for ${skill.name}`);\n        console.log(chalk.yellow(`Skipped: ${skill.name} (SKILL.md not found in repo at ${subpath || '.'})`));\n        missingRepoSkillFile.push({ name: skill.name, subpath: subpath || '.' });\n        skipped++;\n        continue;\n      }\n\n      updateSkillFromDir(skill.path, sourceDir);\n      writeSkillMetadata(skill.path, { ...metadata, installedAt: new Date().toISOString() });\n      spinner.succeed(`Updated ${skill.name}`);\n      updated++;\n    } catch (error) {\n      spinner.fail(`Failed to update ${skill.name}`);\n      const err = error as { stderr?: Buffer };\n      if (err.stderr) {\n        console.error(chalk.dim(err.stderr.toString().trim()));\n      }\n      console.log(chalk.yellow(`Skipped: ${skill.name} (git clone failed)`));\n      cloneFailures.push(skill.name);\n      skipped++;\n    } finally {\n      rmSync(tempDir, { recursive: true, force: true });\n    }\n  }\n\n  console.log(chalk.dim(`Summary: ${updated} updated, ${skipped} skipped (${targets.length} total)`));\n\n  if (missingMetadata.length > 0) {\n    console.log(chalk.yellow(`Missing source metadata (${missingMetadata.length}): ${missingMetadata.join(', ')}`));\n    console.log(chalk.dim('Re-install these skills once to enable updates (e.g., `npx openskills install <source>`).'));\n  }\n  if (missingLocalSource.length > 0) {\n    console.log(chalk.yellow(`Local source missing (${missingLocalSource.length}): ${missingLocalSource.join(', ')}`));\n  }\n  if (missingLocalSkillFile.length > 0) {\n    console.log(chalk.yellow(`Local SKILL.md missing (${missingLocalSkillFile.length}): ${missingLocalSkillFile.join(', ')}`));\n  }\n  if (missingRepoUrl.length > 0) {\n    console.log(chalk.yellow(`Missing repo URL metadata (${missingRepoUrl.length}): ${missingRepoUrl.join(', ')}`));\n  }\n  if (missingRepoSkillFile.length > 0) {\n    const formatted = missingRepoSkillFile.map((item) => `${item.name} (${item.subpath})`).join(', ');\n    console.log(chalk.yellow(`Repo SKILL.md missing (${missingRepoSkillFile.length}): ${formatted}`));\n  }\n  if (cloneFailures.length > 0) {\n    console.log(chalk.yellow(`Clone failed (${cloneFailures.length}): ${cloneFailures.join(', ')}`));\n  }\n}\n\nfunction updateSkillFromDir(targetPath: string, sourceDir: string): void {\n  const targetDir = dirname(targetPath);\n  mkdirSync(targetDir, { recursive: true });\n\n  if (!isPathInside(targetPath, targetDir)) {\n    console.error(chalk.red('Security error: Installation path outside target directory'));\n    process.exit(1);\n  }\n\n  rmSync(targetPath, { recursive: true, force: true });\n  cpSync(sourceDir, targetPath, { recursive: true, dereference: true });\n}\n\nfunction isPathInside(targetPath: string, targetDir: string): boolean {\n  const resolvedTargetPath = resolve(targetPath);\n  const resolvedTargetDir = resolve(targetDir);\n  const resolvedTargetDirWithSep = resolvedTargetDir.endsWith(sep)\n    ? resolvedTargetDir\n    : resolvedTargetDir + sep;\n  return resolvedTargetPath.startsWith(resolvedTargetDirWithSep);\n}\n"
  },
  {
    "path": "src/types.ts",
    "content": "export interface Skill {\n  name: string;\n  description: string;\n  location: 'project' | 'global';\n  path: string;\n}\n\nexport interface SkillLocation {\n  path: string;\n  baseDir: string;\n  source: string;\n}\n\nexport interface InstallOptions {\n  global?: boolean;\n  universal?: boolean;\n  yes?: boolean;\n}\n\nexport interface SkillMetadata {\n  name: string;\n  description: string;\n  context?: string;\n}\n"
  },
  {
    "path": "src/utils/agents-md.ts",
    "content": "import type { Skill } from '../types.js';\n\n/**\n * Parse skill names currently in AGENTS.md\n */\nexport function parseCurrentSkills(content: string): string[] {\n  const skillNames: string[] = [];\n\n  // Match <skill><name>skill-name</name>...</skill>\n  const skillRegex = /<skill>[\\s\\S]*?<name>([^<]+)<\\/name>[\\s\\S]*?<\\/skill>/g;\n\n  let match;\n  while ((match = skillRegex.exec(content)) !== null) {\n    skillNames.push(match[1].trim());\n  }\n\n  return skillNames;\n}\n\n/**\n * Generate skills XML section for AGENTS.md\n */\nexport function generateSkillsXml(skills: Skill[]): string {\n  const skillTags = skills\n    .map(\n      (s) => `<skill>\n<name>${s.name}</name>\n<description>${s.description}</description>\n<location>${s.location}</location>\n</skill>`\n    )\n    .join('\\n\\n');\n\n  return `<skills_system priority=\"1\">\n\n## Available Skills\n\n<!-- SKILLS_TABLE_START -->\n<usage>\nWhen users ask you to perform tasks, check if any of the available skills below can help complete the task more effectively. Skills provide specialized capabilities and domain knowledge.\n\nHow to use skills:\n- Invoke: \\`npx openskills read <skill-name>\\` (run in your shell)\n  - For multiple: \\`npx openskills read skill-one,skill-two\\`\n- The skill content will load with detailed instructions on how to complete the task\n- Base directory provided in output for resolving bundled resources (references/, scripts/, assets/)\n\nUsage notes:\n- Only use skills listed in <available_skills> below\n- Do not invoke a skill that is already loaded in your context\n- Each skill invocation is stateless\n</usage>\n\n<available_skills>\n\n${skillTags}\n\n</available_skills>\n<!-- SKILLS_TABLE_END -->\n\n</skills_system>`;\n}\n\n/**\n * Replace or add skills section in AGENTS.md\n */\nexport function replaceSkillsSection(content: string, newSection: string): string {\n  const startMarker = '<skills_system';\n  const endMarker = '</skills_system>';\n\n  // Check for XML markers\n  if (content.includes(startMarker)) {\n    const regex = /<skills_system[^>]*>[\\s\\S]*?<\\/skills_system>/;\n    return content.replace(regex, newSection);\n  }\n\n  // Fallback to HTML comments\n  const htmlStartMarker = '<!-- SKILLS_TABLE_START -->';\n  const htmlEndMarker = '<!-- SKILLS_TABLE_END -->';\n\n  if (content.includes(htmlStartMarker)) {\n    // Extract content without outer XML wrapper\n    const innerContent = newSection.replace(/<skills_system[^>]*>|<\\/skills_system>/g, '');\n    const regex = new RegExp(\n      `${htmlStartMarker}[\\\\s\\\\S]*?${htmlEndMarker}`,\n      'g'\n    );\n    return content.replace(regex, `${htmlStartMarker}\\n${innerContent}\\n${htmlEndMarker}`);\n  }\n\n  // No markers found - append to end of file\n  return content.trimEnd() + '\\n\\n' + newSection + '\\n';\n}\n\n/**\n * Remove skills section from AGENTS.md\n */\nexport function removeSkillsSection(content: string): string {\n  const startMarker = '<skills_system';\n  const endMarker = '</skills_system>';\n\n  // Check for XML markers\n  if (content.includes(startMarker)) {\n    const regex = /<skills_system[^>]*>[\\s\\S]*?<\\/skills_system>/;\n    return content.replace(regex, '<!-- Skills section removed -->');\n  }\n\n  // Fallback to HTML comments\n  const htmlStartMarker = '<!-- SKILLS_TABLE_START -->';\n  const htmlEndMarker = '<!-- SKILLS_TABLE_END -->';\n\n  if (content.includes(htmlStartMarker)) {\n    const regex = new RegExp(\n      `${htmlStartMarker}[\\\\s\\\\S]*?${htmlEndMarker}`,\n      'g'\n    );\n    return content.replace(regex, `${htmlStartMarker}\\n<!-- Skills section removed -->\\n${htmlEndMarker}`);\n  }\n\n  // No markers found - nothing to remove\n  return content;\n}\n"
  },
  {
    "path": "src/utils/dirs.ts",
    "content": "import { join } from 'path';\nimport { homedir } from 'os';\n\n/**\n * Get skills directory path\n */\nexport function getSkillsDir(projectLocal: boolean = false, universal: boolean = false): string {\n  const folder = universal ? '.agent/skills' : '.claude/skills';\n  return projectLocal\n    ? join(process.cwd(), folder)\n    : join(homedir(), folder);\n}\n\n/**\n * Get all searchable skill directories in priority order\n * Priority: project .agent > global .agent > project .claude > global .claude\n */\nexport function getSearchDirs(): string[] {\n  return [\n    join(process.cwd(), '.agent/skills'),   // 1. Project universal (.agent)\n    join(homedir(), '.agent/skills'),        // 2. Global universal (.agent)\n    join(process.cwd(), '.claude/skills'),  // 3. Project claude\n    join(homedir(), '.claude/skills'),       // 4. Global claude\n  ];\n}\n"
  },
  {
    "path": "src/utils/marketplace-skills.ts",
    "content": "/**\n * Known skills from Anthropic's marketplace\n * Used to warn about potential conflicts with Claude Code plugins\n */\nexport const ANTHROPIC_MARKETPLACE_SKILLS = [\n  // document-skills plugin\n  'xlsx',\n  'docx',\n  'pptx',\n  'pdf',\n\n  // example-skills plugin\n  'algorithmic-art',\n  'artifacts-builder',\n  'brand-guidelines',\n  'canvas-design',\n  'internal-comms',\n  'mcp-builder',\n  'skill-creator',\n  'slack-gif-creator',\n  'template-skill',\n  'theme-factory',\n  'webapp-testing',\n];\n"
  },
  {
    "path": "src/utils/skill-metadata.ts",
    "content": "import { existsSync, readFileSync, writeFileSync } from 'fs';\nimport { join } from 'path';\n\nexport const SKILL_METADATA_FILE = '.openskills.json';\n\nexport type SkillSourceType = 'git' | 'github' | 'local';\n\nexport interface SkillSourceMetadata {\n  source: string;\n  sourceType: SkillSourceType;\n  repoUrl?: string;\n  subpath?: string;\n  localPath?: string;\n  installedAt: string;\n}\n\nexport function readSkillMetadata(skillDir: string): SkillSourceMetadata | null {\n  const metadataPath = join(skillDir, SKILL_METADATA_FILE);\n  if (!existsSync(metadataPath)) return null;\n\n  try {\n    const raw = readFileSync(metadataPath, 'utf-8');\n    return JSON.parse(raw) as SkillSourceMetadata;\n  } catch {\n    return null;\n  }\n}\n\nexport function writeSkillMetadata(skillDir: string, metadata: SkillSourceMetadata): void {\n  const metadataPath = join(skillDir, SKILL_METADATA_FILE);\n  const payload = {\n    ...metadata,\n    installedAt: metadata.installedAt || new Date().toISOString(),\n  };\n  writeFileSync(metadataPath, JSON.stringify(payload, null, 2));\n}\n"
  },
  {
    "path": "src/utils/skill-names.ts",
    "content": "export function normalizeSkillNames(input: string[] | string | undefined): string[] {\n  if (!input) return [];\n  const raw = Array.isArray(input) ? input : [input];\n  const names = raw.flatMap((name) => name.split(','));\n  const cleaned = names.map((name) => name.trim()).filter(Boolean);\n  return Array.from(new Set(cleaned));\n}\n"
  },
  {
    "path": "src/utils/skills.ts",
    "content": "import { readFileSync, readdirSync, existsSync, statSync, Dirent } from 'fs';\nimport { join } from 'path';\nimport { getSearchDirs } from './dirs.js';\nimport { extractYamlField } from './yaml.js';\nimport type { Skill, SkillLocation } from '../types.js';\n\n/**\n * Check if a directory entry is a directory or a symlink pointing to a directory\n */\nfunction isDirectoryOrSymlinkToDirectory(entry: Dirent, parentDir: string): boolean {\n  if (entry.isDirectory()) {\n    return true;\n  }\n  if (entry.isSymbolicLink()) {\n    try {\n      const fullPath = join(parentDir, entry.name);\n      const stats = statSync(fullPath); // statSync follows symlinks\n      return stats.isDirectory();\n    } catch {\n      // Broken symlink or permission error\n      return false;\n    }\n  }\n  return false;\n}\n\n/**\n * Find all installed skills across directories\n */\nexport function findAllSkills(): Skill[] {\n  const skills: Skill[] = [];\n  const seen = new Set<string>();\n  const dirs = getSearchDirs();\n\n  for (const dir of dirs) {\n    if (!existsSync(dir)) continue;\n\n    const entries = readdirSync(dir, { withFileTypes: true });\n\n    for (const entry of entries) {\n      if (isDirectoryOrSymlinkToDirectory(entry, dir)) {\n        // Deduplicate: only add if we haven't seen this skill name yet\n        if (seen.has(entry.name)) continue;\n\n        const skillPath = join(dir, entry.name, 'SKILL.md');\n        if (existsSync(skillPath)) {\n          const content = readFileSync(skillPath, 'utf-8');\n          const isProjectLocal = dir.includes(process.cwd());\n\n          skills.push({\n            name: entry.name,\n            description: extractYamlField(content, 'description'),\n            location: isProjectLocal ? 'project' : 'global',\n            path: join(dir, entry.name),\n          });\n\n          seen.add(entry.name);\n        }\n      }\n    }\n  }\n\n  return skills;\n}\n\n/**\n * Find specific skill by name\n */\nexport function findSkill(skillName: string): SkillLocation | null {\n  const dirs = getSearchDirs();\n\n  for (const dir of dirs) {\n    const skillPath = join(dir, skillName, 'SKILL.md');\n    if (existsSync(skillPath)) {\n      return {\n        path: skillPath,\n        baseDir: join(dir, skillName),\n        source: dir,\n      };\n    }\n  }\n\n  return null;\n}\n"
  },
  {
    "path": "src/utils/yaml.ts",
    "content": "/**\n * Extract field from YAML frontmatter\n */\nexport function extractYamlField(content: string, field: string): string {\n  const match = content.match(new RegExp(`^${field}:\\\\s*(.+?)$`, 'm'));\n  return match ? match[1].trim() : '';\n}\n\n/**\n * Validate SKILL.md has proper YAML frontmatter\n */\nexport function hasValidFrontmatter(content: string): boolean {\n  return content.trim().startsWith('---');\n}\n"
  },
  {
    "path": "tests/commands/install.test.ts",
    "content": "import { describe, it, expect } from 'vitest';\nimport { resolve, join, sep, win32, basename } from 'path';\nimport { homedir, tmpdir } from 'os';\nimport { existsSync, mkdtempSync, readFileSync, rmSync, writeFileSync } from 'fs';\nimport { extractYamlField, hasValidFrontmatter } from '../../src/utils/yaml.js';\n\n// We need to test the helper functions, but they're not exported\n// So we'll test them indirectly or create a test module\n// For now, let's test the logic patterns directly\n\ndescribe('install.ts helper functions', () => {\n  describe('isLocalPath detection', () => {\n    // Replicate the logic from isLocalPath()\n    const isLocalPath = (source: string): boolean => {\n      return (\n        source.startsWith('/') ||\n        source.startsWith('./') ||\n        source.startsWith('../') ||\n        source.startsWith('~/')\n      );\n    };\n\n    it('should detect absolute paths starting with /', () => {\n      expect(isLocalPath('/absolute/path/to/skill')).toBe(true);\n      expect(isLocalPath('/Users/test/skills')).toBe(true);\n    });\n\n    it('should detect relative paths starting with ./', () => {\n      expect(isLocalPath('./relative/path')).toBe(true);\n      expect(isLocalPath('./skill')).toBe(true);\n    });\n\n    it('should detect parent relative paths starting with ../', () => {\n      expect(isLocalPath('../parent/path')).toBe(true);\n      expect(isLocalPath('../../../deep/path')).toBe(true);\n    });\n\n    it('should detect home directory paths starting with ~/', () => {\n      expect(isLocalPath('~/skills/my-skill')).toBe(true);\n      expect(isLocalPath('~/.claude/skills')).toBe(true);\n    });\n\n    it('should NOT detect GitHub shorthand as local path', () => {\n      expect(isLocalPath('owner/repo')).toBe(false);\n      expect(isLocalPath('anthropics/skills')).toBe(false);\n      expect(isLocalPath('owner/repo/skill-path')).toBe(false);\n    });\n\n    it('should NOT detect git URLs as local path', () => {\n      expect(isLocalPath('git@github.com:owner/repo.git')).toBe(false);\n      expect(isLocalPath('https://github.com/owner/repo')).toBe(false);\n      expect(isLocalPath('http://github.com/owner/repo')).toBe(false);\n    });\n\n    it('should NOT detect plain names as local path', () => {\n      expect(isLocalPath('skill-name')).toBe(false);\n      expect(isLocalPath('my-skill')).toBe(false);\n    });\n  });\n\n  describe('isGitUrl detection', () => {\n    // Replicate the logic from isGitUrl()\n    const isGitUrl = (source: string): boolean => {\n      return (\n        source.startsWith('git@') ||\n        source.startsWith('git://') ||\n        source.startsWith('http://') ||\n        source.startsWith('https://') ||\n        source.endsWith('.git')\n      );\n    };\n\n    it('should detect SSH git URLs', () => {\n      expect(isGitUrl('git@github.com:owner/repo.git')).toBe(true);\n      expect(isGitUrl('git@gitlab.com:group/project.git')).toBe(true);\n      expect(isGitUrl('git@bitbucket.org:team/repo.git')).toBe(true);\n    });\n\n    it('should detect git:// protocol URLs', () => {\n      expect(isGitUrl('git://github.com/owner/repo.git')).toBe(true);\n    });\n\n    it('should detect HTTPS URLs', () => {\n      expect(isGitUrl('https://github.com/owner/repo')).toBe(true);\n      expect(isGitUrl('https://github.com/owner/repo.git')).toBe(true);\n      expect(isGitUrl('https://gitlab.com/group/project')).toBe(true);\n    });\n\n    it('should detect HTTP URLs', () => {\n      expect(isGitUrl('http://github.com/owner/repo')).toBe(true);\n    });\n\n    it('should detect URLs ending in .git', () => {\n      expect(isGitUrl('custom-host.com/repo.git')).toBe(true);\n      expect(isGitUrl('anything.git')).toBe(true);\n    });\n\n    it('should NOT detect GitHub shorthand as git URL', () => {\n      expect(isGitUrl('owner/repo')).toBe(false);\n      expect(isGitUrl('anthropics/skills')).toBe(false);\n    });\n\n    it('should NOT detect local paths as git URL', () => {\n      expect(isGitUrl('/absolute/path')).toBe(false);\n      expect(isGitUrl('./relative/path')).toBe(false);\n      expect(isGitUrl('~/home/path')).toBe(false);\n    });\n  });\n\n  describe('expandPath tilde expansion', () => {\n    // Replicate the logic from expandPath()\n    const expandPath = (source: string): string => {\n      if (source.startsWith('~/')) {\n        return join(homedir(), source.slice(2));\n      }\n      return resolve(source);\n    };\n\n    it('should expand ~ to home directory', () => {\n      const expanded = expandPath('~/skills/test');\n      expect(expanded).toBe(join(homedir(), 'skills/test'));\n    });\n\n    it('should expand ~/.claude/skills correctly', () => {\n      const expanded = expandPath('~/.claude/skills');\n      expect(expanded).toBe(join(homedir(), '.claude/skills'));\n    });\n\n    it('should resolve relative paths', () => {\n      const expanded = expandPath('./relative');\n      expect(expanded).toBe(resolve('./relative'));\n    });\n\n    it('should keep absolute paths as-is (resolved)', () => {\n      const expanded = expandPath('/absolute/path');\n      expect(expanded).toBe('/absolute/path');\n    });\n  });\n\n  describe('path traversal security', () => {\n    // Test the security check logic\n    const isPathSafe = (\n      targetPath: string,\n      targetDir: string,\n      pathResolve = resolve,\n      pathSep = sep\n    ): boolean => {\n      const resolvedTargetPath = pathResolve(targetPath);\n      const resolvedTargetDir = pathResolve(targetDir);\n      const resolvedTargetDirWithSep = resolvedTargetDir.endsWith(pathSep)\n        ? resolvedTargetDir\n        : resolvedTargetDir + pathSep;\n      return resolvedTargetPath.startsWith(resolvedTargetDirWithSep);\n    };\n\n    it('should allow normal skill paths within target directory', () => {\n      expect(isPathSafe('/home/user/.claude/skills/my-skill', '/home/user/.claude/skills')).toBe(true);\n    });\n\n    it('should block path traversal attempts with ../', () => {\n      expect(isPathSafe('/home/user/.claude/skills/../../../etc/passwd', '/home/user/.claude/skills')).toBe(false);\n    });\n\n    it('should block paths outside target directory', () => {\n      expect(isPathSafe('/etc/passwd', '/home/user/.claude/skills')).toBe(false);\n    });\n\n    it('should block paths that are prefix but not subdirectory', () => {\n      // /home/user/.claude/skills-evil should NOT be allowed when target is /home/user/.claude/skills\n      expect(isPathSafe('/home/user/.claude/skills-evil', '/home/user/.claude/skills')).toBe(false);\n    });\n\n    it('should allow nested subdirectories', () => {\n      expect(isPathSafe('/home/user/.claude/skills/category/my-skill', '/home/user/.claude/skills')).toBe(true);\n    });\n\n    it('should allow Windows paths within target directory', () => {\n      expect(\n        isPathSafe(\n          'C:\\\\Users\\\\dev\\\\.claude\\\\skills\\\\my-skill',\n          'C:\\\\Users\\\\dev\\\\.claude\\\\skills',\n          win32.resolve,\n          win32.sep\n        )\n      ).toBe(true);\n    });\n\n    it('should block Windows path traversal attempts', () => {\n      expect(\n        isPathSafe(\n          'C:\\\\Users\\\\dev\\\\.claude\\\\skills\\\\..\\\\..\\\\Windows',\n          'C:\\\\Users\\\\dev\\\\.claude\\\\skills',\n          win32.resolve,\n          win32.sep\n        )\n      ).toBe(false);\n    });\n\n    it('should block Windows prefix-but-not-child paths', () => {\n      expect(\n        isPathSafe(\n          'C:\\\\Users\\\\dev\\\\.claude\\\\skills-evil',\n          'C:\\\\Users\\\\dev\\\\.claude\\\\skills',\n          win32.resolve,\n          win32.sep\n        )\n      ).toBe(false);\n    });\n  });\n\n  describe('root SKILL.md detection', () => {\n    const getRootSkillName = (repoDir: string, repoName?: string): string | null => {\n      const skillPath = join(repoDir, 'SKILL.md');\n      if (!existsSync(skillPath)) return null;\n      const content = readFileSync(skillPath, 'utf-8');\n      if (!hasValidFrontmatter(content)) return null;\n      return extractYamlField(content, 'name') || repoName || basename(repoDir);\n    };\n\n    it('should detect root SKILL.md and use frontmatter name', () => {\n      const tempDir = mkdtempSync(join(tmpdir(), 'openskills-test-'));\n      try {\n        writeFileSync(\n          join(tempDir, 'SKILL.md'),\n          \"---\\nname: claude-android-skill\\ndescription: Android helper\\n---\\n\\n# Skill\\n\"\n        );\n        expect(getRootSkillName(tempDir, 'claude-android-skill')).toBe('claude-android-skill');\n      } finally {\n        rmSync(tempDir, { recursive: true, force: true });\n      }\n    });\n\n    it('should fall back to repo name when frontmatter name is missing', () => {\n      const tempDir = mkdtempSync(join(tmpdir(), 'openskills-test-'));\n      try {\n        writeFileSync(\n          join(tempDir, 'SKILL.md'),\n          \"---\\ndescription: Android helper\\n---\\n\\n# Skill\\n\"\n        );\n        expect(getRootSkillName(tempDir, 'claude-android-skill')).toBe('claude-android-skill');\n      } finally {\n        rmSync(tempDir, { recursive: true, force: true });\n      }\n    });\n  });\n});\n\ndescribe('GitHub shorthand parsing', () => {\n  // Test the parsing logic for owner/repo and owner/repo/path\n  const parseGitHubShorthand = (source: string): { repoUrl: string; skillSubpath: string } | null => {\n    const parts = source.split('/');\n    if (parts.length === 2) {\n      return {\n        repoUrl: `https://github.com/${source}`,\n        skillSubpath: '',\n      };\n    } else if (parts.length > 2) {\n      return {\n        repoUrl: `https://github.com/${parts[0]}/${parts[1]}`,\n        skillSubpath: parts.slice(2).join('/'),\n      };\n    }\n    return null;\n  };\n\n  it('should parse owner/repo format', () => {\n    const result = parseGitHubShorthand('anthropics/skills');\n    expect(result).not.toBeNull();\n    expect(result?.repoUrl).toBe('https://github.com/anthropics/skills');\n    expect(result?.skillSubpath).toBe('');\n  });\n\n  it('should parse owner/repo/skill-path format', () => {\n    const result = parseGitHubShorthand('anthropics/skills/document-skills/pdf');\n    expect(result).not.toBeNull();\n    expect(result?.repoUrl).toBe('https://github.com/anthropics/skills');\n    expect(result?.skillSubpath).toBe('document-skills/pdf');\n  });\n\n  it('should parse owner/repo/nested/path format', () => {\n    const result = parseGitHubShorthand('owner/repo/deep/nested/skill');\n    expect(result).not.toBeNull();\n    expect(result?.repoUrl).toBe('https://github.com/owner/repo');\n    expect(result?.skillSubpath).toBe('deep/nested/skill');\n  });\n\n  it('should return null for single part', () => {\n    const result = parseGitHubShorthand('single');\n    expect(result).toBeNull();\n  });\n});\n"
  },
  {
    "path": "tests/commands/sync.test.ts",
    "content": "import { describe, it, expect, beforeEach, afterEach } from 'vitest';\nimport { mkdirSync, writeFileSync, readFileSync, rmSync, existsSync } from 'fs';\nimport { join, dirname, basename } from 'path';\nimport { tmpdir } from 'os';\n\n// Test the sync utility functions directly\nimport {\n  generateSkillsXml,\n  replaceSkillsSection,\n  parseCurrentSkills,\n  removeSkillsSection,\n} from '../../src/utils/agents-md.js';\nimport type { Skill } from '../../src/types.js';\n\nconst testId = Math.random().toString(36).slice(2);\nconst testTempDir = join(tmpdir(), `openskills-sync-test-${testId}`);\n\ndescribe('sync utilities (agents-md.ts)', () => {\n  describe('generateSkillsXml', () => {\n    it('should generate valid XML for skills', () => {\n      const skills: Skill[] = [\n        { name: 'pdf', description: 'PDF manipulation', location: 'project', path: '/path/to/pdf' },\n        { name: 'xlsx', description: 'Spreadsheet editing', location: 'global', path: '/path/to/xlsx' },\n      ];\n\n      const xml = generateSkillsXml(skills);\n\n      expect(xml).toContain('<skills_system priority=\"1\">');\n      expect(xml).toContain('<name>pdf</name>');\n      expect(xml).toContain('<description>PDF manipulation</description>');\n      expect(xml).toContain('<location>project</location>');\n      expect(xml).toContain('<name>xlsx</name>');\n      expect(xml).toContain('<description>Spreadsheet editing</description>');\n      expect(xml).toContain('<location>global</location>');\n      expect(xml).toContain('</skills_system>');\n    });\n\n    it('should include usage instructions', () => {\n      const skills: Skill[] = [\n        { name: 'test', description: 'Test skill', location: 'project', path: '/path' },\n      ];\n\n      const xml = generateSkillsXml(skills);\n\n      expect(xml).toContain('<usage>');\n      expect(xml).toContain('npx openskills read');\n      expect(xml).toContain('</usage>');\n    });\n\n    it('should generate empty skills section for empty array', () => {\n      const xml = generateSkillsXml([]);\n\n      expect(xml).toContain('<available_skills>');\n      expect(xml).toContain('</available_skills>');\n    });\n  });\n\n  describe('parseCurrentSkills', () => {\n    it('should parse skill names from existing content', () => {\n      const content = `\n# AGENTS.md\n\n<skills_system>\n<available_skills>\n<skill>\n<name>pdf</name>\n<description>PDF tools</description>\n</skill>\n<skill>\n<name>xlsx</name>\n<description>Excel tools</description>\n</skill>\n</available_skills>\n</skills_system>\n`;\n\n      const skills = parseCurrentSkills(content);\n\n      expect(skills).toContain('pdf');\n      expect(skills).toContain('xlsx');\n      expect(skills).toHaveLength(2);\n    });\n\n    it('should return empty array for content without skills', () => {\n      const content = '# AGENTS.md\\n\\nNo skills here.';\n\n      const skills = parseCurrentSkills(content);\n\n      expect(skills).toHaveLength(0);\n    });\n\n    it('should handle malformed XML gracefully', () => {\n      const content = '<skill><name>broken';\n\n      const skills = parseCurrentSkills(content);\n\n      expect(Array.isArray(skills)).toBe(true);\n    });\n  });\n\n  describe('replaceSkillsSection', () => {\n    it('should replace existing skills_system section', () => {\n      const content = `# AGENTS.md\n\n<skills_system priority=\"1\">\nOLD CONTENT\n</skills_system>\n\nOther content`;\n\n      const newSection = '<skills_system priority=\"1\">NEW CONTENT</skills_system>';\n      const result = replaceSkillsSection(content, newSection);\n\n      expect(result).toContain('NEW CONTENT');\n      expect(result).not.toContain('OLD CONTENT');\n      expect(result).toContain('Other content');\n    });\n\n    it('should replace HTML comment markers', () => {\n      const content = `# AGENTS.md\n\n<!-- SKILLS_TABLE_START -->\nOLD SKILLS\n<!-- SKILLS_TABLE_END -->\n\nFooter`;\n\n      const newSection = '<skills_system>NEW SKILLS</skills_system>';\n      const result = replaceSkillsSection(content, newSection);\n\n      expect(result).toContain('NEW SKILLS');\n      expect(result).not.toContain('OLD SKILLS');\n    });\n\n    it('should append to end if no markers found', () => {\n      const content = '# AGENTS.md\\n\\nSome content.';\n      const newSection = '<skills_system>SKILLS</skills_system>';\n\n      const result = replaceSkillsSection(content, newSection);\n\n      expect(result).toContain('Some content.');\n      expect(result).toContain('<skills_system>SKILLS</skills_system>');\n    });\n  });\n\n  describe('removeSkillsSection', () => {\n    it('should remove skills_system section', () => {\n      const content = `# AGENTS.md\n\n<skills_system priority=\"1\">\nSkills content\n</skills_system>\n\nFooter`;\n\n      const result = removeSkillsSection(content);\n\n      expect(result).not.toContain('Skills content');\n      expect(result).toContain('Footer');\n    });\n\n    it('should handle content without skills section', () => {\n      const content = '# AGENTS.md\\n\\nNo skills.';\n\n      const result = removeSkillsSection(content);\n\n      expect(result).toBe(content);\n    });\n  });\n});\n\ndescribe('sync --output flag logic', () => {\n  beforeEach(() => {\n    mkdirSync(testTempDir, { recursive: true });\n  });\n\n  afterEach(() => {\n    rmSync(testTempDir, { recursive: true, force: true });\n  });\n\n  describe('output path validation', () => {\n    it('should accept .md files', () => {\n      const validPaths = [\n        'AGENTS.md',\n        'custom.md',\n        '.ruler/AGENTS.md',\n        'docs/rules.md',\n      ];\n\n      for (const path of validPaths) {\n        expect(path.endsWith('.md')).toBe(true);\n      }\n    });\n\n    it('should reject non-.md files', () => {\n      const invalidPaths = [\n        'AGENTS.txt',\n        'rules.yaml',\n        'config.json',\n        'noextension',\n      ];\n\n      for (const path of invalidPaths) {\n        expect(path.endsWith('.md')).toBe(false);\n      }\n    });\n  });\n\n  describe('auto-create file behavior', () => {\n    it('should create file with heading if not exists', () => {\n      const outputPath = join(testTempDir, 'NEW-FILE.md');\n\n      // Simulate the auto-create logic\n      if (!existsSync(outputPath)) {\n        const outputName = basename(outputPath);\n        writeFileSync(outputPath, `# ${outputName.replace('.md', '')}\\n\\n`);\n      }\n\n      expect(existsSync(outputPath)).toBe(true);\n      const content = readFileSync(outputPath, 'utf-8');\n      expect(content).toBe('# NEW-FILE\\n\\n');\n    });\n\n    it('should create nested directories if needed', () => {\n      const outputPath = join(testTempDir, 'nested', 'deep', 'AGENTS.md');\n      const dir = dirname(outputPath);\n\n      // Simulate the directory creation logic\n      if (!existsSync(dir)) {\n        mkdirSync(dir, { recursive: true });\n      }\n      writeFileSync(outputPath, '# AGENTS\\n\\n');\n\n      expect(existsSync(outputPath)).toBe(true);\n      expect(existsSync(dir)).toBe(true);\n    });\n\n    it('should preserve existing file content', () => {\n      const outputPath = join(testTempDir, 'existing.md');\n      const existingContent = '# Existing Content\\n\\nImportant stuff here.';\n      writeFileSync(outputPath, existingContent);\n\n      // Read existing content\n      const content = readFileSync(outputPath, 'utf-8');\n\n      expect(content).toBe(existingContent);\n    });\n  });\n\n  describe('default output path', () => {\n    it('should default to AGENTS.md', () => {\n      const defaultPath = 'AGENTS.md';\n      expect(defaultPath).toBe('AGENTS.md');\n    });\n  });\n});\n"
  },
  {
    "path": "tests/commands/update.test.ts",
    "content": "import { describe, it, expect, beforeEach, afterEach } from 'vitest';\nimport { mkdtempSync, mkdirSync, writeFileSync, readFileSync, rmSync } from 'fs';\nimport { join } from 'path';\nimport { tmpdir } from 'os';\nimport { updateSkills } from '../../src/commands/update.js';\nimport { writeSkillMetadata } from '../../src/utils/skill-metadata.js';\n\ndescribe('updateSkills', () => {\n  const originalCwd = process.cwd();\n  const originalHome = process.env.HOME;\n  let tempRoot: string;\n  let projectDir: string;\n\n  beforeEach(() => {\n    tempRoot = mkdtempSync(join(tmpdir(), 'openskills-update-test-'));\n    projectDir = join(tempRoot, 'project');\n    mkdirSync(projectDir, { recursive: true });\n    process.chdir(projectDir);\n\n    process.env.HOME = join(tempRoot, 'home');\n    mkdirSync(process.env.HOME, { recursive: true });\n  });\n\n  afterEach(() => {\n    process.chdir(originalCwd);\n    if (originalHome) {\n      process.env.HOME = originalHome;\n    } else {\n      delete process.env.HOME;\n    }\n    rmSync(tempRoot, { recursive: true, force: true });\n  });\n\n  it('updates a local skill from recorded source', async () => {\n    const sourceDir = join(tempRoot, 'source-skill');\n    mkdirSync(sourceDir, { recursive: true });\n    writeFileSync(\n      join(sourceDir, 'SKILL.md'),\n      \"---\\nname: demo\\ndescription: v2\\n---\\n\\n# Demo\\nv2\\n\"\n    );\n\n    const targetDir = join(projectDir, '.claude/skills/demo');\n    mkdirSync(targetDir, { recursive: true });\n    writeFileSync(\n      join(targetDir, 'SKILL.md'),\n      \"---\\nname: demo\\ndescription: v1\\n---\\n\\n# Demo\\nv1\\n\"\n    );\n\n    writeSkillMetadata(targetDir, {\n      source: './source-skill',\n      sourceType: 'local',\n      localPath: sourceDir,\n      installedAt: '2026-01-01T00:00:00.000Z',\n    });\n\n    await updateSkills([]);\n\n    const updated = readFileSync(join(targetDir, 'SKILL.md'), 'utf-8');\n    expect(updated).toContain('v2');\n  });\n\n  it('skips skills without metadata without deleting them', async () => {\n    const targetDir = join(projectDir, '.claude/skills/no-metadata');\n    mkdirSync(targetDir, { recursive: true });\n    writeFileSync(\n      join(targetDir, 'SKILL.md'),\n      \"---\\nname: no-metadata\\ndescription: v1\\n---\\n\\n# Demo\\nv1\\n\"\n    );\n\n    await updateSkills([]);\n\n    const content = readFileSync(join(targetDir, 'SKILL.md'), 'utf-8');\n    expect(content).toContain('v1');\n  });\n});\n"
  },
  {
    "path": "tests/integration/e2e.test.ts",
    "content": "import { describe, it, expect, beforeEach, afterEach } from 'vitest';\nimport { mkdirSync, writeFileSync, readFileSync, rmSync, existsSync, symlinkSync } from 'fs';\nimport { join } from 'path';\nimport { tmpdir } from 'os';\nimport { execSync } from 'child_process';\n\nconst testId = Math.random().toString(36).slice(2);\nconst testTempDir = join(tmpdir(), `openskills-e2e-${testId}`);\nconst cliPath = join(process.cwd(), 'dist', 'cli.js');\n\n// Helper to run CLI commands\nfunction runCli(args: string, cwd?: string): { stdout: string; stderr: string; exitCode: number } {\n  try {\n    const stdout = execSync(`node ${cliPath} ${args}`, {\n      cwd: cwd || testTempDir,\n      encoding: 'utf-8',\n      stdio: ['pipe', 'pipe', 'pipe'],\n    });\n    return { stdout, stderr: '', exitCode: 0 };\n  } catch (error: unknown) {\n    const err = error as { stdout?: string; stderr?: string; status?: number };\n    return {\n      stdout: err.stdout || '',\n      stderr: err.stderr || '',\n      exitCode: err.status || 1,\n    };\n  }\n}\n\n// Helper to create a valid skill\nfunction createTestSkill(dir: string, name: string, description: string = 'Test skill'): void {\n  const skillDir = join(dir, name);\n  mkdirSync(skillDir, { recursive: true });\n  writeFileSync(\n    join(skillDir, 'SKILL.md'),\n    `---\nname: ${name}\ndescription: ${description}\n---\n\n# ${name}\n\nInstructions for ${name}.\n`\n  );\n}\n\ndescribe('End-to-end CLI tests', () => {\n  beforeEach(() => {\n    mkdirSync(testTempDir, { recursive: true });\n  });\n\n  afterEach(() => {\n    rmSync(testTempDir, { recursive: true, force: true });\n  });\n\n  describe('openskills list', () => {\n    it('should list installed skills', () => {\n      // Create a project skills directory with a skill\n      const skillsDir = join(testTempDir, '.claude', 'skills');\n      createTestSkill(skillsDir, 'test-skill', 'A test skill');\n\n      const result = runCli('list');\n\n      expect(result.exitCode).toBe(0);\n      expect(result.stdout).toContain('test-skill');\n    });\n\n    it('should show summary with skill counts', () => {\n      // Create empty project skills directory - global skills may still exist\n      mkdirSync(join(testTempDir, '.claude', 'skills'), { recursive: true });\n\n      const result = runCli('list');\n\n      // Should show a summary line with counts\n      expect(result.stdout).toMatch(/Summary:|No skills installed/);\n    });\n  });\n\n  describe('openskills read', () => {\n    it('should read skill content', () => {\n      const skillsDir = join(testTempDir, '.claude', 'skills');\n      createTestSkill(skillsDir, 'readable-skill', 'Readable skill description');\n\n      const result = runCli('read readable-skill');\n\n      expect(result.exitCode).toBe(0);\n      expect(result.stdout).toContain('readable-skill');\n      expect(result.stdout).toContain('Instructions for readable-skill');\n    });\n\n    it('should error for non-existent skill', () => {\n      const result = runCli('read non-existent-skill');\n\n      expect(result.exitCode).toBe(1);\n      expect(result.stderr).toContain('not found');\n    });\n  });\n\n  describe('openskills sync', () => {\n    it('should sync skills to AGENTS.md', () => {\n      // Create skill and AGENTS.md\n      const skillsDir = join(testTempDir, '.claude', 'skills');\n      createTestSkill(skillsDir, 'sync-skill', 'Skill to sync');\n      writeFileSync(join(testTempDir, 'AGENTS.md'), '# AGENTS\\n');\n\n      const result = runCli('sync -y');\n\n      expect(result.exitCode).toBe(0);\n\n      const agentsMd = readFileSync(join(testTempDir, 'AGENTS.md'), 'utf-8');\n      expect(agentsMd).toContain('sync-skill');\n      expect(agentsMd).toContain('<skills_system');\n    });\n\n    it('should create output file with --output flag', () => {\n      const skillsDir = join(testTempDir, '.claude', 'skills');\n      createTestSkill(skillsDir, 'output-skill', 'Test output');\n\n      const outputPath = join(testTempDir, 'custom-output.md');\n      const result = runCli(`sync -y --output ${outputPath}`);\n\n      expect(result.exitCode).toBe(0);\n      expect(existsSync(outputPath)).toBe(true);\n\n      const content = readFileSync(outputPath, 'utf-8');\n      expect(content).toContain('output-skill');\n    });\n\n    it('should create nested directories with --output flag', () => {\n      const skillsDir = join(testTempDir, '.claude', 'skills');\n      createTestSkill(skillsDir, 'nested-skill', 'Test nested');\n\n      const outputPath = join(testTempDir, '.ruler', 'deep', 'AGENTS.md');\n      const result = runCli(`sync -y --output ${outputPath}`);\n\n      expect(result.exitCode).toBe(0);\n      expect(existsSync(outputPath)).toBe(true);\n    });\n\n    it('should reject non-.md output files', () => {\n      const skillsDir = join(testTempDir, '.claude', 'skills');\n      createTestSkill(skillsDir, 'any-skill', 'Test');\n\n      const result = runCli(`sync -y --output ${join(testTempDir, 'invalid.txt')}`);\n\n      expect(result.exitCode).toBe(1);\n      expect(result.stderr).toContain('.md');\n    });\n  });\n\n  describe('openskills install (local paths)', () => {\n    it('should install from absolute local path', () => {\n      // Create a source skill\n      const sourceDir = join(testTempDir, 'source-skills');\n      createTestSkill(sourceDir, 'local-skill', 'Local skill');\n\n      // Install to project\n      const result = runCli(`install ${join(sourceDir, 'local-skill')} -y`);\n\n      expect(result.exitCode).toBe(0);\n      expect(result.stdout).toContain('Installed');\n\n      // Verify skill was copied\n      const installedPath = join(testTempDir, '.claude', 'skills', 'local-skill', 'SKILL.md');\n      expect(existsSync(installedPath)).toBe(true);\n    });\n\n    it('should install directory of skills from local path', () => {\n      // Create multiple source skills\n      const sourceDir = join(testTempDir, 'multi-skills');\n      createTestSkill(sourceDir, 'skill-one', 'First skill');\n      createTestSkill(sourceDir, 'skill-two', 'Second skill');\n\n      const result = runCli(`install ${sourceDir} -y`);\n\n      expect(result.exitCode).toBe(0);\n      expect(result.stdout).toContain('skill-one');\n      expect(result.stdout).toContain('skill-two');\n    });\n\n    it('should error for non-existent local path', () => {\n      const result = runCli(`install /non/existent/path -y`);\n\n      expect(result.exitCode).toBe(1);\n      expect(result.stderr).toContain('does not exist');\n    });\n  });\n\n  describe('openskills remove', () => {\n    it('should remove installed skill', () => {\n      const skillsDir = join(testTempDir, '.claude', 'skills');\n      createTestSkill(skillsDir, 'removable-skill', 'To be removed');\n\n      const result = runCli('remove removable-skill');\n\n      expect(result.exitCode).toBe(0);\n      expect(result.stdout).toContain('Removed');\n      expect(existsSync(join(skillsDir, 'removable-skill'))).toBe(false);\n    });\n\n    it('should error for non-existent skill', () => {\n      const result = runCli('remove ghost-skill');\n\n      expect(result.exitCode).toBe(1);\n    });\n  });\n\n  describe('symlinked skills', () => {\n    it('should list symlinked skills', () => {\n      // Create a skill in a separate location\n      const actualSkillDir = join(testTempDir, 'actual-skills');\n      createTestSkill(actualSkillDir, 'symlinked-skill', 'Symlinked skill');\n\n      // Create symlink in skills directory\n      const skillsDir = join(testTempDir, '.claude', 'skills');\n      mkdirSync(skillsDir, { recursive: true });\n      symlinkSync(\n        join(actualSkillDir, 'symlinked-skill'),\n        join(skillsDir, 'symlinked-skill')\n      );\n\n      const result = runCli('list');\n\n      expect(result.exitCode).toBe(0);\n      expect(result.stdout).toContain('symlinked-skill');\n    });\n\n    it('should read symlinked skill content', () => {\n      const actualSkillDir = join(testTempDir, 'actual-skills');\n      createTestSkill(actualSkillDir, 'linked-readable', 'Linked readable');\n\n      const skillsDir = join(testTempDir, '.claude', 'skills');\n      mkdirSync(skillsDir, { recursive: true });\n      symlinkSync(\n        join(actualSkillDir, 'linked-readable'),\n        join(skillsDir, 'linked-readable')\n      );\n\n      const result = runCli('read linked-readable');\n\n      expect(result.exitCode).toBe(0);\n      expect(result.stdout).toContain('linked-readable');\n    });\n  });\n\n  describe('--yes flag behavior', () => {\n    it('should auto-overwrite with -y flag', () => {\n      // Create initial skill\n      const skillsDir = join(testTempDir, '.claude', 'skills');\n      createTestSkill(skillsDir, 'overwrite-skill', 'Original');\n\n      // Create source skill to install\n      const sourceDir = join(testTempDir, 'source');\n      createTestSkill(sourceDir, 'overwrite-skill', 'Updated');\n\n      // Install with -y should overwrite\n      const result = runCli(`install ${join(sourceDir, 'overwrite-skill')} -y`);\n\n      expect(result.exitCode).toBe(0);\n      expect(result.stdout).toContain('Overwriting');\n\n      // Verify content was updated\n      const content = readFileSync(join(skillsDir, 'overwrite-skill', 'SKILL.md'), 'utf-8');\n      expect(content).toContain('Updated');\n    });\n  });\n});\n"
  },
  {
    "path": "tests/utils/dirs.test.ts",
    "content": "import { describe, it, expect } from 'vitest';\nimport { join } from 'path';\nimport { homedir } from 'os';\nimport { getSkillsDir, getSearchDirs } from '../../src/utils/dirs.js';\n\ndescribe('getSkillsDir', () => {\n  it('should return global .claude dir by default', () => {\n    const dir = getSkillsDir();\n    expect(dir).toBe(join(homedir(), '.claude/skills'));\n  });\n\n  it('should return project .claude dir when projectLocal is true', () => {\n    const dir = getSkillsDir(true);\n    expect(dir).toBe(join(process.cwd(), '.claude/skills'));\n  });\n\n  it('should return global .agent dir when universal is true', () => {\n    const dir = getSkillsDir(false, true);\n    expect(dir).toBe(join(homedir(), '.agent/skills'));\n  });\n\n  it('should return project .agent dir when both projectLocal and universal are true', () => {\n    const dir = getSkillsDir(true, true);\n    expect(dir).toBe(join(process.cwd(), '.agent/skills'));\n  });\n});\n\ndescribe('getSearchDirs', () => {\n  it('should return all 4 dirs in priority order', () => {\n    const dirs = getSearchDirs();\n    expect(dirs).toHaveLength(4);\n    expect(dirs[0]).toBe(join(process.cwd(), '.agent/skills'));   // 1. Project universal\n    expect(dirs[1]).toBe(join(homedir(), '.agent/skills'));        // 2. Global universal\n    expect(dirs[2]).toBe(join(process.cwd(), '.claude/skills'));  // 3. Project claude\n    expect(dirs[3]).toBe(join(homedir(), '.claude/skills'));       // 4. Global claude\n  });\n});\n"
  },
  {
    "path": "tests/utils/skill-metadata.test.ts",
    "content": "import { describe, it, expect, beforeEach, afterEach } from 'vitest';\nimport { mkdtempSync, rmSync, writeFileSync } from 'fs';\nimport { join } from 'path';\nimport { tmpdir } from 'os';\nimport { readSkillMetadata, writeSkillMetadata, SKILL_METADATA_FILE } from '../../src/utils/skill-metadata.js';\n\ndescribe('skill-metadata', () => {\n  let tempDir: string;\n\n  beforeEach(() => {\n    tempDir = mkdtempSync(join(tmpdir(), 'openskills-metadata-test-'));\n  });\n\n  afterEach(() => {\n    rmSync(tempDir, { recursive: true, force: true });\n  });\n\n  it('writes and reads metadata', () => {\n    const payload = {\n      source: 'owner/repo',\n      sourceType: 'git' as const,\n      repoUrl: 'https://github.com/owner/repo',\n      subpath: 'skills/demo',\n      installedAt: '2026-01-01T00:00:00.000Z',\n    };\n\n    writeSkillMetadata(tempDir, payload);\n    const read = readSkillMetadata(tempDir);\n\n    expect(read).toMatchObject(payload);\n  });\n\n  it('returns null when metadata is missing', () => {\n    expect(readSkillMetadata(tempDir)).toBeNull();\n  });\n\n  it('returns null for invalid JSON', () => {\n    writeFileSync(join(tempDir, SKILL_METADATA_FILE), '{not-json');\n    expect(readSkillMetadata(tempDir)).toBeNull();\n  });\n});\n"
  },
  {
    "path": "tests/utils/skill-names.test.ts",
    "content": "import { describe, it, expect } from 'vitest';\nimport { normalizeSkillNames } from '../../src/utils/skill-names.js';\n\ndescribe('normalizeSkillNames', () => {\n  it('splits comma-separated names', () => {\n    expect(normalizeSkillNames('alpha,beta')).toEqual(['alpha', 'beta']);\n  });\n\n  it('trims whitespace and removes empties', () => {\n    expect(normalizeSkillNames(' alpha, , beta ,')).toEqual(['alpha', 'beta']);\n  });\n\n  it('supports arrays with comma values', () => {\n    expect(normalizeSkillNames(['alpha', 'beta,gamma'])).toEqual(['alpha', 'beta', 'gamma']);\n  });\n\n  it('deduplicates names', () => {\n    expect(normalizeSkillNames(['alpha', 'alpha', 'beta'])).toEqual(['alpha', 'beta']);\n  });\n\n  it('returns empty array for undefined', () => {\n    expect(normalizeSkillNames(undefined)).toEqual([]);\n  });\n});\n"
  },
  {
    "path": "tests/utils/skills.test.ts",
    "content": "import { describe, it, expect, beforeEach, afterEach, vi } from 'vitest';\nimport { mkdirSync, writeFileSync, symlinkSync, rmSync, existsSync } from 'fs';\nimport { join } from 'path';\nimport { tmpdir } from 'os';\nimport { findAllSkills, findSkill } from '../../src/utils/skills.js';\nimport * as dirsModule from '../../src/utils/dirs.js';\n\n// Create unique temp directories for each test run\nconst testId = Math.random().toString(36).slice(2);\nconst testTempDir = join(tmpdir(), `openskills-test-${testId}`);\nconst testProjectSkillsDir = join(testTempDir, 'project', '.claude', 'skills');\nconst testGlobalSkillsDir = join(testTempDir, 'global', '.claude', 'skills');\nconst testSymlinkTargetDir = join(testTempDir, 'symlink-targets');\n\n// Helper to create a skill directory with SKILL.md\nfunction createSkill(baseDir: string, skillName: string, description: string = 'Test skill'): void {\n  const skillDir = join(baseDir, skillName);\n  mkdirSync(skillDir, { recursive: true });\n  writeFileSync(\n    join(skillDir, 'SKILL.md'),\n    `---\nname: ${skillName}\ndescription: ${description}\n---\n\n# ${skillName}\n\nThis is a test skill.`\n  );\n}\n\n// Helper to create a symlinked skill\nfunction createSymlinkedSkill(\n  skillsDir: string,\n  skillName: string,\n  description: string = 'Symlinked test skill'\n): void {\n  // Create the actual skill in the symlink target directory\n  const actualSkillDir = join(testSymlinkTargetDir, skillName);\n  mkdirSync(actualSkillDir, { recursive: true });\n  writeFileSync(\n    join(actualSkillDir, 'SKILL.md'),\n    `---\nname: ${skillName}\ndescription: ${description}\n---\n\n# ${skillName}\n\nThis is a symlinked test skill.`\n  );\n\n  // Create symlink in the skills directory\n  mkdirSync(skillsDir, { recursive: true });\n  symlinkSync(actualSkillDir, join(skillsDir, skillName), 'dir');\n}\n\n// Helper to create a broken symlink\nfunction createBrokenSymlink(skillsDir: string, skillName: string): void {\n  mkdirSync(skillsDir, { recursive: true });\n  const nonExistentTarget = join(testTempDir, 'non-existent', skillName);\n  symlinkSync(nonExistentTarget, join(skillsDir, skillName), 'dir');\n}\n\ndescribe('skills.ts', () => {\n  beforeEach(() => {\n    // Create test directories\n    mkdirSync(testProjectSkillsDir, { recursive: true });\n    mkdirSync(testGlobalSkillsDir, { recursive: true });\n    mkdirSync(testSymlinkTargetDir, { recursive: true });\n\n    // Mock getSearchDirs to return our test directories\n    vi.spyOn(dirsModule, 'getSearchDirs').mockReturnValue([\n      testProjectSkillsDir,\n      testGlobalSkillsDir,\n    ]);\n  });\n\n  afterEach(() => {\n    // Cleanup test directories\n    rmSync(testTempDir, { recursive: true, force: true });\n    vi.restoreAllMocks();\n  });\n\n  describe('findAllSkills', () => {\n    it('should find regular directory skills', () => {\n      createSkill(testProjectSkillsDir, 'regular-skill', 'A regular skill');\n\n      const skills = findAllSkills();\n\n      expect(skills).toHaveLength(1);\n      expect(skills[0].name).toBe('regular-skill');\n      expect(skills[0].description).toBe('A regular skill');\n    });\n\n    it('should find symlinked skill directories', () => {\n      createSymlinkedSkill(testGlobalSkillsDir, 'symlinked-skill', 'A symlinked skill');\n\n      const skills = findAllSkills();\n\n      expect(skills).toHaveLength(1);\n      expect(skills[0].name).toBe('symlinked-skill');\n      expect(skills[0].description).toBe('A symlinked skill');\n    });\n\n    it('should find both regular and symlinked skills', () => {\n      createSkill(testProjectSkillsDir, 'regular-skill', 'Regular');\n      createSymlinkedSkill(testGlobalSkillsDir, 'symlinked-skill', 'Symlinked');\n\n      const skills = findAllSkills();\n\n      expect(skills).toHaveLength(2);\n      const names = skills.map(s => s.name);\n      expect(names).toContain('regular-skill');\n      expect(names).toContain('symlinked-skill');\n    });\n\n    it('should skip broken symlinks gracefully', () => {\n      createSkill(testProjectSkillsDir, 'good-skill', 'Good skill');\n      createBrokenSymlink(testGlobalSkillsDir, 'broken-symlink');\n\n      const skills = findAllSkills();\n\n      expect(skills).toHaveLength(1);\n      expect(skills[0].name).toBe('good-skill');\n    });\n\n    it('should deduplicate skills with same name (project takes priority)', () => {\n      createSkill(testProjectSkillsDir, 'duplicate-skill', 'Project version');\n      createSkill(testGlobalSkillsDir, 'duplicate-skill', 'Global version');\n\n      const skills = findAllSkills();\n\n      expect(skills).toHaveLength(1);\n      expect(skills[0].name).toBe('duplicate-skill');\n      expect(skills[0].description).toBe('Project version');\n    });\n\n    it('should skip directories without SKILL.md', () => {\n      const noSkillDir = join(testProjectSkillsDir, 'not-a-skill');\n      mkdirSync(noSkillDir, { recursive: true });\n      writeFileSync(join(noSkillDir, 'README.md'), '# Not a skill');\n\n      const skills = findAllSkills();\n\n      expect(skills).toHaveLength(0);\n    });\n\n    it('should skip files (not directories)', () => {\n      writeFileSync(join(testProjectSkillsDir, 'file.txt'), 'Just a file');\n      createSkill(testProjectSkillsDir, 'actual-skill', 'Real skill');\n\n      const skills = findAllSkills();\n\n      expect(skills).toHaveLength(1);\n      expect(skills[0].name).toBe('actual-skill');\n    });\n\n    it('should handle empty skills directories', () => {\n      const skills = findAllSkills();\n      expect(skills).toHaveLength(0);\n    });\n\n    it('should handle non-existent directories gracefully', () => {\n      vi.spyOn(dirsModule, 'getSearchDirs').mockReturnValue([\n        '/non/existent/path',\n        testProjectSkillsDir,\n      ]);\n      createSkill(testProjectSkillsDir, 'skill', 'Test');\n\n      const skills = findAllSkills();\n\n      expect(skills).toHaveLength(1);\n    });\n  });\n\n  describe('findSkill', () => {\n    it('should find a skill by name', () => {\n      createSkill(testProjectSkillsDir, 'my-skill', 'My skill description');\n\n      const skill = findSkill('my-skill');\n\n      expect(skill).not.toBeNull();\n      expect(skill?.path).toContain('my-skill/SKILL.md');\n      expect(skill?.baseDir).toContain('my-skill');\n    });\n\n    it('should find symlinked skills by name', () => {\n      createSymlinkedSkill(testGlobalSkillsDir, 'linked-skill', 'Linked description');\n\n      const skill = findSkill('linked-skill');\n\n      expect(skill).not.toBeNull();\n      expect(skill?.path).toContain('linked-skill/SKILL.md');\n    });\n\n    it('should return null for non-existent skill', () => {\n      const skill = findSkill('non-existent');\n      expect(skill).toBeNull();\n    });\n\n    it('should return project skill over global with same name', () => {\n      createSkill(testProjectSkillsDir, 'shared-skill', 'Project');\n      createSkill(testGlobalSkillsDir, 'shared-skill', 'Global');\n\n      const skill = findSkill('shared-skill');\n\n      expect(skill).not.toBeNull();\n      expect(skill?.source).toBe(testProjectSkillsDir);\n    });\n  });\n});\n"
  },
  {
    "path": "tests/utils/yaml.test.ts",
    "content": "import { describe, it, expect } from 'vitest';\nimport { extractYamlField, hasValidFrontmatter } from '../../src/utils/yaml.js';\n\ndescribe('extractYamlField', () => {\n  it('should extract field from YAML frontmatter', () => {\n    const content = `---\nname: test-skill\ndescription: Test description\n---\n\nContent`;\n\n    expect(extractYamlField(content, 'name')).toBe('test-skill');\n    expect(extractYamlField(content, 'description')).toBe('Test description');\n  });\n\n  it('should return empty string if field not found', () => {\n    const content = `---\nname: test-skill\n---`;\n\n    expect(extractYamlField(content, 'missing')).toBe('');\n  });\n\n  it('should handle multiline descriptions', () => {\n    const content = `---\nname: test\ndescription: First line\n---`;\n\n    expect(extractYamlField(content, 'description')).toBe('First line');\n  });\n\n  // Security tests for non-greedy regex\n  it('should use non-greedy matching (security)', () => {\n    // With greedy regex (.+), this could match across lines incorrectly\n    const content = `---\nname: skill-name\ndescription: Short desc\nother: value\n---`;\n\n    // Non-greedy should stop at end of line\n    const name = extractYamlField(content, 'name');\n    expect(name).toBe('skill-name');\n    expect(name).not.toContain('description');\n  });\n\n  it('should not be vulnerable to ReDoS patterns', () => {\n    // Test that extraction completes quickly even with potentially problematic input\n    const start = Date.now();\n    const content = `---\nname: ${'a'.repeat(1000)}\n---`;\n\n    extractYamlField(content, 'name');\n    const elapsed = Date.now() - start;\n\n    // Should complete in under 100ms even with long input\n    expect(elapsed).toBeLessThan(100);\n  });\n\n  it('should handle special characters in values', () => {\n    const content = `---\nname: skill-with-special_chars.v2\ndescription: Contains \"quotes\" and 'apostrophes'\n---`;\n\n    expect(extractYamlField(content, 'name')).toBe('skill-with-special_chars.v2');\n  });\n\n  it('should handle colons in values', () => {\n    const content = `---\nname: my-skill\ndescription: URL: https://example.com\n---`;\n\n    // Should capture the full value including the colon\n    const desc = extractYamlField(content, 'description');\n    expect(desc).toContain('URL:');\n  });\n});\n\ndescribe('hasValidFrontmatter', () => {\n  it('should return true for valid frontmatter', () => {\n    const content = `---\nname: test\n---\n\nContent`;\n\n    expect(hasValidFrontmatter(content)).toBe(true);\n  });\n\n  it('should return false for missing frontmatter', () => {\n    const content = 'No frontmatter here';\n    expect(hasValidFrontmatter(content)).toBe(false);\n  });\n\n  it('should return false for empty content', () => {\n    expect(hasValidFrontmatter('')).toBe(false);\n  });\n});\n"
  },
  {
    "path": "tsconfig.json",
    "content": "{\n  \"compilerOptions\": {\n    \"target\": \"ES2022\",\n    \"module\": \"ESNext\",\n    \"lib\": [\"ES2022\"],\n    \"moduleResolution\": \"node\",\n    \"resolveJsonModule\": true,\n    \"allowSyntheticDefaultImports\": true,\n    \"esModuleInterop\": true,\n    \"strict\": true,\n    \"skipLibCheck\": true,\n    \"forceConsistentCasingInFileNames\": true,\n    \"declaration\": true,\n    \"declarationMap\": true,\n    \"sourceMap\": true,\n    \"outDir\": \"./dist\",\n    \"rootDir\": \"./src\"\n  },\n  \"include\": [\"src/**/*\"],\n  \"exclude\": [\"node_modules\", \"dist\", \"tests\"]\n}\n"
  },
  {
    "path": "tsup.config.ts",
    "content": "import { defineConfig } from 'tsup';\n\nexport default defineConfig({\n  entry: {\n    cli: 'src/cli.ts',\n  },\n  format: ['esm'],\n  target: 'node18',\n  outDir: 'dist',\n  clean: true,\n  sourcemap: false,\n  dts: false,\n  splitting: false,\n  treeshake: false,\n  platform: 'node',\n  skipNodeModulesBundle: true, // Don't bundle node_modules\n});\n"
  },
  {
    "path": "vitest.config.ts",
    "content": "import { defineConfig } from 'vitest/config';\n\nexport default defineConfig({\n  test: {\n    globals: true,\n    environment: 'node',\n    coverage: {\n      provider: 'v8',\n      reporter: ['text', 'json', 'html'],\n      exclude: [\n        'node_modules/',\n        'dist/',\n        'tests/',\n        '**/*.test.ts',\n        '**/*.config.ts',\n      ],\n    },\n  },\n});\n"
  }
]