codeboard 0.1.0__tar.gz → 0.2.0__tar.gz

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: codeboard
3
- Version: 0.1.0
3
+ Version: 0.2.0
4
4
  Summary: Git repository dashboard for your local codebase
5
5
  Author: Shaoyi Yang
6
6
  License-Expression: MIT
@@ -212,6 +212,37 @@ cb --filter simona health # also works
212
212
  | `cb graph <repo> [action]` | Code graph analysis | [gitnexus](https://github.com/nicolo-ribaudo/gitnexus) |
213
213
  | `cb config` | Show or generate config file | — |
214
214
  | `cb completions [bash\|zsh\|fish]` | Generate shell completion script | — |
215
+ | `cb mcp` | Run as MCP server for AI assistants | — |
216
+
217
+ ## MCP Server (AI Integration)
218
+
219
+ CodeBoard can run as an [MCP](https://modelcontextprotocol.io/) server, letting AI assistants (Claude Code, Cursor, etc.) query your repo status directly.
220
+
221
+ Add to your Claude Code MCP config (`~/.claude/claude_desktop_config.json`):
222
+
223
+ ```json
224
+ {
225
+ "mcpServers": {
226
+ "codeboard": {
227
+ "command": "cb",
228
+ "args": ["mcp"]
229
+ }
230
+ }
231
+ }
232
+ ```
233
+
234
+ Available tools:
235
+
236
+ | Tool | Description |
237
+ |------|-------------|
238
+ | `list_repos` | Quick list of all repos with name, branch, dirty status |
239
+ | `repo_status` | Full dashboard data (branch, commits, language, remote) |
240
+ | `health_check` | Categorized health report (uncommitted, unpushed, inactive) |
241
+ | `recent_activity` | Cross-repo commit timeline |
242
+ | `repo_detail` | Deep dive into a single repo |
243
+ | `search_code` | Search code across all repos (regex) |
244
+
245
+ No extra dependencies — uses stdio JSON-RPC 2.0, zero config.
215
246
 
216
247
  ## Shell Completion
217
248
 
@@ -186,6 +186,37 @@ cb --filter simona health # also works
186
186
  | `cb graph <repo> [action]` | Code graph analysis | [gitnexus](https://github.com/nicolo-ribaudo/gitnexus) |
187
187
  | `cb config` | Show or generate config file | — |
188
188
  | `cb completions [bash\|zsh\|fish]` | Generate shell completion script | — |
189
+ | `cb mcp` | Run as MCP server for AI assistants | — |
190
+
191
+ ## MCP Server (AI Integration)
192
+
193
+ CodeBoard can run as an [MCP](https://modelcontextprotocol.io/) server, letting AI assistants (Claude Code, Cursor, etc.) query your repo status directly.
194
+
195
+ Add to your Claude Code MCP config (`~/.claude/claude_desktop_config.json`):
196
+
197
+ ```json
198
+ {
199
+ "mcpServers": {
200
+ "codeboard": {
201
+ "command": "cb",
202
+ "args": ["mcp"]
203
+ }
204
+ }
205
+ }
206
+ ```
207
+
208
+ Available tools:
209
+
210
+ | Tool | Description |
211
+ |------|-------------|
212
+ | `list_repos` | Quick list of all repos with name, branch, dirty status |
213
+ | `repo_status` | Full dashboard data (branch, commits, language, remote) |
214
+ | `health_check` | Categorized health report (uncommitted, unpushed, inactive) |
215
+ | `recent_activity` | Cross-repo commit timeline |
216
+ | `repo_detail` | Deep dive into a single repo |
217
+ | `search_code` | Search code across all repos (regex) |
218
+
219
+ No extra dependencies — uses stdio JSON-RPC 2.0, zero config.
189
220
 
190
221
  ## Shell Completion
191
222
 
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: codeboard
3
- Version: 0.1.0
3
+ Version: 0.2.0
4
4
  Summary: Git repository dashboard for your local codebase
5
5
  Author: Shaoyi Yang
6
6
  License-Expression: MIT
@@ -212,6 +212,37 @@ cb --filter simona health # also works
212
212
  | `cb graph <repo> [action]` | Code graph analysis | [gitnexus](https://github.com/nicolo-ribaudo/gitnexus) |
213
213
  | `cb config` | Show or generate config file | — |
214
214
  | `cb completions [bash\|zsh\|fish]` | Generate shell completion script | — |
215
+ | `cb mcp` | Run as MCP server for AI assistants | — |
216
+
217
+ ## MCP Server (AI Integration)
218
+
219
+ CodeBoard can run as an [MCP](https://modelcontextprotocol.io/) server, letting AI assistants (Claude Code, Cursor, etc.) query your repo status directly.
220
+
221
+ Add to your Claude Code MCP config (`~/.claude/claude_desktop_config.json`):
222
+
223
+ ```json
224
+ {
225
+ "mcpServers": {
226
+ "codeboard": {
227
+ "command": "cb",
228
+ "args": ["mcp"]
229
+ }
230
+ }
231
+ }
232
+ ```
233
+
234
+ Available tools:
235
+
236
+ | Tool | Description |
237
+ |------|-------------|
238
+ | `list_repos` | Quick list of all repos with name, branch, dirty status |
239
+ | `repo_status` | Full dashboard data (branch, commits, language, remote) |
240
+ | `health_check` | Categorized health report (uncommitted, unpushed, inactive) |
241
+ | `recent_activity` | Cross-repo commit timeline |
242
+ | `repo_detail` | Deep dive into a single repo |
243
+ | `search_code` | Search code across all repos (regex) |
244
+
245
+ No extra dependencies — uses stdio JSON-RPC 2.0, zero config.
215
246
 
216
247
  ## Shell Completion
217
248
 
@@ -1,7 +1,7 @@
1
1
  #!/usr/bin/env python3
2
2
  """CodeBoard — Git repository dashboard for your local codebase."""
3
3
 
4
- __version__ = "0.1.0"
4
+ __version__ = "0.2.0"
5
5
 
6
6
  import argparse
7
7
  import json
@@ -3056,6 +3056,286 @@ def cmd_config(args):
3056
3056
  console.print(f"[green]Generated default config: {CONFIG_FILE}[/green]")
3057
3057
 
3058
3058
 
3059
+ # ---------------------------------------------------------------------------
3060
+ # MCP Server (Model Context Protocol) — stdio JSON-RPC 2.0
3061
+ # ---------------------------------------------------------------------------
3062
+
3063
+ _MCP_TOOLS = [
3064
+ {
3065
+ "name": "list_repos",
3066
+ "description": "List all git repositories with basic status (name, path, branch, dirty file count, remote type). Fast, lightweight scan.",
3067
+ "inputSchema": {
3068
+ "type": "object",
3069
+ "properties": {
3070
+ "filter": {"type": "string", "description": "Filter repos by name keyword"},
3071
+ },
3072
+ },
3073
+ },
3074
+ {
3075
+ "name": "repo_status",
3076
+ "description": "Full dashboard: all repos with branch, last commit time, dirty count, commit count, language, remote URL, ahead/behind counts. Sorted by recent activity.",
3077
+ "inputSchema": {
3078
+ "type": "object",
3079
+ "properties": {
3080
+ "filter": {"type": "string", "description": "Filter repos by name keyword"},
3081
+ "sort": {"type": "string", "enum": ["activity", "name", "commits", "changes"], "description": "Sort order (default: activity)"},
3082
+ },
3083
+ },
3084
+ },
3085
+ {
3086
+ "name": "health_check",
3087
+ "description": "Health report: categorizes repos into uncommitted changes, unpushed commits, behind remote, no remote, inactive >30d, and clean. Great for finding repos that need attention.",
3088
+ "inputSchema": {
3089
+ "type": "object",
3090
+ "properties": {
3091
+ "filter": {"type": "string", "description": "Filter repos by name keyword"},
3092
+ },
3093
+ },
3094
+ },
3095
+ {
3096
+ "name": "recent_activity",
3097
+ "description": "Cross-repo commit timeline: recent commits across all repos, sorted by time. Shows repo, author, message, and relative time.",
3098
+ "inputSchema": {
3099
+ "type": "object",
3100
+ "properties": {
3101
+ "filter": {"type": "string", "description": "Filter repos by name keyword"},
3102
+ "limit": {"type": "integer", "description": "Max number of commits (default: 30)"},
3103
+ },
3104
+ },
3105
+ },
3106
+ {
3107
+ "name": "repo_detail",
3108
+ "description": "Deep dive into a single repo: languages, contributors, tags, recent commits, branch list, remote info.",
3109
+ "inputSchema": {
3110
+ "type": "object",
3111
+ "properties": {
3112
+ "repo": {"type": "string", "description": "Repository name (fuzzy matched)"},
3113
+ },
3114
+ "required": ["repo"],
3115
+ },
3116
+ },
3117
+ {
3118
+ "name": "search_code",
3119
+ "description": "Search code across all repos using git grep (regex). Returns matching file paths and line content per repo.",
3120
+ "inputSchema": {
3121
+ "type": "object",
3122
+ "properties": {
3123
+ "pattern": {"type": "string", "description": "Search pattern (regex)"},
3124
+ "filter": {"type": "string", "description": "Filter repos by name keyword"},
3125
+ },
3126
+ "required": ["pattern"],
3127
+ },
3128
+ },
3129
+ ]
3130
+
3131
+
3132
+ def _mcp_handle_tool(name: str, arguments: dict, code_dir: Path) -> str:
3133
+ """Execute an MCP tool and return JSON string result."""
3134
+
3135
+ if name == "list_repos":
3136
+ repos = scan_all(code_dir, full=False, filter_kw=arguments.get("filter", ""))
3137
+ data = [
3138
+ {"name": r["name"], "path": r["path"], "branch": r["branch"],
3139
+ "dirty": r["dirty"], "remote_type": r["remote_type"],
3140
+ "last_commit": r["last_time_rel"]}
3141
+ for r in sort_repos(repos, "activity")
3142
+ ]
3143
+ return json.dumps(data, ensure_ascii=False)
3144
+
3145
+ if name == "repo_status":
3146
+ repos = scan_all(code_dir, full=True, filter_kw=arguments.get("filter", ""))
3147
+ repos = sort_repos(repos, arguments.get("sort", "activity"))
3148
+ for r in repos:
3149
+ r.pop("last_time", None)
3150
+ return json.dumps(repos, ensure_ascii=False)
3151
+
3152
+ if name == "health_check":
3153
+ repos = scan_all(code_dir, full=False, filter_kw=arguments.get("filter", ""))
3154
+ now_ts = datetime.now(timezone.utc).timestamp()
3155
+ categories = {
3156
+ "uncommitted": [],
3157
+ "unpushed": [],
3158
+ "behind": [],
3159
+ "no_remote": [],
3160
+ "inactive_30d": [],
3161
+ "clean": [],
3162
+ }
3163
+ for r in repos:
3164
+ if r["dirty"] > 0:
3165
+ categories["uncommitted"].append({"name": r["name"], "dirty": r["dirty"]})
3166
+ if r["ahead"] > 0:
3167
+ categories["unpushed"].append({"name": r["name"], "ahead": r["ahead"]})
3168
+ if r["behind"] > 0:
3169
+ categories["behind"].append({"name": r["name"], "behind": r["behind"]})
3170
+ if r["remote_type"] == "none":
3171
+ categories["no_remote"].append(r["name"])
3172
+ elif r["last_time_ts"] and (now_ts - r["last_time_ts"]) > 30 * 86400:
3173
+ categories["inactive_30d"].append(r["name"])
3174
+ if r["dirty"] == 0 and r["ahead"] == 0:
3175
+ categories["clean"].append(r["name"])
3176
+ summary = {k: {"count": len(v), "repos": v} for k, v in categories.items()}
3177
+ summary["total_repos"] = len(repos)
3178
+ return json.dumps(summary, ensure_ascii=False)
3179
+
3180
+ if name == "recent_activity":
3181
+ filter_kw = arguments.get("filter", "")
3182
+ limit = arguments.get("limit", 30)
3183
+ repos = list_git_repos(code_dir, filter_kw)
3184
+ all_commits = []
3185
+
3186
+ def _get_commits(repo_path):
3187
+ output = run_git(repo_path, "log", "--all", f"--max-count={limit}", "--format=%aI|%an|%s")
3188
+ if not output:
3189
+ return []
3190
+ entries = []
3191
+ for line in output.split("\n"):
3192
+ if "|" not in line:
3193
+ continue
3194
+ parts = line.split("|", 2)
3195
+ if len(parts) < 3:
3196
+ continue
3197
+ try:
3198
+ dt = datetime.fromisoformat(parts[0])
3199
+ except ValueError:
3200
+ continue
3201
+ entries.append({
3202
+ "time": parts[0], "time_rel": relative_time(dt),
3203
+ "author": parts[1], "message": parts[2],
3204
+ "repo": repo_path.name,
3205
+ })
3206
+ return entries
3207
+
3208
+ with ThreadPoolExecutor(max_workers=8) as pool:
3209
+ for future in as_completed({pool.submit(_get_commits, r): r for r in repos}):
3210
+ all_commits.extend(future.result())
3211
+
3212
+ all_commits.sort(key=lambda c: c["time"], reverse=True)
3213
+ return json.dumps(all_commits[:limit], ensure_ascii=False)
3214
+
3215
+ if name == "repo_detail":
3216
+ repo_name = arguments.get("repo", "")
3217
+ repo_path = find_repo(code_dir, repo_name)
3218
+ if not repo_path:
3219
+ return json.dumps({"error": f"Repository not found: {repo_name}"})
3220
+ info = scan_repo(repo_path, full=True)
3221
+ if not info:
3222
+ return json.dumps({"error": f"Failed to scan: {repo_name}"})
3223
+ info.pop("last_time", None)
3224
+ # Add extra detail
3225
+ branches = run_git(repo_path, "branch", "--list", "--no-color")
3226
+ info["branches"] = [b.strip().lstrip("* ") for b in branches.split("\n") if b.strip()] if branches else []
3227
+ tags = run_git(repo_path, "tag", "--list", "--sort=-creatordate")
3228
+ info["tags"] = [t.strip() for t in tags.split("\n") if t.strip()][:10] if tags else []
3229
+ recent = run_git(repo_path, "log", "--max-count=10", "--format=%aI|%an|%s")
3230
+ info["recent_commits"] = []
3231
+ if recent:
3232
+ for line in recent.split("\n"):
3233
+ parts = line.split("|", 2)
3234
+ if len(parts) >= 3:
3235
+ info["recent_commits"].append({"time": parts[0], "author": parts[1], "message": parts[2]})
3236
+ return json.dumps(info, ensure_ascii=False)
3237
+
3238
+ if name == "search_code":
3239
+ pattern = arguments.get("pattern", "")
3240
+ if not pattern:
3241
+ return json.dumps({"error": "pattern is required"})
3242
+ filter_kw = arguments.get("filter", "")
3243
+ repos = list_git_repos(code_dir, filter_kw)
3244
+ results = {}
3245
+
3246
+ def _search(repo_path):
3247
+ try:
3248
+ r = subprocess.run(
3249
+ ["git", "-C", str(repo_path), "grep", "-n", "--color=never", "-I", pattern],
3250
+ capture_output=True, text=True, timeout=15,
3251
+ )
3252
+ if r.returncode == 0 and r.stdout.strip():
3253
+ return (repo_path.name, r.stdout.strip())
3254
+ except subprocess.TimeoutExpired:
3255
+ pass
3256
+ return None
3257
+
3258
+ with ThreadPoolExecutor(max_workers=8) as pool:
3259
+ for future in as_completed({pool.submit(_search, r): r for r in repos}):
3260
+ result = future.result()
3261
+ if result:
3262
+ repo_name, output = result
3263
+ matches = []
3264
+ for line in output.split("\n")[:50]: # cap at 50 per repo
3265
+ parts = line.split(":", 2)
3266
+ if len(parts) >= 3:
3267
+ matches.append({"file": parts[0], "line": parts[1], "content": parts[2].strip()})
3268
+ else:
3269
+ matches.append({"raw": line})
3270
+ results[repo_name] = matches
3271
+
3272
+ return json.dumps(results, ensure_ascii=False)
3273
+
3274
+ return json.dumps({"error": f"Unknown tool: {name}"})
3275
+
3276
+
3277
+ def cmd_mcp(args):
3278
+ """Run as MCP server over stdio (JSON-RPC 2.0)."""
3279
+ code_dir = Path(args.path).expanduser()
3280
+
3281
+ def _respond(msg_id, result):
3282
+ resp = {"jsonrpc": "2.0", "id": msg_id, "result": result}
3283
+ sys.stdout.write(json.dumps(resp) + "\n")
3284
+ sys.stdout.flush()
3285
+
3286
+ def _error(msg_id, code, message):
3287
+ resp = {"jsonrpc": "2.0", "id": msg_id, "error": {"code": code, "message": message}}
3288
+ sys.stdout.write(json.dumps(resp) + "\n")
3289
+ sys.stdout.flush()
3290
+
3291
+ for line in sys.stdin:
3292
+ line = line.strip()
3293
+ if not line:
3294
+ continue
3295
+ try:
3296
+ msg = json.loads(line)
3297
+ except json.JSONDecodeError:
3298
+ continue
3299
+
3300
+ method = msg.get("method")
3301
+ msg_id = msg.get("id")
3302
+ params = msg.get("params", {})
3303
+
3304
+ # Notifications (no id) — just acknowledge silently
3305
+ if msg_id is None:
3306
+ continue
3307
+
3308
+ if method == "initialize":
3309
+ _respond(msg_id, {
3310
+ "protocolVersion": "2024-11-05",
3311
+ "capabilities": {"tools": {}},
3312
+ "serverInfo": {"name": "codeboard", "version": __version__},
3313
+ })
3314
+
3315
+ elif method == "ping":
3316
+ _respond(msg_id, {})
3317
+
3318
+ elif method == "tools/list":
3319
+ _respond(msg_id, {"tools": _MCP_TOOLS})
3320
+
3321
+ elif method == "tools/call":
3322
+ tool_name = params.get("name", "")
3323
+ tool_args = params.get("arguments", {})
3324
+ try:
3325
+ result_text = _mcp_handle_tool(tool_name, tool_args, code_dir)
3326
+ _respond(msg_id, {
3327
+ "content": [{"type": "text", "text": result_text}],
3328
+ })
3329
+ except Exception as e:
3330
+ _respond(msg_id, {
3331
+ "content": [{"type": "text", "text": f"Error: {e}"}],
3332
+ "isError": True,
3333
+ })
3334
+
3335
+ else:
3336
+ _error(msg_id, -32601, f"Method not found: {method}")
3337
+
3338
+
3059
3339
  def main():
3060
3340
  global _ui_lang, console
3061
3341
 
@@ -3124,6 +3404,8 @@ def main():
3124
3404
  comp_parser = sub.add_parser("completions", help="Generate shell completion script")
3125
3405
  comp_parser.add_argument("shell", nargs="?", default="bash", choices=["bash", "zsh", "fish"], help="Shell type (default: bash)")
3126
3406
 
3407
+ sub.add_parser("mcp", help="Run as MCP server (stdio, for AI assistants)")
3408
+
3127
3409
  args = parser.parse_args()
3128
3410
 
3129
3411
  # Apply --lang and --no-color
@@ -3152,6 +3434,7 @@ def main():
3152
3434
  "graph": cmd_graph,
3153
3435
  "config": cmd_config,
3154
3436
  "completions": cmd_completions,
3437
+ "mcp": cmd_mcp,
3155
3438
  }
3156
3439
 
3157
3440
  handler = commands.get(cmd)
@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
4
4
 
5
5
  [project]
6
6
  name = "codeboard"
7
- version = "0.1.0"
7
+ version = "0.2.0"
8
8
  description = "Git repository dashboard for your local codebase"
9
9
  readme = "README.md"
10
10
  license = "MIT"
File without changes
File without changes