
Idea Coach
- 26 installs
- 61 repo stars
- Updated March 16, 2026
- kirkluokun/awesome-a-stock-openclawskills
Helps with ai & agent building tasks.
About
idea-coach is a Claude Code skill for ai & agent building. It helps solo builders move faster with AI-assisted development.
- idea-coach
- AI & Agent Building
- AI-coding skill
Idea Coach by the numbers
- 26 all-time installs (skills.sh)
- Ranked #9,702 of 16,546 AI & Agent Building skills by installs in the Skillselion catalog
- Data as of Aug 4, 2026 (Skillselion catalog sync)
npx skills add https://github.com/kirkluokun/awesome-a-stock-openclawskills --skill idea-coachAdd your badge
Show developers this skill is listed on Skillselion. Paste this into your README.
| Installs | 26 |
|---|---|
| repo stars | ★ 61 |
| Last updated | March 16, 2026 |
| Repository | kirkluokun/awesome-a-stock-openclawskills ↗ |
What it does
Helps with ai & agent building tasks.
Files
Idea Coach
Your critical sparring partner for ideas, problems, and challenges — now with GitHub integration!
What It Does
Idea Coach helps you:
- Capture ideas, problems, and challenges as they come
- Categorize by type, domain, energy, urgency, and importance
- Review periodically (daily → quarterly based on importance)
- Ship ideas to GitHub repos when ready
- Track progress and know when to let go
Philosophy
Be critical, not just supportive. Idea Coach will:
- Suggest dropping ideas that aren't worth pursuing
- Ask hard questions during reviews
- Track which ideas actually ship vs. rot forever
Commands
Core Commands
| Command | Description |
|---|---|
/idea <text> | Capture a new idea |
/idea_list | List active ideas |
/idea_list --due | Show ideas due for review |
/idea_get <id> | Get idea details |
/idea_update <id> | Update idea attributes |
/idea_review <id> | Add review interaction |
/idea_drop <id> | Mark as dropped (requires reason) |
/idea_done <id> | Mark as completed |
/idea_stats | Show statistics |
GitHub Commands
| Command | Description |
|---|---|
/idea_link <id> <owner/repo> | Link to existing repo |
/idea_ship <id> | Create new repo for idea |
/idea_ship <id> --public | Create public repo |
/idea_repo <id> | Show linked repo status |
/idea_sync <id> | Create/update GitHub issue |
Attributes
Types
- 💡 idea — Something to build or create
- 🔧 problem — Something to fix or solve
- 🎯 challenge — Something to overcome
Status Flow
captured → exploring → developing → shipped/done
↓ ↓
parked blocked
↓
droppedImportance → Review Cycle
| Importance | Energy | Review Cycle |
|---|---|---|
| critical | high | daily |
| critical | * | weekly |
| important | high | weekly |
| important | * | biweekly |
| nice-to-have | * | monthly |
| parked | * | quarterly |
GitHub Integration
Prerequisites
ghCLI installed and authenticated- Run
gh auth loginif not set up
Workflow Example
# 1. Capture idea
/idea "Build a CLI for task management"
# 2. Develop it
/idea_update abc123 --status developing
# 3. Ship it to GitHub
/idea_ship abc123
# 4. Or link to existing repo
/idea_link abc123 moinsen-dev/my-cli
# 5. Check repo status
/idea_repo abc123
# 6. Sync as GitHub issue
/idea_sync abc123CLI Usage
# Add idea
python scripts/coach.py add "Build something cool" --type idea --importance important
# List ideas
python scripts/coach.py list
python scripts/coach.py list --due
python scripts/coach.py list --github # Only with linked repos
# GitHub operations
python scripts/coach.py link <id> owner/repo
python scripts/coach.py ship <id> --owner moinsen-dev
python scripts/coach.py repo-status <id>
python scripts/coach.py sync-issue <id> --labels enhancement,ideaData Storage
Ideas are stored in ~/.openclaw/idea-coach/ideas.json
Each idea tracks:
- Basic info (title, description, type, domain)
- Status and progress
- Energy, urgency, importance
- Review schedule and history
- GitHub integration (repo, issue, sync timestamps)
- Interaction log
Tips
1. Capture quickly — Don't overthink the initial capture 2. Review honestly — Use reviews to kill stale ideas 3. Ship early — Create a repo as soon as an idea has momentum 4. Sync issues — Use GitHub issues for detailed tracking 5. Drop freely — A dropped idea is a decision, not a failure
{
"owner": "udiedrichsen",
"slug": "idea-coach",
"displayName": "Idea Coach",
"latest": {
"version": "0.2.0",
"publishedAt": 1770022097561,
"commit": "https://github.com/clawdbot/skills/commit/4a9957b1ab68b14122286e7b48abaa053751fee0"
},
"history": []
}
Idea Coach - Konzept
Vision
Ein intelligenter Sparringspartner für Ideen, Probleme und Herausforderungen. Nicht aggressiv, aber präsent. Hilft Gedanken aufs nächste Level zu bringen.
---
Kernprinzipien
1. Erfassen > Bewerten - Erst aufnehmen, dann kategorisieren 2. Sanfte Begleitung - Kein Druck, aber regelmäßige Impulse 3. Adaptive Frequenz - Wichtiges öfter, Nebensächliches seltener 4. Sparring, nicht Coaching - Dialog statt Belehrung
---
Datenmodell
Eintrag:
id: uuid
type: idea | problem | challenge
title: "Kurzbeschreibung"
description: "Ausführliche Beschreibung"
# Kategorisierung
domain: work | personal | health | finance | creative | tech | other
energy: high | medium | low # Wie viel Energie gibt/kostet es?
urgency: urgent | soon | someday # Zeitdruck
importance: critical | important | nice-to-have
# Status & Fortschritt
status: captured | exploring | developing | blocked | parked | done
progress: 0-100 # Gefühlter Fortschritt
nextAction: "Konkreter nächster Schritt"
# Review-Rhythmus (automatisch berechnet)
reviewCycle: daily | weekly | biweekly | monthly | quarterly
lastReview: timestamp
nextReview: timestamp
# History
created: timestamp
updated: timestamp
interactions: [
{ date, type: "capture|review|brainstorm|progress", notes }
]
# Verknüpfungen
relatedTo: [ids] # Verbundene Einträge
blockedBy: [ids] # Abhängigkeiten---
Review-Rhythmus-Logik
┌─────────────────────────────────────────────────────────┐
│ REVIEW FREQUENCY │
├─────────────────────────────────────────────────────────┤
│ │
│ importance: critical + energy: high → DAILY │
│ importance: important + energy: high → WEEKLY │
│ importance: important + energy: medium → BIWEEKLY │
│ importance: nice-to-have → MONTHLY │
│ status: blocked → WEEKLY check │
│ status: parked → QUARTERLY │
│ │
└─────────────────────────────────────────────────────────┘---
Interaktions-Modi
1. Capture Mode - Schnelles Erfassen
User: "Ich hab da eine Idee für..."
Bot: Erfasst, fragt kurz nach Typ (Idee/Problem/Challenge)
Optional: Domain, erste Einschätzung
→ Speichern, fertig. Kein Stress.2. Review Mode - Sanftes Erinnern
Bot: "Hey, du hattest vor 3 Tagen die Idee X notiert.
Magst du kurz drüber nachdenken?
1️⃣ Ja, lass uns brainstormen
2️⃣ Später (in 2 Tagen)
3️⃣ Parken (Quarterly Review)
4️⃣ Erledigt / Verworfen"3. Brainstorm Mode - Aktives Sparring
User: "Lass uns über Idee X reden"
Bot: Stellt gezielte Fragen:
- "Was wäre der ideale Outcome?"
- "Was hält dich gerade zurück?"
- "Was wäre der kleinste nächste Schritt?"
- "Wer könnte dir dabei helfen?"
Fasst am Ende zusammen, updatet Status4. Progress Mode - Fortschritt tracken
User: "Update zu Idee X: Habe heute Y gemacht"
Bot: Erfasst Fortschritt, gratuliert
Fragt nach nächstem Schritt
Passt ggf. Review-Rhythmus an5. Dashboard Mode - Übersicht
User: "/ideas" oder "Was liegt an?"
Bot: Zeigt gruppiert:
- 🔥 Heute dran (daily reviews)
- 📅 Diese Woche (weekly)
- 💡 Neue Ideen (unbewertet)
- 🚧 Blockiert
- 📊 Statistik---
Proaktives Verhalten
Morning Check (optional, 9:00)
"Guten Morgen! Du hast 2 Ideen für heute:
💡 [Idee A] - Status: exploring
🔧 [Problem B] - Status: developing
Womit möchtest du anfangen?"Gentle Nudge (wenn Review überfällig)
"Hey, deine Idee 'X' wartet seit 5 Tagen auf dich.
Kurz drüber nachdenken? (Dauert nur 2 Min)
[Ja] [Später] [Parken]"Weekly Summary (Sonntag abend)
"📊 Deine Woche:
- 3 neue Ideen erfasst
- 2 Ideen weiterentwickelt
- 1 Problem gelöst! 🎉
Nächste Woche anstehend:
- [Idee X] (wichtig, high energy)
- [Challenge Y] (blockiert seit 2 Wochen)"---
Commands
| Command | Beschreibung |
|---|---|
/idea <text> | Neue Idee erfassen |
/problem <text> | Neues Problem erfassen |
/challenge <text> | Neue Herausforderung erfassen |
/ideas | Dashboard / Übersicht |
/ideas today | Was ist heute dran? |
/ideas blocked | Blockierte Einträge |
/brainstorm <id> | Sparring-Session starten |
/progress <id> | Fortschritt updaten |
/park <id> | Auf Quarterly Review setzen |
/done <id> | Als erledigt markieren |
---
Ton & Persönlichkeit
DO:
- Freundlich, aber nicht überschwänglich
- Neugierig, stellt gute Fragen
- Geduldig, kein Druck
- Feiert kleine Erfolge
- Erinnert sanft
DON'T:
- Nicht belehrend
- Nicht zu viele Fragen auf einmal
- Nicht täglich nerven bei unwichtigen Dingen
- Nicht bewerten ("Das ist keine gute Idee")
---
Beispiel-Dialoge
Erfassung
User: "Ich könnte mal eine App bauen die X macht"
Bot: "📝 Notiert! Klingt nach einer Idee.
Kurze Einschätzung für mich:
• Domain? [Tech/Kreativ/Business/...]
• Energie-Level? [Gibt mir Energie / Neutral / Kostet Energie]
(Kannst du auch später machen)"Sanfte Erinnerung
Bot: "💭 Kleine Erinnerung:
Deine Idee 'App für X' ist jetzt 5 Tage alt.
Magst du 2 Minuten investieren?
Ich hätte ein paar Fragen die helfen könnten.
[Ja, lass uns] [Nicht jetzt] [Parken]"Brainstorming
User: [Ja, lass uns]
Bot: "Cool! Also: 'App für X'
Erste Frage: Wenn die App perfekt wäre,
was wäre das Wichtigste was sie kann?"
User: "Sie müsste Y und Z können"
Bot: "Y und Z - verstanden.
Zweite Frage: Was hält dich davon ab,
heute damit anzufangen?"
User: "Keine Zeit und weiß nicht wo anfangen"
Bot: "Klassiker! 😄
Was wäre der allerkleinste Schritt?
Einer der max. 15 Minuten dauert?"
User: "Hmm, mal schauen was es schon gibt"
Bot: "Perfekt! ✅
Nächster Schritt: '15 Min recherchieren was existiert'
Soll ich dich morgen daran erinnern?"---
Technische Umsetzung
Storage
~/.openclaw/idea-coach/
├── ideas.json # Alle Einträge
├── config.json # User-Preferences (Reminder-Zeiten, etc.)
└── interactions/ # Detail-Logs pro Eintrag
└── {id}.jsonCron Jobs
# Morning Check (optional)
- schedule: "0 9 * * *"
action: morning_check
# Evening Nudge für überfällige
- schedule: "0 18 * * *"
action: check_overdue
# Weekly Summary
- schedule: "0 20 * * 0"
action: weekly_summaryIntegration mit OpenClaw
- Heartbeat nutzen für sanfte Erinnerungen
- Cron für feste Zeiten (Morning, Weekly)
- Memory-Integration: Coach kennt Kontext aus anderen Gesprächen
---
MVP Features (v1)
1. ✅ Erfassen (idea/problem/challenge) 2. ✅ Einfaches Kategorisieren 3. ✅ Dashboard-Übersicht 4. ✅ Manuelle Review-Trigger 5. ✅ Fortschritt tracken
v2 Features
- ⏰ Automatische Reminder (Cron)
- 🧠 Brainstorm-Modus mit Fragen
- 📊 Weekly Summary
- 🔗 Verknüpfungen zwischen Ideen
v3 Features
- 🤖 KI-generierte Impulse
- 📈 Langzeit-Statistiken
- 🎯 Ziel-Tracking
- 👥 Teilen von Ideen
---
Offene Fragen
1. Reminder-Kanal: Telegram DM? Oder eigene Session? 2. Ton: Deutsch? Englisch? User-Sprache? 3. Integration: Standalone oder Teil von Second Brain? 4. Onboarding: Wie lernt der Coach den User kennen?
---
Konzept v0.1 - 2026-02-01
💡 Idea Coach
AI-powered idea/problem/challenge manager with GitHub integration.
An OpenClaw skill that helps you capture, categorize, review, and ship your ideas.
Features
- ✅ Capture ideas, problems, and challenges
- ✅ Categorize by type, domain, energy, urgency, importance
- ✅ Review cycles based on importance (daily → quarterly)
- ✅ GitHub integration — link or create repos
- ✅ Issue sync — turn ideas into trackable GitHub issues
- ✅ Critical feedback — suggests dropping stale ideas
Installation
As OpenClaw Skill
# Coming soon to ClawHub
clawhub install idea-coachManual Installation
git clone https://github.com/moinsen-dev/idea-coach.git
# Copy to your OpenClaw skills directoryRequirements
- Python 3.8+
ghCLI (for GitHub integration)
# macOS
brew install gh
# Linux
sudo apt install gh
# Then authenticate
gh auth loginQuick Start
# Capture an idea
python scripts/coach.py add "Build a CLI for X" --type idea --importance important
# List your ideas
python scripts/coach.py list
# Check what's due for review
python scripts/coach.py due
# Ship an idea to GitHub
python scripts/coach.py ship <id>
# Link to existing repo
python scripts/coach.py link <id> owner/repo
# Get repo status
python scripts/coach.py repo-status <id>OpenClaw Commands
| Command | Description |
|---|---|
/idea <text> | Capture new idea |
/idea_list | List active ideas |
/idea_due | Show due for review |
/idea_ship <id> | Create GitHub repo |
/idea_link <id> <repo> | Link to existing repo |
/idea_repo <id> | Show repo status |
/idea_sync <id> | Sync as GitHub issue |
Philosophy
Be critical, not just supportive.
- Ideas that sit too long get flagged
- Review prompts ask hard questions
- Dropping an idea is a valid decision
- Shipped > Perfect
Status Flow
captured → exploring → developing → shipped/done
↓ ↓
parked blocked
↓
droppedData
Ideas stored in ~/.openclaw/idea-coach/ideas.json
License
MIT
---
Built for OpenClaw 🦞
#!/usr/bin/env python3
"""
Idea Coach - Intelligent Idea/Problem/Challenge Manager
A sparring partner that captures, categorizes, and helps evolve ideas.
Can be critical - will suggest dropping ideas that aren't worth pursuing.
Now with GitHub integration for shipping ideas to repos!
Usage:
python coach.py add --type idea "App für X bauen"
python coach.py add --type problem "Server ist zu langsam"
python coach.py list
python coach.py list --due # What's due for review
python coach.py review <id> # Start review session
python coach.py update <id> --status developing
python coach.py drop <id> # Mark as dropped (with reason)
python coach.py stats
# GitHub Integration
python coach.py link <id> <owner/repo> # Link idea to existing repo
python coach.py ship <id> # Create new repo for idea
python coach.py repo-status <id> # Show linked repo status
python coach.py sync-issues <id> # Sync idea as GitHub issue
"""
import argparse
import json
import os
import re
import subprocess
import uuid
from datetime import datetime, timedelta, timezone
from pathlib import Path
from typing import Optional, List, Dict, Any
# Storage
COACH_DIR = Path.home() / ".openclaw" / "idea-coach"
IDEAS_FILE = COACH_DIR / "ideas.json"
CONFIG_FILE = COACH_DIR / "config.json"
# Types
TYPES = ["idea", "problem", "challenge"]
DOMAINS = ["work", "personal", "health", "finance", "creative", "tech", "other"]
ENERGY_LEVELS = ["high", "medium", "low"]
URGENCY_LEVELS = ["urgent", "soon", "someday"]
IMPORTANCE_LEVELS = ["critical", "important", "nice-to-have"]
STATUS_OPTIONS = ["captured", "exploring", "developing", "blocked", "parked", "shipped", "done", "dropped"]
# Review cycles in days
REVIEW_CYCLES = {
"daily": 1,
"weekly": 7,
"biweekly": 14,
"monthly": 30,
"quarterly": 90
}
# =============================================================================
# Core Functions
# =============================================================================
def ensure_dirs():
"""Create storage directories."""
COACH_DIR.mkdir(parents=True, exist_ok=True)
def load_ideas() -> List[Dict]:
"""Load all ideas."""
if IDEAS_FILE.exists():
return json.loads(IDEAS_FILE.read_text())
return []
def save_ideas(ideas: List[Dict]):
"""Save all ideas."""
ensure_dirs()
IDEAS_FILE.write_text(json.dumps(ideas, indent=2, ensure_ascii=False))
def generate_id() -> str:
"""Generate short unique ID."""
return str(uuid.uuid4())[:8]
def now_iso() -> str:
"""Current time in ISO format."""
return datetime.now(timezone.utc).isoformat()
def parse_date(date_str: str) -> datetime:
"""Parse ISO date string."""
return datetime.fromisoformat(date_str.replace('Z', '+00:00'))
def calculate_review_cycle(importance: str, energy: str, status: str) -> str:
"""Calculate review frequency based on attributes."""
if status == "blocked":
return "weekly"
if status == "parked":
return "quarterly"
if status in ["done", "dropped", "shipped"]:
return "quarterly"
if importance == "critical" and energy == "high":
return "daily"
if importance == "critical":
return "weekly"
if importance == "important" and energy == "high":
return "weekly"
if importance == "important":
return "biweekly"
return "monthly"
def calculate_next_review(last_review: str, cycle: str) -> str:
"""Calculate next review date."""
last = parse_date(last_review)
days = REVIEW_CYCLES.get(cycle, 30)
next_date = last + timedelta(days=days)
return next_date.isoformat()
def detect_language(text: str) -> str:
"""Simple language detection."""
german_indicators = ["ich", "und", "der", "die", "das", "ist", "ein", "für", "mit", "auf"]
words = text.lower().split()
german_count = sum(1 for w in words if w in german_indicators)
return "de" if german_count >= 2 or any(c in text for c in "äöüß") else "en"
def slugify(text: str) -> str:
"""Convert text to URL-safe slug."""
text = text.lower()
text = re.sub(r'[äÄ]', 'ae', text)
text = re.sub(r'[öÖ]', 'oe', text)
text = re.sub(r'[üÜ]', 'ue', text)
text = re.sub(r'[ß]', 'ss', text)
text = re.sub(r'[^a-z0-9]+', '-', text)
text = text.strip('-')
return text[:50]
# =============================================================================
# Idea CRUD
# =============================================================================
def create_idea(
idea_type: str,
title: str,
description: str = "",
domain: str = "other",
energy: str = "medium",
urgency: str = "someday",
importance: str = "nice-to-have"
) -> Dict:
"""Create a new idea/problem/challenge."""
now = now_iso()
idea = {
"id": generate_id(),
"type": idea_type,
"title": title,
"description": description,
"domain": domain,
"energy": energy,
"urgency": urgency,
"importance": importance,
"status": "captured",
"progress": 0,
"nextAction": None,
"reviewCycle": calculate_review_cycle(importance, energy, "captured"),
"lastReview": now,
"nextReview": None,
"created": now,
"updated": now,
"interactions": [
{"date": now, "type": "capture", "notes": "Initial capture"}
],
"relatedTo": [],
"dropReason": None,
"language": detect_language(title + " " + description),
# GitHub integration fields
"github": {
"repo": None, # "owner/repo"
"url": None, # Full URL
"issueNumber": None, # If synced as issue
"issueUrl": None,
"linkedAt": None,
"lastSync": None
}
}
idea["nextReview"] = calculate_next_review(now, idea["reviewCycle"])
return idea
def add_idea(idea_type: str, title: str, **kwargs) -> Dict:
"""Add a new idea to storage."""
ideas = load_ideas()
idea = create_idea(idea_type, title, **kwargs)
ideas.append(idea)
save_ideas(ideas)
return idea
def get_idea(idea_id: str) -> Optional[Dict]:
"""Get idea by ID."""
ideas = load_ideas()
for idea in ideas:
if idea["id"] == idea_id or idea["id"].startswith(idea_id):
return idea
return None
def update_idea(idea_id: str, updates: Dict) -> Optional[Dict]:
"""Update an idea."""
ideas = load_ideas()
for i, idea in enumerate(ideas):
if idea["id"] == idea_id or idea["id"].startswith(idea_id):
# Deep merge for nested dicts like 'github'
for key, value in updates.items():
if isinstance(value, dict) and isinstance(idea.get(key), dict):
idea[key].update(value)
else:
idea[key] = value
idea["updated"] = now_iso()
# Recalculate review cycle if relevant fields changed
if any(k in updates for k in ["importance", "energy", "status"]):
idea["reviewCycle"] = calculate_review_cycle(
idea.get("importance", "nice-to-have"),
idea.get("energy", "medium"),
idea.get("status", "captured")
)
ideas[i] = idea
save_ideas(ideas)
return idea
return None
def add_interaction(idea_id: str, interaction_type: str, notes: str) -> Optional[Dict]:
"""Add an interaction to an idea."""
idea = get_idea(idea_id)
if not idea:
return None
interaction = {
"date": now_iso(),
"type": interaction_type,
"notes": notes
}
idea["interactions"].append(interaction)
idea["lastReview"] = now_iso()
idea["nextReview"] = calculate_next_review(now_iso(), idea["reviewCycle"])
return update_idea(idea_id, {
"interactions": idea["interactions"],
"lastReview": idea["lastReview"],
"nextReview": idea["nextReview"]
})
def drop_idea(idea_id: str, reason: str) -> Optional[Dict]:
"""Mark idea as dropped with reason."""
idea = get_idea(idea_id)
if not idea:
return None
return update_idea(idea_id, {
"status": "dropped",
"dropReason": reason,
"interactions": idea["interactions"] + [{
"date": now_iso(),
"type": "dropped",
"notes": reason
}]
})
# =============================================================================
# GitHub Integration
# =============================================================================
def run_gh_command(args: List[str]) -> Dict[str, Any]:
"""Run a gh CLI command and return result."""
try:
result = subprocess.run(
["gh"] + args,
capture_output=True,
text=True,
timeout=30
)
return {
"success": result.returncode == 0,
"stdout": result.stdout.strip(),
"stderr": result.stderr.strip(),
"code": result.returncode
}
except FileNotFoundError:
return {"success": False, "error": "gh CLI not installed", "code": -1}
except subprocess.TimeoutExpired:
return {"success": False, "error": "Command timed out", "code": -2}
def check_gh_auth() -> bool:
"""Check if gh CLI is authenticated."""
result = run_gh_command(["auth", "status"])
return result["success"]
def get_repo_info(repo: str) -> Optional[Dict]:
"""Get repository information from GitHub."""
result = run_gh_command([
"repo", "view", repo, "--json",
"name,owner,url,description,isPrivate,updatedAt,stargazerCount,forkCount,issues,pullRequests"
])
if result["success"]:
try:
data = json.loads(result["stdout"])
return {
"name": data.get("name"),
"owner": data.get("owner", {}).get("login"),
"url": data.get("url"),
"description": data.get("description"),
"isPrivate": data.get("isPrivate"),
"updatedAt": data.get("updatedAt"),
"stars": data.get("stargazerCount", 0),
"forks": data.get("forkCount", 0),
"openIssues": data.get("issues", {}).get("totalCount", 0),
"openPRs": data.get("pullRequests", {}).get("totalCount", 0)
}
except json.JSONDecodeError:
return None
return None
def get_repo_commits(repo: str, limit: int = 5) -> List[Dict]:
"""Get recent commits from a repository."""
result = run_gh_command([
"api", f"repos/{repo}/commits",
"--jq", f".[:{limit}] | .[] | {{sha: .sha[:7], message: .commit.message | split(\"\\n\")[0], date: .commit.author.date[:10]}}"
])
if result["success"]:
commits = []
for line in result["stdout"].strip().split("\n"):
if line:
try:
commits.append(json.loads(line))
except:
pass
return commits
return []
def link_repo(idea_id: str, repo: str) -> Dict:
"""Link an idea to an existing GitHub repository."""
idea = get_idea(idea_id)
if not idea:
return {"success": False, "error": "Idea not found"}
# Validate repo exists
repo_info = get_repo_info(repo)
if not repo_info:
return {"success": False, "error": f"Repository '{repo}' not found or not accessible"}
# Update idea
update_idea(idea_id, {
"github": {
"repo": repo,
"url": repo_info["url"],
"issueNumber": idea.get("github", {}).get("issueNumber"),
"issueUrl": idea.get("github", {}).get("issueUrl"),
"linkedAt": now_iso(),
"lastSync": now_iso()
}
})
add_interaction(idea_id, "github_linked", f"Linked to {repo}")
return {
"success": True,
"idea_id": idea_id,
"repo": repo,
"url": repo_info["url"],
"repo_info": repo_info
}
def ship_idea(idea_id: str, owner: str = None, private: bool = True, description: str = None) -> Dict:
"""Create a new GitHub repository for an idea."""
idea = get_idea(idea_id)
if not idea:
return {"success": False, "error": "Idea not found"}
# Check if already linked
if idea.get("github", {}).get("repo"):
return {"success": False, "error": f"Idea already linked to {idea['github']['repo']}"}
# Generate repo name from title
repo_name = slugify(idea["title"])
if not repo_name:
repo_name = f"idea-{idea['id']}"
# Build create command
args = ["repo", "create"]
if owner:
args.append(f"{owner}/{repo_name}")
else:
args.append(repo_name)
if private:
args.append("--private")
else:
args.append("--public")
desc = description or idea.get("description") or idea["title"]
args.extend(["--description", desc[:255]])
# Create repo
result = run_gh_command(args)
if not result["success"]:
return {"success": False, "error": result.get("stderr") or result.get("error")}
# Parse created repo URL
repo_url = result["stdout"].strip()
# Extract owner/repo from URL
match = re.search(r'github\.com[/:]([^/]+)/([^/\s]+)', repo_url)
if match:
full_repo = f"{match.group(1)}/{match.group(2).replace('.git', '')}"
else:
full_repo = repo_name
# Update idea
update_idea(idea_id, {
"status": "shipped",
"github": {
"repo": full_repo,
"url": repo_url,
"issueNumber": None,
"issueUrl": None,
"linkedAt": now_iso(),
"lastSync": now_iso()
}
})
add_interaction(idea_id, "shipped", f"Created repo {full_repo}")
return {
"success": True,
"idea_id": idea_id,
"repo": full_repo,
"url": repo_url,
"private": private
}
def get_repo_status(idea_id: str) -> Dict:
"""Get status of linked GitHub repository."""
idea = get_idea(idea_id)
if not idea:
return {"success": False, "error": "Idea not found"}
repo = idea.get("github", {}).get("repo")
if not repo:
return {"success": False, "error": "No GitHub repo linked", "idea_id": idea_id}
repo_info = get_repo_info(repo)
if not repo_info:
return {"success": False, "error": f"Could not fetch repo info for {repo}"}
commits = get_repo_commits(repo, 5)
# Update last sync time
update_idea(idea_id, {"github": {"lastSync": now_iso()}})
return {
"success": True,
"idea_id": idea_id,
"idea_title": idea["title"],
"repo": repo,
"url": repo_info["url"],
"description": repo_info["description"],
"isPrivate": repo_info["isPrivate"],
"stars": repo_info["stars"],
"forks": repo_info["forks"],
"openIssues": repo_info["openIssues"],
"openPRs": repo_info["openPRs"],
"updatedAt": repo_info["updatedAt"],
"recentCommits": commits
}
def sync_as_issue(idea_id: str, labels: List[str] = None) -> Dict:
"""Create or update a GitHub issue from the idea."""
idea = get_idea(idea_id)
if not idea:
return {"success": False, "error": "Idea not found"}
repo = idea.get("github", {}).get("repo")
if not repo:
return {"success": False, "error": "No GitHub repo linked. Use 'link' first."}
existing_issue = idea.get("github", {}).get("issueNumber")
# Build issue body
body_parts = [
f"**Type:** {idea['type'].capitalize()}",
f"**Status:** {idea['status']}",
f"**Importance:** {idea['importance']}",
f"**Energy:** {idea['energy']}",
"",
"## Description",
idea.get("description") or "_No description provided._",
"",
"---",
f"_Synced from Idea Coach (ID: {idea['id']})_"
]
body = "\n".join(body_parts)
if existing_issue:
# Update existing issue
args = ["issue", "edit", str(existing_issue), "-R", repo, "--body", body]
result = run_gh_command(args)
if result["success"]:
return {
"success": True,
"action": "updated",
"issue_number": existing_issue,
"url": idea["github"]["issueUrl"]
}
else:
return {"success": False, "error": result.get("stderr") or "Failed to update issue"}
else:
# Create new issue
type_emoji = {"idea": "💡", "problem": "🔧", "challenge": "🎯"}.get(idea["type"], "📝")
title = f"{type_emoji} {idea['title']}"
args = ["issue", "create", "-R", repo, "--title", title, "--body", body]
if labels:
for label in labels:
args.extend(["--label", label])
result = run_gh_command(args)
if result["success"]:
# Parse issue URL from output
issue_url = result["stdout"].strip()
issue_number = None
match = re.search(r'/issues/(\d+)', issue_url)
if match:
issue_number = int(match.group(1))
# Update idea with issue info
update_idea(idea_id, {
"github": {
"issueNumber": issue_number,
"issueUrl": issue_url,
"lastSync": now_iso()
}
})
add_interaction(idea_id, "issue_created", f"Created issue #{issue_number}")
return {
"success": True,
"action": "created",
"issue_number": issue_number,
"url": issue_url
}
else:
return {"success": False, "error": result.get("stderr") or "Failed to create issue"}
def unlink_repo(idea_id: str) -> Dict:
"""Unlink a GitHub repository from an idea."""
idea = get_idea(idea_id)
if not idea:
return {"success": False, "error": "Idea not found"}
if not idea.get("github", {}).get("repo"):
return {"success": False, "error": "No repo linked"}
old_repo = idea["github"]["repo"]
update_idea(idea_id, {
"github": {
"repo": None,
"url": None,
"issueNumber": None,
"issueUrl": None,
"linkedAt": None,
"lastSync": None
}
})
add_interaction(idea_id, "github_unlinked", f"Unlinked from {old_repo}")
return {"success": True, "unlinked": old_repo}
# =============================================================================
# Query Functions
# =============================================================================
def get_due_for_review() -> List[Dict]:
"""Get ideas due for review."""
ideas = load_ideas()
now = datetime.now(timezone.utc)
due = []
for idea in ideas:
if idea["status"] in ["done", "dropped"]:
continue
next_review = parse_date(idea.get("nextReview", idea["created"]))
if next_review <= now:
due.append(idea)
importance_order = {"critical": 0, "important": 1, "nice-to-have": 2}
urgency_order = {"urgent": 0, "soon": 1, "someday": 2}
due.sort(key=lambda x: (
importance_order.get(x.get("importance"), 2),
urgency_order.get(x.get("urgency"), 2)
))
return due
def get_stats() -> Dict:
"""Get statistics."""
ideas = load_ideas()
stats = {
"total": len(ideas),
"byType": {},
"byStatus": {},
"byImportance": {},
"dueForReview": len(get_due_for_review()),
"droppedCount": 0,
"completedCount": 0,
"shippedCount": 0,
"activeCount": 0,
"withGithub": 0
}
for idea in ideas:
t = idea.get("type", "idea")
stats["byType"][t] = stats["byType"].get(t, 0) + 1
s = idea.get("status", "captured")
stats["byStatus"][s] = stats["byStatus"].get(s, 0) + 1
imp = idea.get("importance", "nice-to-have")
stats["byImportance"][imp] = stats["byImportance"].get(imp, 0) + 1
if s == "dropped":
stats["droppedCount"] += 1
elif s == "done":
stats["completedCount"] += 1
elif s == "shipped":
stats["shippedCount"] += 1
elif s not in ["done", "dropped", "parked", "shipped"]:
stats["activeCount"] += 1
if idea.get("github", {}).get("repo"):
stats["withGithub"] += 1
return stats
def list_ideas(
status: Optional[str] = None,
idea_type: Optional[str] = None,
due_only: bool = False,
with_github: bool = False
) -> List[Dict]:
"""List ideas with optional filters."""
if due_only:
return get_due_for_review()
ideas = load_ideas()
if status is None:
ideas = [i for i in ideas if i.get("status") not in ["done", "dropped"]]
elif status:
ideas = [i for i in ideas if i.get("status") == status]
if idea_type:
ideas = [i for i in ideas if i.get("type") == idea_type]
if with_github:
ideas = [i for i in ideas if i.get("github", {}).get("repo")]
return ideas
# =============================================================================
# CLI
# =============================================================================
def main():
parser = argparse.ArgumentParser(description="Idea Coach - with GitHub Integration")
subparsers = parser.add_subparsers(dest="command", required=True)
# Add
add_parser = subparsers.add_parser("add", help="Add new idea/problem/challenge")
add_parser.add_argument("title", help="Title/description")
add_parser.add_argument("--type", "-t", choices=TYPES, default="idea")
add_parser.add_argument("--domain", "-d", choices=DOMAINS, default="other")
add_parser.add_argument("--energy", "-e", choices=ENERGY_LEVELS, default="medium")
add_parser.add_argument("--urgency", "-u", choices=URGENCY_LEVELS, default="someday")
add_parser.add_argument("--importance", "-i", choices=IMPORTANCE_LEVELS, default="nice-to-have")
add_parser.add_argument("--description", help="Detailed description")
# List
list_parser = subparsers.add_parser("list", help="List ideas")
list_parser.add_argument("--status", "-s", choices=STATUS_OPTIONS)
list_parser.add_argument("--type", "-t", choices=TYPES)
list_parser.add_argument("--due", action="store_true", help="Only show due for review")
list_parser.add_argument("--all", "-a", action="store_true", help="Include done/dropped")
list_parser.add_argument("--github", "-g", action="store_true", help="Only with GitHub repos")
# Get
get_parser = subparsers.add_parser("get", help="Get idea details")
get_parser.add_argument("id", help="Idea ID")
# Update
update_parser = subparsers.add_parser("update", help="Update idea")
update_parser.add_argument("id", help="Idea ID")
update_parser.add_argument("--status", "-s", choices=STATUS_OPTIONS)
update_parser.add_argument("--importance", "-i", choices=IMPORTANCE_LEVELS)
update_parser.add_argument("--energy", "-e", choices=ENERGY_LEVELS)
update_parser.add_argument("--progress", "-p", type=int)
update_parser.add_argument("--next-action", "-n")
update_parser.add_argument("--description")
# Review
review_parser = subparsers.add_parser("review", help="Add review interaction")
review_parser.add_argument("id", help="Idea ID")
review_parser.add_argument("--notes", "-n", default="Reviewed")
# Drop
drop_parser = subparsers.add_parser("drop", help="Drop an idea")
drop_parser.add_argument("id", help="Idea ID")
drop_parser.add_argument("--reason", "-r", required=True, help="Reason for dropping")
# Done
done_parser = subparsers.add_parser("done", help="Mark as done")
done_parser.add_argument("id", help="Idea ID")
done_parser.add_argument("--notes", "-n", default="Completed")
# Stats
subparsers.add_parser("stats", help="Show statistics")
# Due
subparsers.add_parser("due", help="Show items due for review")
# === GitHub Commands ===
# Link
link_parser = subparsers.add_parser("link", help="Link idea to existing GitHub repo")
link_parser.add_argument("id", help="Idea ID")
link_parser.add_argument("repo", help="Repository (owner/repo)")
# Ship
ship_parser = subparsers.add_parser("ship", help="Create new GitHub repo for idea")
ship_parser.add_argument("id", help="Idea ID")
ship_parser.add_argument("--owner", "-o", help="GitHub owner/org (default: your user)")
ship_parser.add_argument("--public", action="store_true", help="Make repo public")
ship_parser.add_argument("--description", "-d", help="Repo description")
# Repo Status
repo_status_parser = subparsers.add_parser("repo-status", help="Get linked repo status")
repo_status_parser.add_argument("id", help="Idea ID")
# Sync Issue
sync_parser = subparsers.add_parser("sync-issue", help="Sync idea as GitHub issue")
sync_parser.add_argument("id", help="Idea ID")
sync_parser.add_argument("--labels", "-l", nargs="*", help="Issue labels")
# Unlink
unlink_parser = subparsers.add_parser("unlink", help="Unlink GitHub repo from idea")
unlink_parser.add_argument("id", help="Idea ID")
# Check Auth
subparsers.add_parser("gh-auth", help="Check GitHub CLI auth status")
args = parser.parse_args()
ensure_dirs()
# Dispatch commands
if args.command == "add":
idea = add_idea(
args.type, args.title,
description=getattr(args, 'description', '') or '',
domain=args.domain,
energy=args.energy,
urgency=args.urgency,
importance=args.importance
)
print(json.dumps(idea, indent=2, ensure_ascii=False))
elif args.command == "list":
status = None if args.all else args.status
ideas = list_ideas(
status=status,
idea_type=args.type,
due_only=args.due,
with_github=args.github
)
result = [{
"id": i["id"],
"type": i["type"],
"title": i["title"],
"status": i["status"],
"importance": i["importance"],
"github": i.get("github", {}).get("repo")
} for i in ideas]
print(json.dumps(result, indent=2, ensure_ascii=False))
elif args.command == "get":
idea = get_idea(args.id)
if idea:
print(json.dumps(idea, indent=2, ensure_ascii=False))
else:
print(json.dumps({"error": "Not found"}))
elif args.command == "update":
updates = {}
if args.status:
updates["status"] = args.status
if args.importance:
updates["importance"] = args.importance
if args.energy:
updates["energy"] = args.energy
if args.progress is not None:
updates["progress"] = args.progress
if args.next_action:
updates["nextAction"] = args.next_action
if hasattr(args, 'description') and args.description:
updates["description"] = args.description
idea = update_idea(args.id, updates)
if idea:
print(json.dumps(idea, indent=2, ensure_ascii=False))
else:
print(json.dumps({"error": "Not found"}))
elif args.command == "review":
idea = add_interaction(args.id, "review", args.notes)
if idea:
print(json.dumps(idea, indent=2, ensure_ascii=False))
else:
print(json.dumps({"error": "Not found"}))
elif args.command == "drop":
idea = drop_idea(args.id, args.reason)
if idea:
print(json.dumps(idea, indent=2, ensure_ascii=False))
else:
print(json.dumps({"error": "Not found"}))
elif args.command == "done":
idea = update_idea(args.id, {"status": "done"})
if idea:
add_interaction(args.id, "completed", args.notes)
print(json.dumps(idea, indent=2, ensure_ascii=False))
else:
print(json.dumps({"error": "Not found"}))
elif args.command == "stats":
stats = get_stats()
print(json.dumps(stats, indent=2))
elif args.command == "due":
due = get_due_for_review()
result = [{
"id": i["id"],
"type": i["type"],
"title": i["title"],
"reviewCycle": i["reviewCycle"],
"lastReview": i["lastReview"][:10],
"github": i.get("github", {}).get("repo")
} for i in due]
print(json.dumps(result, indent=2, ensure_ascii=False))
# GitHub commands
elif args.command == "link":
result = link_repo(args.id, args.repo)
print(json.dumps(result, indent=2, ensure_ascii=False))
elif args.command == "ship":
result = ship_idea(
args.id,
owner=args.owner,
private=not args.public,
description=args.description
)
print(json.dumps(result, indent=2, ensure_ascii=False))
elif args.command == "repo-status":
result = get_repo_status(args.id)
print(json.dumps(result, indent=2, ensure_ascii=False))
elif args.command == "sync-issue":
result = sync_as_issue(args.id, labels=args.labels)
print(json.dumps(result, indent=2, ensure_ascii=False))
elif args.command == "unlink":
result = unlink_repo(args.id)
print(json.dumps(result, indent=2, ensure_ascii=False))
elif args.command == "gh-auth":
if check_gh_auth():
print(json.dumps({"authenticated": True}))
else:
print(json.dumps({"authenticated": False, "hint": "Run: gh auth login"}))
if __name__ == "__main__":
main()