From 5ba5cd1aa735d05a3380f9243f31f3fc3aebbc23 Mon Sep 17 00:00:00 2001 From: zlei9 Date: Sun, 29 Mar 2026 09:37:44 +0800 Subject: [PATCH] Initial commit with translated description --- README.md | 152 ++++++++++++++++++++++++++ SKILL.md | 108 ++++++++++++++++++ _meta.json | 6 + api.d.ts | 46 ++++++++ api.js | 303 +++++++++++++++++++++++++++++++++++++++++++++++++++ index.d.ts | 209 +++++++++++++++++++++++++++++++++++ index.js | 157 ++++++++++++++++++++++++++ package.json | 32 ++++++ test.js | 156 ++++++++++++++++++++++++++ types.d.ts | 119 ++++++++++++++++++++ types.js | 6 + 11 files changed, 1294 insertions(+) create mode 100644 README.md create mode 100644 SKILL.md create mode 100644 _meta.json create mode 100644 api.d.ts create mode 100644 api.js create mode 100644 index.d.ts create mode 100644 index.js create mode 100644 package.json create mode 100644 test.js create mode 100644 types.d.ts create mode 100644 types.js 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