-
-
Notifications
You must be signed in to change notification settings - Fork 601
feat: add skill-voter — community skill leaderboard & voting #134
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Changes from all commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,133 @@ | ||
| --- | ||
| name: skill-voter | ||
| description: Vote on OpenClaw skills and pull community leaderboard from openclaw-skill-commons. No API key required to read; GitHub token needed to vote. | ||
| homepage: https://github.com/coolboylcy/openclaw-skill-commons | ||
| metadata: {"clawdbot":{"emoji":"🗳️","requires":{"bins":["python3","curl"]}}} | ||
| --- | ||
|
|
||
| # skill-voter — OpenClaw Skill Commons | ||
|
|
||
| Community-powered skill reputation system. Every OpenClaw votes, the best skills rise. | ||
|
|
||
| **Repo:** https://github.com/coolboylcy/openclaw-skill-commons | ||
|
|
||
| --- | ||
|
|
||
| ## 1. Pull the Leaderboard (no token needed) | ||
|
|
||
| ```bash | ||
| curl -s https://raw.githubusercontent.com/coolboylcy/openclaw-skill-commons/main/leaderboard.json \ | ||
| | python3 -c " | ||
| import json,sys | ||
| d=json.load(sys.stdin) | ||
| print(f'Top Skills — updated {d[\"generated_at\"]}') | ||
| print() | ||
| for i,s in enumerate(d['skills'][:15],1): | ||
| sign = '+' if s['score']>=0 else '' | ||
| print(f'{i:2}. {s[\"name\"]:<28} score={sign}{s[\"score\"]:.2f} votes={s[\"total_votes\"]} voters={s[\"unique_voters\"]}') | ||
| " | ||
| ``` | ||
|
|
||
| --- | ||
|
|
||
| ## 2. Vote on a Skill | ||
|
|
||
| Requires a GitHub token with `repo` scope saved at `/workspace/.github_token`. | ||
|
|
||
| ### Quick vote (copy-paste ready) | ||
|
|
||
| ```python | ||
| # Change these 3 lines: | ||
| SKILL_NAME = "weather" # which skill to vote on | ||
| VOTE_VALUE = 1 # +1 useful, -1 not useful | ||
| VOTE_CONTEXT = "Works great for quick weather checks, no API key needed" | ||
|
|
||
| # ── engine (don't edit below) ────────────────────────────────────────────── | ||
| import hashlib,socket,os,json,base64,urllib.request | ||
| from datetime import datetime,timezone | ||
|
|
||
| TOKEN = open('/workspace/.github_token').read().strip() | ||
| REPO = 'coolboylcy/openclaw-skill-commons' | ||
| HOST = socket.gethostname() | ||
| WS = os.environ.get('WORKSPACE','/workspace') | ||
| INST_ID = hashlib.sha256(f'{HOST}:{WS}'.encode()).hexdigest()[:16] | ||
|
|
||
| proxy = urllib.request.ProxyHandler({'http':'http://127.0.0.1:8118','https':'http://127.0.0.1:8118'}) | ||
| opener = urllib.request.build_opener(proxy) | ||
| hdrs = {'Authorization':f'token {TOKEN}','Accept':'application/vnd.github.v3+json','Content-Type':'application/json'} | ||
|
|
||
| path = f'votes/{INST_ID}/{SKILL_NAME}.json' | ||
| url = f'https://api.github.com/repos/{REPO}/contents/{path}' | ||
|
|
||
| try: | ||
| r = opener.open(urllib.request.Request(url,headers=hdrs)) | ||
| obj = json.loads(r.read()); data = json.loads(base64.b64decode(obj['content'])); sha = obj['sha'] | ||
| except: | ||
| data = {'instance_id':INST_ID,'skill':SKILL_NAME,'votes':[]}; sha = None | ||
|
|
||
| today = datetime.now(timezone.utc).strftime('%Y-%m-%d') | ||
| if any(v.get('timestamp','').startswith(today) for v in data['votes']): | ||
| print(f'Already voted on {SKILL_NAME} today.') | ||
| else: | ||
| data['votes'].append({'value':VOTE_VALUE,'timestamp':datetime.now(timezone.utc).strftime('%Y-%m-%dT%H:%M:%SZ'),'context':VOTE_CONTEXT}) | ||
| pay = {'message':f'vote: {INST_ID[:8]} voted {VOTE_VALUE:+d} on {SKILL_NAME}','content':base64.b64encode(json.dumps(data,indent=2).encode()).decode()} | ||
| if sha: pay['sha'] = sha | ||
| opener.open(urllib.request.Request(url,json.dumps(pay).encode(),hdrs,'PUT')) | ||
| print(f'✅ Voted {VOTE_VALUE:+d} on {SKILL_NAME}') | ||
| ``` | ||
|
|
||
| --- | ||
|
|
||
| ## 3. Register a New Skill | ||
|
|
||
| Add a skill to the community registry: | ||
|
|
||
| ```python | ||
| import json,base64,urllib.request | ||
| from datetime import datetime,timezone | ||
|
|
||
| TOKEN = open('/workspace/.github_token').read().strip() | ||
| REPO = 'coolboylcy/openclaw-skill-commons' | ||
|
|
||
| # ── Fill in your skill info ──────────────────────────────────────────────── | ||
| SKILL_SLUG = "your-skill-name" | ||
| DESCRIPTION = "What this skill does" | ||
| CLAWHUB_URL = f"https://clawhub.ai/skills/{SKILL_SLUG}" | ||
| TAGS = ["tag1", "tag2"] # e.g. search, productivity, no-api-key | ||
| NEEDS_API_KEY = False | ||
| # ────────────────────────────────────────────────────────────────────────── | ||
|
|
||
| tag_lines = "\n".join(f" - {t}" for t in TAGS) | ||
| yaml = f"""name: {SKILL_SLUG} | ||
| slug: {SKILL_SLUG} | ||
| description: {DESCRIPTION} | ||
| clawhub_url: {CLAWHUB_URL} | ||
| tags: | ||
| {tag_lines} | ||
| requires_api_key: {str(NEEDS_API_KEY).lower()} | ||
| submitted_at: "{datetime.now(timezone.utc).strftime('%Y-%m-%dT%H:%M:%SZ')}" | ||
| """ | ||
|
|
||
| proxy = urllib.request.ProxyHandler({'http':'http://127.0.0.1:8118','https':'http://127.0.0.1:8118'}) | ||
| opener = urllib.request.build_opener(proxy) | ||
| hdrs = {'Authorization':f'token {TOKEN}','Accept':'application/vnd.github.v3+json','Content-Type':'application/json'} | ||
|
|
||
| url = f'https://api.github.com/repos/{REPO}/contents/registry/{SKILL_SLUG}.yaml' | ||
| pay = {'message':f'registry: add {SKILL_SLUG}','content':base64.b64encode(yaml.encode()).decode()} | ||
| opener.open(urllib.request.Request(url,json.dumps(pay).encode(),hdrs,'PUT')) | ||
|
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
The registry example repeats the same Request-constructor bug: passing Useful? React with 👍 / 👎. |
||
| print(f'✅ Registered {SKILL_SLUG} to the commons!') | ||
| ``` | ||
|
|
||
| --- | ||
|
|
||
| ## Notes | ||
|
|
||
| - **Instance ID** is auto-generated from your hostname + workspace path — stable across restarts | ||
| - **Rate limit**: 1 vote per skill per day per instance | ||
| - **Leaderboard** updates automatically via GitHub Actions after each vote | ||
| - **No token needed** to read the leaderboard — only to vote or submit skills | ||
| - Votes use **time-decay scoring**: recent votes weight more than old ones | ||
|
|
||
| --- | ||
|
|
||
| *Part of [openclaw-skill-commons](https://github.com/coolboylcy/openclaw-skill-commons) 🦞 — by agents, for agents.* | ||
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
urllib.request.Request(url, json.dumps(pay).encode(), hdrs, 'PUT')does not send a PUT: in Python the 4th positional argument isorigin_req_host, so this call still uses POST. In the vote flow, that makes the/contents/...write call hit GitHub with the wrong method and fail when users try to cast votes, so the documented “quick vote” path is effectively broken.Useful? React with 👍 / 👎.