commit 5ba5cd1aa735d05a3380f9243f31f3fc3aebbc23 Author: zlei9 Date: Sun Mar 29 09:37:44 2026 +0800 Initial commit with translated description diff --git a/README.md b/README.md new file mode 100644 index 0000000..04155a8 --- /dev/null +++ b/README.md @@ -0,0 +1,152 @@ +# OpenClaw GitHub Skill + +A skill that lets your AI assistant query and manage GitHub repositories. + +## Features + +- ๐Ÿ“‹ **List Repos** โ€” View your repositories with filters +- ๐Ÿ“Š **Get Repo Details** โ€” Stars, forks, language, last updated +- ๐Ÿ”„ **Check CI Status** โ€” Monitor CI/CD pipelines +- ๐Ÿ“ **Create Issues** โ€” Open issues from conversation +- ๐Ÿ“ **Create Repos** โ€” Create new repositories +- ๐Ÿ” **Search Repos** โ€” Find repos by name/query +- ๐Ÿ“Š **Recent Activity** โ€” View recent commits + +## Prerequisites + +- OpenClaw gateway running +- Node.js 18+ +- GitHub account with a Personal Access Token (PAT) + +## Setup + +### 1. Generate a GitHub Personal Access Token + +1. Go to https://github.com/settings/tokens +2. Click "Generate new token (classic)" +3. Name: `openclaw-github-skill` +4. Scopes (permissions): + - `repo` โ€” Full control of private repositories + - `public_repo` โ€” Limited access to public repositories only + - `read:user` โ€” Read user profile data (optional) +5. Copy the token + +### 2. Configure Credentials + +**Option A: Environment Variables (Recommended for local use)** + +Set before starting OpenClaw: + +```bash +export GITHUB_TOKEN="ghp_your_token_here" +export GITHUB_USERNAME="your_github_username" +``` + +**Option B: OpenClaw Config** + +Add to `~/.openclaw/openclaw.json`: + +```json +{ + "github": { + "token": "ghp_your_token_here", + "username": "your_username" + } +} +``` + +### 3. Restart OpenClaw + +```bash +openclaw gateway restart +``` + +## Usage + +``` +You: List my Python repositories +Bot: [lists your Python repositories] + +You: Check CI status on my-project +Bot: [shows CI/CD status] + +You: Create an issue in my-project about the login bug +Bot: [creates the issue and returns the link] + +You: What's the recent activity on my-project? +Bot: [shows recent commits] + +You: Search my repos for "trading" +Bot: [shows matching repositories] +``` + +## Directory Structure + +``` +openclaw-github-skill/ +โ”œโ”€โ”€ SKILL.md # Skill documentation for OpenClaw +โ”œโ”€โ”€ README.md # This file +โ”œโ”€โ”€ index.js # Skill implementation +โ””โ”€โ”€ package.json # NPM package metadata +``` + +## Commands Reference + +| Command | Description | +|---------|-------------| +| `list_repos` | List repositories (filter by type, language, sort) | +| `get_repo` | Get detailed repo info (stars, forks, etc.) | +| `check_ci_status` | CI/CD status | +| `create_issue` | Create a new issue | +| `create_repo` | Create a new repository | +| `search_repos` | Search your repositories | +| `get_recent_activity` | View recent commits | + +## Security + +โš ๏ธ **IMPORTANT: Protect Your GitHub Token!** + +**Do:** +- โœ… Use environment variables or OpenClaw config +- โœ… Use minimal required scopes (`repo` or `public_repo`) +- โœ… Rotate tokens if compromised + +**Don't:** +- โŒ Commit tokens to git +- โŒ Share tokens in code or public repos +- โŒ Store tokens in unprotected files + +**Best Practices:** +- For local development: Environment variables are acceptable +- For shared machines: Use OpenClaw config or a secrets manager +- For production: Use your platform's credential store + +## Rate Limits + +- **Unauthenticated requests:** 60/hour +- **Authenticated requests:** 5,000/hour + +The skill automatically uses your credentials for authentication. + +## Requirements + +- OpenClaw 2024+ +- Node.js 18+ +- GitHub Personal Access Token with appropriate scopes + +## Contributing + +Contributions welcome! To contribute: + +1. Fork this repository +2. Create a feature branch +3. Make your changes +4. Submit a pull request + +## License + +MIT License + +## Acknowledgments + +Built for the OpenClaw ecosystem. diff --git a/SKILL.md b/SKILL.md new file mode 100644 index 0000000..29130fe --- /dev/null +++ b/SKILL.md @@ -0,0 +1,108 @@ +--- +name: github +description: "ๆŸฅ่ฏขๅ’Œ็ฎก็†GitHubไป“ๅบ“โ€”โ€”ๅˆ—ๅ‡บไป“ๅบ“ใ€ๆฃ€ๆŸฅCI็Šถๆ€ใ€ๅˆ›ๅปบ้—ฎ้ข˜ใ€ๆœ็ดขไป“ๅบ“ๅ’ŒๆŸฅ็œ‹ๆœ€่ฟ‘ๆดปๅŠจใ€‚" +metadata: + openclaw: + emoji: "๐Ÿ™" + requires: + env: + - GITHUB_TOKEN + - GITHUB_USERNAME + config: + - github.token + - github.username +--- + +# GitHub Integration Skill + +Query and manage GitHub repositories directly from your AI assistant. + +## Capabilities + +| Capability | Description | +|------------|-------------| +| `list_repos` | List your repositories with filters | +| `get_repo` | Get detailed info about a specific repo | +| `check_ci_status` | Check CI/CD pipeline status | +| `create_issue` | Create a new issue in a repo | +| `create_repo` | Create a new repository | +| `search_repos` | Search your repositories | +| `get_recent_activity` | Get recent commits | + +## Usage + +``` +You: List my Python repos +Bot: [lists your Python repositories] + +You: Check CI status on my main project +Bot: [shows CI/CD status] + +You: Create an issue about the bug +Bot: [creates the issue] +``` + +## Setup + +### 1. Generate GitHub Personal Access Token + +1. Go to https://github.com/settings/tokens +2. Click "Generate new token (classic)" +3. Name: `openclaw-github-skill` +4. Scopes: `repo` (required), `read:user` (optional) +5. Copy the token + +### 2. Configure Credentials + +**Option A: Environment Variables (Recommended)** + +Set environment variables before starting OpenClaw: + +```bash +export GITHUB_TOKEN="ghp_your_token_here" +export GITHUB_USERNAME="your_github_username" +``` + +**Option B: OpenClaw Config** + +Add to `~/.openclaw/openclaw.json`: + +```json +{ + "github": { + "token": "ghp_your_token_here", + "username": "your_username" + } +} +``` + +### 3. Restart OpenClaw + +```bash +openclaw gateway restart +``` + +## Security Notes + +โš ๏ธ **Protect Your Token:** + +- Never commit your token to git or share it publicly +- Use the minimal required scopes (`repo` for private repos, `public_repo` for public-only) +- Rotate your token if you suspect it was compromised +- Consider using a secrets manager for production use + +โš ๏ธ **Best Practices:** + +- Don't store tokens in shell profiles (~/.zshrc) on shared machines +- For local development, environment variables are acceptable +- For production, use your platform's secret/credential store + +## Rate Limits + +- Unauthenticated requests: 60/hour +- Authenticated requests: 5,000/hour + +## Requirements + +- OpenClaw gateway running +- GitHub Personal Access Token with appropriate scopes diff --git a/_meta.json b/_meta.json new file mode 100644 index 0000000..f22bf15 --- /dev/null +++ b/_meta.json @@ -0,0 +1,6 @@ +{ + "ownerId": "kn7dysjcrg739jqwtz8k12xapd80yswr", + "slug": "openclaw-github-assistant", + "version": "2.0.1", + "publishedAt": 1770912461686 +} \ No newline at end of file diff --git a/api.d.ts b/api.d.ts new file mode 100644 index 0000000..f2e8791 --- /dev/null +++ b/api.d.ts @@ -0,0 +1,46 @@ +/** + * GitHub API Module + * Core functions for interacting with GitHub API + */ +import { Context, Repository, RepositoryListParams, RepoDetailsParams, CIStatusParams, RecentActivityParams, CreateIssueParams, Issue, CreateRepoParams, SearchReposParams, SearchReposResult, CreatePRParams, PullRequest, ListReposResult, CheckCIResult, RecentActivityResult } from './types'; +/** + * Get auth headers for API requests + */ +export declare function getAuthHeaders(context: Context): Record; +/** + * Get GitHub username + */ +export declare function getUsername(context: Context): Promise; +/** + * List repositories + */ +export declare function listRepos(args: RepositoryListParams, context: Context): Promise; +/** + * Get repository details + */ +export declare function getRepo(args: RepoDetailsParams, context: Context): Promise; +/** + * Check CI/CD status + */ +export declare function checkCIStatus(args: CIStatusParams, context: Context): Promise; +/** + * Get recent activity (commits) + */ +export declare function getRecentActivity(args: RecentActivityParams, context: Context): Promise; +/** + * Create an issue + */ +export declare function createIssue(args: CreateIssueParams, context: Context): Promise; +/** + * Create a new repository + */ +export declare function createRepo(args: CreateRepoParams, context: Context): Promise; +/** + * Search repositories + */ +export declare function searchRepos(args: SearchReposParams, context: Context): Promise; +/** + * Create a pull request + */ +export declare function createPullRequest(args: CreatePRParams, context: Context): Promise; +//# sourceMappingURL=api.d.ts.map \ No newline at end of file diff --git a/api.js b/api.js new file mode 100644 index 0000000..fe3f68e --- /dev/null +++ b/api.js @@ -0,0 +1,303 @@ +"use strict"; +/** + * GitHub API Module + * Core functions for interacting with GitHub API + */ +Object.defineProperty(exports, "__esModule", { value: true }); +exports.getAuthHeaders = getAuthHeaders; +exports.getUsername = getUsername; +exports.listRepos = listRepos; +exports.getRepo = getRepo; +exports.checkCIStatus = checkCIStatus; +exports.getRecentActivity = getRecentActivity; +exports.createIssue = createIssue; +exports.createRepo = createRepo; +exports.searchRepos = searchRepos; +exports.createPullRequest = createPullRequest; +const GITHUB_API = 'https://api.github.com'; +// Cached user info +let cachedUser = null; +/** + * Get auth headers for API requests + */ +function getAuthHeaders(context) { + const headers = { + 'Accept': 'application/vnd.github.v3+json', + 'User-Agent': 'OpenClaw-GitHub-Skill' + }; + // 1. Check environment variable first + if (process.env.GITHUB_TOKEN) { + headers['Authorization'] = `token ${process.env.GITHUB_TOKEN}`; + return headers; + } + // 2. Fall back to OpenClaw config + const config = context.config?.github || {}; + if (config.token) { + headers['Authorization'] = `token ${config.token}`; + } + return headers; +} +/** + * Get GitHub username + */ +async function getUsername(context) { + // 1. Check environment variable first + if (process.env.GITHUB_USERNAME) { + return process.env.GITHUB_USERNAME; + } + // 2. Check OpenClaw config + const config = context.config?.github || {}; + if (config.username) { + return config.username; + } + // 3. Fetch from API if not configured + if (!cachedUser) { + const response = await fetch(`${GITHUB_API}/user`, { + headers: getAuthHeaders(context) + }); + const data = await response.json(); + cachedUser = data.login; + } + return cachedUser; +} +/** + * List repositories + */ +async function listRepos(args, context) { + const username = await getUsername(context); + const { type = 'owner', sort = 'updated', direction = 'desc', limit = 30 } = args; + const url = `${GITHUB_API}/users/${username}/repos?type=${type}&sort=${sort}&direction=${direction}&per_page=${limit}`; + const response = await fetch(url, { headers: getAuthHeaders(context) }); + if (!response.ok) { + throw new Error(`GitHub API error: ${response.status}`); + } + const repos = await response.json(); + // Filter by language if specified + let filtered = repos; + if (args.language) { + filtered = repos.filter(r => r.language?.toLowerCase() === args.language.toLowerCase()); + } + // Limit results + filtered = filtered.slice(0, limit); + return { + total: filtered.length, + repos: filtered + }; +} +/** + * Get repository details + */ +async function getRepo(args, context) { + const { owner, repo } = args; + const url = `${GITHUB_API}/repos/${owner}/${repo}`; + const response = await fetch(url, { headers: getAuthHeaders(context) }); + if (!response.ok) { + throw new Error(`Repo not found: ${owner}/${repo}`); + } + const data = await response.json(); + return { + name: data.name, + full_name: data.full_name, + description: data.description, + stars: data.stargazers_count, + forks: data.forks_count, + watchers: data.watchers_count, + language: data.language, + open_issues: data.open_issues_count, + created: data.created_at, + updated: data.updated_at, + pushed: data.pushed_at, + url: data.html_url, + default_branch: data.default_branch, + private: data.private + }; +} +/** + * Check CI/CD status + */ +async function checkCIStatus(args, context) { + const { owner, repo } = args; + // Get recent workflows/runs + const runsUrl = `${GITHUB_API}/repos/${owner}/${repo}/actions/runs?per_page=5`; + const response = await fetch(runsUrl, { headers: getAuthHeaders(context) }); + if (!response.ok) { + throw new Error(`Failed to get CI status: ${response.status}`); + } + const data = await response.json(); + const runs = (data.workflow_runs || []).map((run) => ({ + name: run.name, + status: run.status, + conclusion: run.conclusion, + branch: run.head_branch, + commit: run.head_sha?.substring(0, 7) || '', + created: run.created_at, + url: run.html_url + })); + return { + repo: `${owner}/${repo}`, + runs + }; +} +/** + * Get recent activity (commits) + */ +async function getRecentActivity(args, context) { + const username = await getUsername(context); + const { repo, limit = 10 } = args; + if (!repo) { + throw new Error('Repository name required'); + } + // Get recent commits + const commitsUrl = `${GITHUB_API}/repos/${username}/${repo}/commits?per_page=${limit}`; + const commitsRes = await fetch(commitsUrl, { headers: getAuthHeaders(context) }); + if (!commitsRes.ok) { + throw new Error(`Failed to get activity: ${commitsRes.status}`); + } + const commits = await commitsRes.json(); + return { + repo: `${username}/${repo}`, + commits: commits.map((c) => ({ + sha: c.sha.substring(0, 7), + message: c.commit.message.split('\n')[0], + author: c.commit.author.name, + date: c.commit.author.date, + url: c.html_url + })) + }; +} +/** + * Create an issue + */ +async function createIssue(args, context) { + const username = await getUsername(context); + const { repo, title, body } = args; + if (!title) { + throw new Error('Issue title required'); + } + const url = `${GITHUB_API}/repos/${username}/${repo}/issues`; + const response = await fetch(url, { + method: 'POST', + headers: getAuthHeaders(context), + body: JSON.stringify({ + title, + body: body || '', + ...(args.extra || {}) + }) + }); + if (!response.ok) { + const error = await response.json(); + throw new Error(`Failed to create issue: ${error.message || response.status}`); + } + const issue = await response.json(); + return { + number: issue.number, + title: issue.title, + url: issue.html_url, + state: issue.state + }; +} +/** + * Create a new repository + */ +async function createRepo(args, context) { + const username = await getUsername(context); + const { name, description, private: isPrivate = false, auto_init = true } = args; + if (!name) { + throw new Error('Repository name required'); + } + const url = `${GITHUB_API}/user/repos`; + const response = await fetch(url, { + method: 'POST', + headers: getAuthHeaders(context), + body: JSON.stringify({ + name, + description: description || '', + private: isPrivate, + auto_init + }) + }); + if (!response.ok) { + const error = await response.json(); + throw new Error(`Failed to create repo: ${error.message || response.status}`); + } + const repo = await response.json(); + return { + name: repo.name, + full_name: repo.full_name, + description: repo.description, + stars: repo.stargazers_count || 0, + forks: repo.forks_count || 0, + language: repo.language || null, + updated: repo.updated_at || '', + url: repo.html_url, + private: repo.private + }; +} +/** + * Search repositories + */ +async function searchRepos(args, context) { + const username = await getUsername(context); + const { query, sort = 'updated', limit = 30 } = args; + if (!query) { + throw new Error('Search query required'); + } + // Search user's repos + const url = `${GITHUB_API}/search/repositories?q=${encodeURIComponent(query)}+user:${username}&sort=${sort}&per_page=${limit}`; + const response = await fetch(url, { headers: getAuthHeaders(context) }); + if (!response.ok) { + throw new Error(`Search failed: ${response.status}`); + } + const data = await response.json(); + return { + total: data.total_count, + repos: (data.items || []).map((r) => ({ + name: r.name, + full_name: r.full_name, + description: r.description, + stars: r.stargazers_count, + language: r.language, + url: r.html_url, + forks: 0, + updated: '', + private: false + })) + }; +} +/** + * Create a pull request + */ +async function createPullRequest(args, context) { + const username = await getUsername(context); + const { owner, repo, title, body, head, base = 'main' } = args; + // Use username if owner not specified + const prOwner = owner || username; + if (!prOwner || !repo || !title || !head) { + throw new Error('owner, repo, title, and head are required'); + } + const url = `${GITHUB_API}/repos/${prOwner}/${repo}/pulls`; + const response = await fetch(url, { + method: 'POST', + headers: getAuthHeaders(context), + body: JSON.stringify({ + title, + body: body || '', + head, + base + }) + }); + if (!response.ok) { + const error = await response.json(); + throw new Error(`Failed to create PR: ${error.message || response.status}`); + } + const pr = await response.json(); + return { + number: pr.number, + title: pr.title, + url: pr.html_url, + state: pr.state, + head: pr.head.ref, + base: pr.base.ref + }; +} +//# sourceMappingURL=api.js.map \ No newline at end of file diff --git a/index.d.ts b/index.d.ts new file mode 100644 index 0000000..4119643 --- /dev/null +++ b/index.d.ts @@ -0,0 +1,209 @@ +/** + * OpenClaw GitHub Skill + * Query and manage GitHub repositories from conversation + * + * TypeScript version - Compiled to JavaScript for OpenClaw + */ +import { Context, RepositoryListParams, RepoDetailsParams, CIStatusParams, RecentActivityParams, CreateIssueParams, CreateRepoParams, SearchReposParams, CreatePRParams, ListReposResult, Repository, CheckCIResult, RecentActivityResult, Issue, SearchReposResult, PullRequest } from './types'; +export declare const skillName = "github"; +export declare const skillVersion = "2.0.1"; +export declare const skillDescription = "Query and manage GitHub repositories"; +declare function listReposHandler(args: RepositoryListParams, context: Context): Promise; +declare function getRepoHandler(args: RepoDetailsParams, context: Context): Promise; +declare function checkCIStatusHandler(args: CIStatusParams, context: Context): Promise; +declare function getRecentActivityHandler(args: RecentActivityParams, context: Context): Promise; +declare function createIssueHandler(args: CreateIssueParams, context: Context): Promise; +declare function createRepoHandler(args: CreateRepoParams, context: Context): Promise; +declare function searchReposHandler(args: SearchReposParams, context: Context): Promise; +declare function createPullRequestHandler(args: CreatePRParams, context: Context): Promise; +declare const skill: { + name: string; + version: string; + description: string; + actions: { + list_repos: { + description: string; + parameters: { + type: string; + properties: { + type: { + type: string; + enum: string[]; + default: string; + }; + sort: { + type: string; + enum: string[]; + default: string; + }; + language: { + type: string; + }; + limit: { + type: string; + default: number; + }; + }; + }; + handler: typeof listReposHandler; + }; + get_repo: { + description: string; + parameters: { + type: string; + properties: { + owner: { + type: string; + }; + repo: { + type: string; + }; + }; + required: string[]; + }; + handler: typeof getRepoHandler; + }; + check_ci_status: { + description: string; + parameters: { + type: string; + properties: { + owner: { + type: string; + }; + repo: { + type: string; + }; + }; + required: string[]; + }; + handler: typeof checkCIStatusHandler; + }; + get_recent_activity: { + description: string; + parameters: { + type: string; + properties: { + repo: { + type: string; + }; + limit: { + type: string; + default: number; + }; + }; + required: string[]; + }; + handler: typeof getRecentActivityHandler; + }; + create_issue: { + description: string; + parameters: { + type: string; + properties: { + repo: { + type: string; + }; + title: { + type: string; + }; + body: { + type: string; + }; + extra: { + type: string; + }; + }; + required: string[]; + }; + handler: typeof createIssueHandler; + }; + create_repo: { + description: string; + parameters: { + type: string; + properties: { + name: { + type: string; + description: string; + }; + description: { + type: string; + description: string; + }; + private: { + type: string; + description: string; + default: boolean; + }; + auto_init: { + type: string; + description: string; + default: boolean; + }; + }; + required: string[]; + }; + handler: typeof createRepoHandler; + }; + create_pull_request: { + description: string; + parameters: { + type: string; + properties: { + owner: { + type: string; + description: string; + }; + repo: { + type: string; + description: string; + }; + title: { + type: string; + description: string; + }; + body: { + type: string; + description: string; + }; + head: { + type: string; + description: string; + }; + base: { + type: string; + description: string; + default: string; + }; + }; + required: string[]; + }; + handler: typeof createPullRequestHandler; + }; + search_repos: { + description: string; + parameters: { + type: string; + properties: { + query: { + type: string; + }; + sort: { + type: string; + enum: string[]; + default: string; + }; + limit: { + type: string; + default: number; + }; + }; + required: string[]; + }; + handler: typeof searchReposHandler; + }; + }; +}; +export default skill; +//# sourceMappingURL=index.d.ts.map \ No newline at end of file diff --git a/index.js b/index.js new file mode 100644 index 0000000..44492b8 --- /dev/null +++ b/index.js @@ -0,0 +1,157 @@ +"use strict"; +/** + * OpenClaw GitHub Skill + * Query and manage GitHub repositories from conversation + * + * TypeScript version - Compiled to JavaScript for OpenClaw + */ +Object.defineProperty(exports, "__esModule", { value: true }); +exports.skillDescription = exports.skillVersion = exports.skillName = void 0; +const api_1 = require("./api"); +// Skill metadata +exports.skillName = 'github'; +exports.skillVersion = '2.0.1'; +exports.skillDescription = 'Query and manage GitHub repositories'; +// Handler functions with proper signatures +async function listReposHandler(args, context) { + return (0, api_1.listRepos)(args, context); +} +async function getRepoHandler(args, context) { + return (0, api_1.getRepo)(args, context); +} +async function checkCIStatusHandler(args, context) { + return (0, api_1.checkCIStatus)(args, context); +} +async function getRecentActivityHandler(args, context) { + return (0, api_1.getRecentActivity)(args, context); +} +async function createIssueHandler(args, context) { + return (0, api_1.createIssue)(args, context); +} +async function createRepoHandler(args, context) { + return (0, api_1.createRepo)(args, context); +} +async function searchReposHandler(args, context) { + return (0, api_1.searchRepos)(args, context); +} +async function createPullRequestHandler(args, context) { + return (0, api_1.createPullRequest)(args, context); +} +// Skill definition for OpenClaw +const skill = { + name: exports.skillName, + version: exports.skillVersion, + description: exports.skillDescription, + actions: { + list_repos: { + description: 'List your repositories', + parameters: { + type: 'object', + properties: { + type: { type: 'string', enum: ['owner', 'all', 'member'], default: 'owner' }, + sort: { type: 'string', enum: ['created', 'updated', 'pushed', 'full_name'], default: 'updated' }, + language: { type: 'string' }, + limit: { type: 'number', default: 30 } + } + }, + handler: listReposHandler + }, + get_repo: { + description: 'Get repository details', + parameters: { + type: 'object', + properties: { + owner: { type: 'string' }, + repo: { type: 'string' } + }, + required: ['owner', 'repo'] + }, + handler: getRepoHandler + }, + check_ci_status: { + description: 'Check CI/CD pipeline status', + parameters: { + type: 'object', + properties: { + owner: { type: 'string' }, + repo: { type: 'string' } + }, + required: ['owner', 'repo'] + }, + handler: checkCIStatusHandler + }, + get_recent_activity: { + description: 'Get recent commits', + parameters: { + type: 'object', + properties: { + repo: { type: 'string' }, + limit: { type: 'number', default: 10 } + }, + required: ['repo'] + }, + handler: getRecentActivityHandler + }, + create_issue: { + description: 'Create a new issue', + parameters: { + type: 'object', + properties: { + repo: { type: 'string' }, + title: { type: 'string' }, + body: { type: 'string' }, + extra: { type: 'object' } + }, + required: ['repo', 'title'] + }, + handler: createIssueHandler + }, + create_repo: { + description: 'Create a new repository', + parameters: { + type: 'object', + properties: { + name: { type: 'string', description: 'Repository name' }, + description: { type: 'string', description: 'Repository description' }, + private: { type: 'boolean', description: 'Private repository', default: false }, + auto_init: { type: 'boolean', description: 'Initialize with README', default: true } + }, + required: ['name'] + }, + handler: createRepoHandler + }, + create_pull_request: { + description: 'Create a pull request', + parameters: { + type: 'object', + properties: { + owner: { type: 'string', description: 'Repository owner' }, + repo: { type: 'string', description: 'Repository name' }, + title: { type: 'string', description: 'PR title' }, + body: { type: 'string', description: 'PR description' }, + head: { type: 'string', description: 'Source branch' }, + base: { type: 'string', description: 'Target branch', default: 'main' } + }, + required: ['owner', 'repo', 'title', 'head'] + }, + handler: createPullRequestHandler + }, + search_repos: { + description: 'Search your repositories', + parameters: { + type: 'object', + properties: { + query: { type: 'string' }, + sort: { type: 'string', enum: ['stars', 'updated', 'created'], default: 'updated' }, + limit: { type: 'number', default: 30 } + }, + required: ['query'] + }, + handler: searchReposHandler + } + } +}; +// Export for both CommonJS and ES modules +exports.default = skill; +module.exports = skill; +//# sourceMappingURL=index.js.map \ No newline at end of file diff --git a/package.json b/package.json new file mode 100644 index 0000000..cca5a7b --- /dev/null +++ b/package.json @@ -0,0 +1,32 @@ +{ + "name": "openclaw-github-skill", + "version": "2.0.0", + "description": "GitHub integration skill for OpenClaw โ€” query repos, check CI status, create issues", + "main": "index.js", + "scripts": { + "test": "node test.js" + }, + "keywords": [ + "openclaw", + "skill", + "github", + "git", + "automation" + ], + "repository": { + "type": "git", + "url": "https://github.com/conorkennedy/openclaw-github-skill" + }, + "scripts": { + "test": "node index.js" + }, + "keywords": [ + "openclaw", + "skill", + "github", + "git", + "automation" + ], + "author": "Conor Kennedy", + "license": "MIT" +} diff --git a/test.js b/test.js new file mode 100644 index 0000000..e32874d --- /dev/null +++ b/test.js @@ -0,0 +1,156 @@ +// OpenClaw GitHub Skill - Test Suite +// Run with: node test.js + +const { execSync } = require('child_process'); + +// Test configuration +const TEST_REPO = 'conorkenn/openclaw-github-skill'; +const TEST_OWNER = 'conorkenn'; + +// Helper to run GitHub API calls +async function githubAPI(endpoint, options = {}) { + const token = process.env.GITHUB_TOKEN; + const response = await fetch(`https://api.github.com${endpoint}`, { + ...options, + headers: { + 'Authorization': `token ${token}`, + 'Accept': 'application/vnd.github.v3+json', + 'User-Agent': 'OpenClaw-GitHub-Skill-Test', + ...options.headers + } + }); + return response; +} + +// Test functions +const tests = [ + { + name: 'Environment Variables Set', + test: () => { + if (!process.env.GITHUB_TOKEN) throw new Error('GITHUB_TOKEN not set'); + if (!process.env.GITHUB_USERNAME) throw new Error('GITHUB_USERNAME not set'); + return true; + } + }, + { + name: 'GitHub API Authentication', + test: async () => { + const response = await githubAPI('/user'); + if (!response.ok) throw new Error(`Auth failed: ${response.status}`); + const data = await response.json(); + if (data.login !== process.env.GITHUB_USERNAME) { + throw new Error(`Username mismatch: expected ${process.env.GITHUB_USERNAME}, got ${data.login}`); + } + return true; + } + }, + { + name: 'List Repositories', + test: async () => { + const response = await githubAPI(`/users/${TEST_OWNER}/repos?per_page=5`); + if (!response.ok) throw new Error(`Failed to list repos: ${response.status}`); + const repos = await response.json(); + if (!Array.isArray(repos) || repos.length === 0) { + throw new Error('No repositories found'); + } + console.log(` Found ${repos.length} repos`); + return true; + } + }, + { + name: 'Get Repository', + test: async () => { + const response = await githubAPI(`/repos/${TEST_REPO}`); + if (!response.ok) throw new Error(`Repo not found: ${response.status}`); + const repo = await response.json(); + if (repo.full_name !== TEST_REPO) { + throw new Error(`Repo mismatch: expected ${TEST_REPO}, got ${repo.full_name}`); + } + console.log(` Repo: ${repo.full_name} (โญ ${repo.stargazers_count})`); + return true; + } + }, + { + name: 'Check CI Status', + test: async () => { + const response = await githubAPI(`/repos/${TEST_REPO}/actions/runs?per_page=1`); + if (!response.ok) throw new Error(`Failed to get CI status: ${response.status}`); + const data = await response.json(); + console.log(` Latest run: ${data.workflow_runs?.[0]?.name || 'none'}`); + return true; + } + }, + { + name: 'Search Repositories', + test: async () => { + const response = await githubAPI(`/search/repositories?q=user:${TEST_OWNER}+github&per_page=5`); + if (!response.ok) throw new Error(`Search failed: ${response.status}`); + const data = await response.json(); + if (data.total_count === 0) { + throw new Error('No repos found in search'); + } + console.log(` Found ${data.total_count} repos matching search`); + return true; + } + }, + { + name: 'Get Recent Commits', + test: async () => { + const response = await githubAPI(`/repos/${TEST_REPO}/commits?per_page=3`); + if (!response.ok) throw new Error(`Failed to get commits: ${response.status}`); + const commits = await response.json(); + if (!Array.isArray(commits) || commits.length === 0) { + throw new Error('No commits found'); + } + console.log(` Latest commit: ${commits[0].sha.substring(0, 7)} - ${commits[0].commit.message.split('\n')[0]}`); + return true; + } + }, + { + name: 'Create Repository (Dry Run - Skip)', + test: async () => { + console.log(' Skipping actual repo creation (would create test-repo)'); + return true; + } + } +]; + +// Run tests +async function runTests() { + console.log('๐Ÿงช OpenClaw GitHub Skill - Test Suite\n'); + console.log('=' .repeat(50)); + + let passed = 0; + let failed = 0; + + for (const { name, test } of tests) { + process.stdout.write(`\n๐Ÿ” ${name}... `); + + try { + const result = await test(); + if (result) { + console.log('โœ… PASS'); + passed++; + } else { + console.log('โŒ FAIL'); + failed++; + } + } catch (error) { + console.log(`โŒ FAIL: ${error.message}`); + failed++; + } + } + + console.log('\n' + '=' .repeat(50)); + console.log(`\n๐Ÿ“Š Results: ${passed} passed, ${failed} failed\n`); + + if (failed > 0) { + console.log('โŒ Some tests failed. Check your configuration.'); + process.exit(1); + } else { + console.log('โœ… All tests passed! Your GitHub skill is working correctly.'); + process.exit(0); + } +} + +runTests(); diff --git a/types.d.ts b/types.d.ts new file mode 100644 index 0000000..08353b7 --- /dev/null +++ b/types.d.ts @@ -0,0 +1,119 @@ +/** + * GitHub Skill - Type Definitions + */ +export interface GitHubConfig { + token?: string; + username?: string; +} +export interface Context { + config?: { + github?: GitHubConfig; + }; +} +export interface Repository { + name: string; + full_name: string; + description: string | null; + stars: number; + forks: number; + watchers?: number; + language: string | null; + open_issues?: number; + updated: string; + created?: string; + pushed?: string; + url: string; + private: boolean; + default_branch?: string; +} +export interface RepositoryListParams { + type?: 'owner' | 'all' | 'member'; + sort?: 'created' | 'updated' | 'pushed' | 'full_name'; + direction?: 'asc' | 'desc'; + language?: string; + limit?: number; +} +export interface RepoDetailsParams { + owner: string; + repo: string; +} +export interface CIStatusParams { + owner: string; + repo: string; +} +export interface WorkflowRun { + name: string; + status: string; + conclusion: string | null; + branch: string; + commit: string; + created: string; + url: string; +} +export interface RecentActivityParams { + repo: string; + limit?: number; +} +export interface Commit { + sha: string; + message: string; + author: string; + date: string; + url: string; +} +export interface CreateIssueParams { + repo: string; + title: string; + body?: string; + extra?: Record; +} +export interface Issue { + number: number; + title: string; + url: string; + state: string; +} +export interface CreateRepoParams { + name: string; + description?: string; + private?: boolean; + auto_init?: boolean; +} +export interface SearchReposParams { + query: string; + sort?: 'stars' | 'updated' | 'created'; + limit?: number; +} +export interface CreatePRParams { + owner: string; + repo: string; + title: string; + body?: string; + head: string; + base?: string; +} +export interface PullRequest { + number: number; + title: string; + url: string; + state: string; + head: string; + base: string; +} +export interface ListReposResult { + total: number; + repos: Repository[]; +} +export interface SearchReposResult { + total: number; + repos: Repository[]; +} +export interface CheckCIResult { + repo: string; + runs: WorkflowRun[]; +} +export interface RecentActivityResult { + repo: string; + commits: Commit[]; +} +//# sourceMappingURL=types.d.ts.map \ No newline at end of file diff --git a/types.js b/types.js new file mode 100644 index 0000000..0202863 --- /dev/null +++ b/types.js @@ -0,0 +1,6 @@ +"use strict"; +/** + * GitHub Skill - Type Definitions + */ +Object.defineProperty(exports, "__esModule", { value: true }); +//# sourceMappingURL=types.js.map \ No newline at end of file