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