kanbot 0.1.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.
@@ -0,0 +1,12 @@
1
+ .venv/
2
+ __pycache__/
3
+ *.pyc
4
+ *.db
5
+ *.db-wal
6
+ *.db-shm
7
+ .deckhand/
8
+ dist/
9
+ build/
10
+ *.egg-info/
11
+ .vercel
12
+ .vercel/
kanbot-0.1.0/PKG-INFO ADDED
@@ -0,0 +1,145 @@
1
+ Metadata-Version: 2.4
2
+ Name: kanbot
3
+ Version: 0.1.0
4
+ Summary: A Kanban board where every card is a task run by your local CLI coding agents (Claude, Codex, Gemini, GLM/ZAI, or any CLI you define).
5
+ Project-URL: Homepage, https://github.com/yourname/deckhand
6
+ Author: Deckhand
7
+ License: MIT
8
+ Keywords: agents,automation,claude,cli,codex,kanban
9
+ Requires-Python: >=3.9
10
+ Requires-Dist: fastapi>=0.110
11
+ Requires-Dist: httpx>=0.27
12
+ Requires-Dist: rich>=13.0
13
+ Requires-Dist: uvicorn[standard]>=0.27
14
+ Requires-Dist: websockets>=12.0
15
+ Description-Content-Type: text/markdown
16
+
17
+ # KanBot
18
+
19
+ **A visual control room for your coding-agent TUIs — and a Kanban board where every card is a task run by them.**
20
+
21
+ You run a lot of terminal coding agents (Claude Code, Codex, …). KanBot gives you
22
+ one screen to *see what every session is doing*, pick any of them back up, and
23
+ drop new tasks that agents run for you — with live logs streamed straight to the
24
+ card.
25
+
26
+ Two things in one board:
27
+
28
+ 1. **Track your TUIs.** A background runner watches each agent's local session
29
+ store and surfaces every session as a card: the project, the latest message,
30
+ how many turns, how long it's been brewing, and whether it's **working right
31
+ now**. Sessions flow by recency — working → **Running**, just-finished →
32
+ **Done**, older → **Backlog**.
33
+ 2. **Run new tasks.** Drop a card, pick an agent, and the runner executes it and
34
+ streams stdout/stderr to the card. Or drag any tracked session into **Running**
35
+ to resume it (`claude --resume`, `codex exec resume`).
36
+
37
+ ```
38
+ Backlog Running Review Done
39
+ (stale sessions (sessions (your (recently
40
+ + new tasks) working now finished finished
41
+ + running tasks) tasks) sessions)
42
+ │ drag → Running, or "Run", queues for a runner
43
+
44
+ ╔══════════════════════════════╗
45
+ ║ kanbot runner (background) ║ detects claude · codex · gemini · glm · shell
46
+ ║ watches ~/.claude, ~/.codex ║ executes & resumes, streams logs back
47
+ ╚══════════════════════════════╝
48
+ ```
49
+
50
+ ## Quickstart
51
+
52
+ Easiest (isolated, sidesteps Homebrew's PEP 668 `externally-managed` error):
53
+
54
+ ```bash
55
+ pipx install kanbot && kanbot up # or zero-install: uvx kanbot up
56
+ ```
57
+
58
+ From source (until it's on PyPI):
59
+
60
+ ```bash
61
+ python3 -m venv .venv && . .venv/bin/activate
62
+ pip install -e .
63
+ kanbot up # server + local runner, board at :8787
64
+ ```
65
+
66
+ > Don't use bare `pip install` on macOS Homebrew Python — it errors with
67
+ > `externally-managed-environment` (PEP 668). `pipx`/`uv` handle the env for you.
68
+
69
+ The board immediately fills with your recent Claude/Codex sessions. Click any one
70
+ to see its recent transcript in a terminal view and **resume** it; or hit
71
+ **+ add task** to give an agent fresh work.
72
+
73
+ Run the pieces separately (e.g. runner on another machine):
74
+
75
+ ```bash
76
+ kanbot server # the board / API
77
+ kanbot runner --server http://HOST:8787 --name gpu-box
78
+ ```
79
+
80
+ ## Tracking other agents (Hermes, OpenCode, your own…)
81
+
82
+ Claude Code and Codex are tracked out of the box. Any agent that logs
83
+ newline-delimited JSON transcripts can be added with **no code change** — point
84
+ KanBot at its store in `~/.kanbot/config.json` (a.k.a. `~/.kanbot/config.json`):
85
+
86
+ ```json
87
+ {
88
+ "discovery_sources": [
89
+ {
90
+ "name": "hermes",
91
+ "label": "Hermes",
92
+ "root": "~/.hermes/sessions",
93
+ "pattern": "*.jsonl",
94
+ "recursive": true,
95
+ "fmt": "claude"
96
+ }
97
+ ]
98
+ }
99
+ ```
100
+
101
+ - `fmt`: `"claude"` for flat records (`{type, message, cwd, timestamp}`) or
102
+ `"codex"` for payload-nested records (`{payload: {role, content, cwd}}`).
103
+ - `kanbot agents` shows which trackers are active and where they read from.
104
+
105
+ ## Run agents
106
+
107
+ `kanbot agents` lists the CLIs detected on this machine. Built-in catalog:
108
+
109
+ | agent | run | resume |
110
+ |-------|-----|--------|
111
+ | `claude` | `claude -p "<prompt>"` | `claude --resume <id> -p "<prompt>"` |
112
+ | `codex` | `codex exec --full-auto "<prompt>"` | `codex exec resume <id> "<prompt>"` |
113
+ | `gemini` | `gemini -y -p "<prompt>"` | — |
114
+ | `glm` | Claude Code w/ `ANTHROPIC_BASE_URL=https://api.z.ai/api/anthropic` | ✓ |
115
+ | `opencode`, `aider`, `cursor-agent`, `hermes`, `shell` | see `kanbot/agents.py` | — |
116
+
117
+ Override or add any agent's command in `~/.kanbot/config.json` →
118
+ `agent_overrides`. A card set to `auto` runs on whatever the matched runner has.
119
+
120
+ > Note: built-in run commands use auto-approve flags so tasks run unattended.
121
+ > Review `kanbot/agents.py` and dial them back if you want a human in the loop.
122
+
123
+ ## Tags & insights
124
+
125
+ Tags are colored labels; a tag can also be an **insight provider** (◆) that pulls
126
+ live context onto any card: **git** (branch/diff), **files** (recent changes), or
127
+ a **custom command** (e.g. `pytest -q`).
128
+
129
+ ## CLI
130
+
131
+ ```
132
+ kanbot up server + local runner (best first run)
133
+ kanbot server board / API only
134
+ kanbot runner background runner only (--server, --name, --concurrency)
135
+ kanbot agents detected agents + active session trackers
136
+ kanbot config server URL, token, runner name, enable/disable agents
137
+ kanbot open open the board
138
+ ```
139
+
140
+ Config: `~/.kanbot/config.json` · data: `~/.kanbot/kanbot.db`.
141
+ Set `KANBOT_TOKEN` on the server to require a matching `--token` from runners.
142
+
143
+ ## License
144
+
145
+ MIT
kanbot-0.1.0/README.md ADDED
@@ -0,0 +1,129 @@
1
+ # KanBot
2
+
3
+ **A visual control room for your coding-agent TUIs — and a Kanban board where every card is a task run by them.**
4
+
5
+ You run a lot of terminal coding agents (Claude Code, Codex, …). KanBot gives you
6
+ one screen to *see what every session is doing*, pick any of them back up, and
7
+ drop new tasks that agents run for you — with live logs streamed straight to the
8
+ card.
9
+
10
+ Two things in one board:
11
+
12
+ 1. **Track your TUIs.** A background runner watches each agent's local session
13
+ store and surfaces every session as a card: the project, the latest message,
14
+ how many turns, how long it's been brewing, and whether it's **working right
15
+ now**. Sessions flow by recency — working → **Running**, just-finished →
16
+ **Done**, older → **Backlog**.
17
+ 2. **Run new tasks.** Drop a card, pick an agent, and the runner executes it and
18
+ streams stdout/stderr to the card. Or drag any tracked session into **Running**
19
+ to resume it (`claude --resume`, `codex exec resume`).
20
+
21
+ ```
22
+ Backlog Running Review Done
23
+ (stale sessions (sessions (your (recently
24
+ + new tasks) working now finished finished
25
+ + running tasks) tasks) sessions)
26
+ │ drag → Running, or "Run", queues for a runner
27
+
28
+ ╔══════════════════════════════╗
29
+ ║ kanbot runner (background) ║ detects claude · codex · gemini · glm · shell
30
+ ║ watches ~/.claude, ~/.codex ║ executes & resumes, streams logs back
31
+ ╚══════════════════════════════╝
32
+ ```
33
+
34
+ ## Quickstart
35
+
36
+ Easiest (isolated, sidesteps Homebrew's PEP 668 `externally-managed` error):
37
+
38
+ ```bash
39
+ pipx install kanbot && kanbot up # or zero-install: uvx kanbot up
40
+ ```
41
+
42
+ From source (until it's on PyPI):
43
+
44
+ ```bash
45
+ python3 -m venv .venv && . .venv/bin/activate
46
+ pip install -e .
47
+ kanbot up # server + local runner, board at :8787
48
+ ```
49
+
50
+ > Don't use bare `pip install` on macOS Homebrew Python — it errors with
51
+ > `externally-managed-environment` (PEP 668). `pipx`/`uv` handle the env for you.
52
+
53
+ The board immediately fills with your recent Claude/Codex sessions. Click any one
54
+ to see its recent transcript in a terminal view and **resume** it; or hit
55
+ **+ add task** to give an agent fresh work.
56
+
57
+ Run the pieces separately (e.g. runner on another machine):
58
+
59
+ ```bash
60
+ kanbot server # the board / API
61
+ kanbot runner --server http://HOST:8787 --name gpu-box
62
+ ```
63
+
64
+ ## Tracking other agents (Hermes, OpenCode, your own…)
65
+
66
+ Claude Code and Codex are tracked out of the box. Any agent that logs
67
+ newline-delimited JSON transcripts can be added with **no code change** — point
68
+ KanBot at its store in `~/.kanbot/config.json` (a.k.a. `~/.kanbot/config.json`):
69
+
70
+ ```json
71
+ {
72
+ "discovery_sources": [
73
+ {
74
+ "name": "hermes",
75
+ "label": "Hermes",
76
+ "root": "~/.hermes/sessions",
77
+ "pattern": "*.jsonl",
78
+ "recursive": true,
79
+ "fmt": "claude"
80
+ }
81
+ ]
82
+ }
83
+ ```
84
+
85
+ - `fmt`: `"claude"` for flat records (`{type, message, cwd, timestamp}`) or
86
+ `"codex"` for payload-nested records (`{payload: {role, content, cwd}}`).
87
+ - `kanbot agents` shows which trackers are active and where they read from.
88
+
89
+ ## Run agents
90
+
91
+ `kanbot agents` lists the CLIs detected on this machine. Built-in catalog:
92
+
93
+ | agent | run | resume |
94
+ |-------|-----|--------|
95
+ | `claude` | `claude -p "<prompt>"` | `claude --resume <id> -p "<prompt>"` |
96
+ | `codex` | `codex exec --full-auto "<prompt>"` | `codex exec resume <id> "<prompt>"` |
97
+ | `gemini` | `gemini -y -p "<prompt>"` | — |
98
+ | `glm` | Claude Code w/ `ANTHROPIC_BASE_URL=https://api.z.ai/api/anthropic` | ✓ |
99
+ | `opencode`, `aider`, `cursor-agent`, `hermes`, `shell` | see `kanbot/agents.py` | — |
100
+
101
+ Override or add any agent's command in `~/.kanbot/config.json` →
102
+ `agent_overrides`. A card set to `auto` runs on whatever the matched runner has.
103
+
104
+ > Note: built-in run commands use auto-approve flags so tasks run unattended.
105
+ > Review `kanbot/agents.py` and dial them back if you want a human in the loop.
106
+
107
+ ## Tags & insights
108
+
109
+ Tags are colored labels; a tag can also be an **insight provider** (◆) that pulls
110
+ live context onto any card: **git** (branch/diff), **files** (recent changes), or
111
+ a **custom command** (e.g. `pytest -q`).
112
+
113
+ ## CLI
114
+
115
+ ```
116
+ kanbot up server + local runner (best first run)
117
+ kanbot server board / API only
118
+ kanbot runner background runner only (--server, --name, --concurrency)
119
+ kanbot agents detected agents + active session trackers
120
+ kanbot config server URL, token, runner name, enable/disable agents
121
+ kanbot open open the board
122
+ ```
123
+
124
+ Config: `~/.kanbot/config.json` · data: `~/.kanbot/kanbot.db`.
125
+ Set `KANBOT_TOKEN` on the server to require a matching `--token` from runners.
126
+
127
+ ## License
128
+
129
+ MIT
@@ -0,0 +1,3 @@
1
+ """Deckhand — a Kanban board where every card is a task run by your local CLI coding agents."""
2
+
3
+ __version__ = "0.1.0"
@@ -0,0 +1,6 @@
1
+ import sys
2
+
3
+ from .cli import main
4
+
5
+ if __name__ == "__main__":
6
+ sys.exit(main())
@@ -0,0 +1,134 @@
1
+ """Built-in CLI agent catalog, shared by the server (display) and runner (execution).
2
+
3
+ Each agent is defined declaratively so adding "whatever else is available in the
4
+ CLI" is a one-liner here, or a config override on the runner side. The runner
5
+ detects which `bin` are on PATH and only advertises those it finds.
6
+
7
+ Command templates use Python str.format with:
8
+ {prompt} -> the task prompt (already shell-safe; passed as a single argv item)
9
+ The command is a list of argv tokens; the runner substitutes {prompt} per-token
10
+ so no shell quoting is needed.
11
+ """
12
+ from __future__ import annotations
13
+
14
+ from dataclasses import dataclass, field
15
+ from typing import Dict, List, Optional
16
+
17
+
18
+ @dataclass
19
+ class AgentSpec:
20
+ name: str # stable id, e.g. "claude"
21
+ label: str # display name
22
+ bin: str # executable to look for on PATH
23
+ argv: List[str] # argv template; tokens may contain {prompt}
24
+ description: str = ""
25
+ env: Dict[str, str] = field(default_factory=dict)
26
+ color: str = "#8b5cf6"
27
+ # argv template to resume/continue an existing agent session. Tokens may
28
+ # contain {prompt} and {session_id}. Empty => resume not supported.
29
+ resume_argv: List[str] = field(default_factory=list)
30
+
31
+
32
+ # Non-interactive / headless invocations for each known coding CLI.
33
+ BUILTIN_AGENTS: List[AgentSpec] = [
34
+ AgentSpec(
35
+ name="claude",
36
+ label="Claude Code",
37
+ bin="claude",
38
+ argv=["claude", "-p", "{prompt}", "--dangerously-skip-permissions"],
39
+ resume_argv=["claude", "--resume", "{session_id}", "-p", "{prompt}",
40
+ "--dangerously-skip-permissions"],
41
+ description="Anthropic Claude Code in headless print mode.",
42
+ color="#d97757",
43
+ ),
44
+ AgentSpec(
45
+ name="codex",
46
+ label="Codex",
47
+ bin="codex",
48
+ argv=["codex", "exec", "--sandbox", "workspace-write",
49
+ "--skip-git-repo-check", "{prompt}"],
50
+ resume_argv=["codex", "exec", "resume", "--skip-git-repo-check",
51
+ "{session_id}", "{prompt}"],
52
+ description="OpenAI Codex CLI, non-interactive exec (workspace-write sandbox).",
53
+ color="#10a37f",
54
+ ),
55
+ AgentSpec(
56
+ name="gemini",
57
+ label="Gemini CLI",
58
+ bin="gemini",
59
+ argv=["gemini", "-y", "-p", "{prompt}"],
60
+ description="Google Gemini CLI in YOLO/auto mode.",
61
+ color="#4285f4",
62
+ ),
63
+ AgentSpec(
64
+ name="glm",
65
+ label="GLM / Z.ai",
66
+ bin="claude",
67
+ argv=["claude", "-p", "{prompt}", "--dangerously-skip-permissions"],
68
+ resume_argv=["claude", "--resume", "{session_id}", "-p", "{prompt}",
69
+ "--dangerously-skip-permissions"],
70
+ description="Z.ai GLM coding plan via Claude Code (set ANTHROPIC_BASE_URL).",
71
+ env={"ANTHROPIC_BASE_URL": "https://api.z.ai/api/anthropic"},
72
+ color="#2563eb",
73
+ ),
74
+ AgentSpec(
75
+ name="opencode",
76
+ label="OpenCode",
77
+ bin="opencode",
78
+ argv=["opencode", "run", "{prompt}"],
79
+ description="OpenCode terminal agent, non-interactive run.",
80
+ color="#f59e0b",
81
+ ),
82
+ AgentSpec(
83
+ name="hermes",
84
+ label="Hermes",
85
+ bin="hermes",
86
+ argv=["hermes", "-p", "{prompt}"],
87
+ description="Hermes coding agent (best-effort; override argv in config if it differs).",
88
+ color="#e879f9",
89
+ ),
90
+ AgentSpec(
91
+ name="aider",
92
+ label="Aider",
93
+ bin="aider",
94
+ argv=["aider", "--yes", "--no-auto-commits", "--message", "{prompt}"],
95
+ description="Aider pair-programmer, single message mode.",
96
+ color="#22c55e",
97
+ ),
98
+ AgentSpec(
99
+ name="cursor-agent",
100
+ label="Cursor Agent",
101
+ bin="cursor-agent",
102
+ argv=["cursor-agent", "-p", "{prompt}"],
103
+ description="Cursor CLI agent in print mode.",
104
+ color="#000000",
105
+ ),
106
+ AgentSpec(
107
+ name="shell",
108
+ label="Shell command",
109
+ bin="bash",
110
+ argv=["bash", "-lc", "{prompt}"],
111
+ description="Run the prompt as a raw shell command. Always available.",
112
+ color="#64748b",
113
+ ),
114
+ ]
115
+
116
+ BUILTIN_BY_NAME: Dict[str, AgentSpec] = {a.name: a for a in BUILTIN_AGENTS}
117
+
118
+
119
+ def builtin_names() -> List[str]:
120
+ return [a.name for a in BUILTIN_AGENTS]
121
+
122
+
123
+ def spec_to_dict(a: AgentSpec) -> dict:
124
+ return {
125
+ "name": a.name,
126
+ "label": a.label,
127
+ "bin": a.bin,
128
+ "description": a.description,
129
+ "color": a.color,
130
+ }
131
+
132
+
133
+ def catalog() -> List[dict]:
134
+ return [spec_to_dict(a) for a in BUILTIN_AGENTS]
@@ -0,0 +1,260 @@
1
+ """KanBot command-line interface.
2
+
3
+ kanbot up # start server + a local runner together (best first run)
4
+ kanbot server # just the web server / API / board
5
+ kanbot runner # just the background runner (connects to a server)
6
+ kanbot agents # show which CLI coding agents are detected here
7
+ kanbot config # view / set server URL, token, runner name
8
+ kanbot open # open the board in your browser
9
+ """
10
+ from __future__ import annotations
11
+
12
+ import argparse
13
+ import asyncio
14
+ import sys
15
+ import threading
16
+ import time
17
+ import webbrowser
18
+
19
+ from . import __version__
20
+ from .config import Config, config_path, db_path
21
+
22
+
23
+ def _rich():
24
+ try:
25
+ from rich.console import Console
26
+ return Console()
27
+ except Exception:
28
+ return None
29
+
30
+
31
+ def cmd_server(args) -> int:
32
+ import uvicorn
33
+ from .server.app import create_app
34
+
35
+ app = create_app(db_path=args.db)
36
+ url = f"http://{args.host}:{args.port}"
37
+ print(f"KanBot server v{__version__} → {url}")
38
+ print(f" db: {args.db or db_path()}")
39
+ print(f" open: {url} (then run `kanbot runner` on any machine)")
40
+ uvicorn.run(app, host=args.host, port=args.port, log_level=args.log_level)
41
+ return 0
42
+
43
+
44
+ def cmd_runner(args) -> int:
45
+ from .runner.worker import Runner
46
+
47
+ cfg = Config.load()
48
+ if args.server:
49
+ cfg.server_url = args.server
50
+ if args.token:
51
+ cfg.token = args.token
52
+ if args.name:
53
+ cfg.runner_name = args.name
54
+ if args.concurrency:
55
+ cfg.max_concurrency = args.concurrency
56
+ cfg.save()
57
+
58
+ runner = Runner(cfg)
59
+ try:
60
+ asyncio.run(runner.run_forever())
61
+ except KeyboardInterrupt:
62
+ print("\nrunner stopped.")
63
+ return 0
64
+
65
+
66
+ def cmd_up(args) -> int:
67
+ """Start the server in-process and attach a local runner. One command demo."""
68
+ import uvicorn
69
+ from .runner.worker import Runner
70
+ from .server.app import create_app
71
+
72
+ app = create_app(db_path=args.db)
73
+ config = uvicorn.Config(app, host=args.host, port=args.port, log_level="warning")
74
+ server = uvicorn.Server(config)
75
+
76
+ def serve():
77
+ asyncio.run(server.serve())
78
+
79
+ t = threading.Thread(target=serve, daemon=True)
80
+ t.start()
81
+
82
+ # wait for the server to come up
83
+ import httpx
84
+ base = f"http://{args.host}:{args.port}"
85
+ for _ in range(50):
86
+ try:
87
+ httpx.get(base + "/api/health", timeout=0.5)
88
+ break
89
+ except Exception:
90
+ time.sleep(0.1)
91
+
92
+ print(f"KanBot is up → {base}")
93
+ if not args.no_open:
94
+ try:
95
+ webbrowser.open(base)
96
+ except Exception:
97
+ pass
98
+
99
+ cfg = Config.load()
100
+ cfg.server_url = base
101
+ if args.name:
102
+ cfg.runner_name = args.name
103
+ if args.concurrency:
104
+ cfg.max_concurrency = args.concurrency
105
+ cfg.save()
106
+ runner = Runner(cfg)
107
+ print(f"Local runner '{cfg.runner_name}' attaching with agents: "
108
+ f"{', '.join(runner.agents) or '(none — install claude/codex/etc.)'}")
109
+ print("Press Ctrl-C to stop.\n")
110
+ try:
111
+ asyncio.run(runner.run_forever())
112
+ except KeyboardInterrupt:
113
+ print("\nshutting down.")
114
+ server.should_exit = True
115
+ return 0
116
+
117
+
118
+ def cmd_agents(args) -> int:
119
+ from .runner.agents import detect_agents
120
+ from .agents import BUILTIN_AGENTS
121
+
122
+ cfg = Config.load()
123
+ found = detect_agents(cfg)
124
+ console = _rich()
125
+ if console:
126
+ from rich.table import Table
127
+ table = Table(title="CLI agents on this machine")
128
+ table.add_column("agent")
129
+ table.add_column("status")
130
+ table.add_column("description")
131
+ for spec in BUILTIN_AGENTS:
132
+ ok = spec.name in found
133
+ disabled = spec.name in cfg.disabled_agents
134
+ status = "[green]available[/green]" if ok else (
135
+ "[yellow]disabled[/yellow]" if disabled else "[dim]not found[/dim]")
136
+ table.add_row(spec.name, status, spec.description)
137
+ console.print(table)
138
+ else:
139
+ for spec in BUILTIN_AGENTS:
140
+ mark = "✓" if spec.name in found else "·"
141
+ print(f" {mark} {spec.name:14} {spec.description}")
142
+ print(f"\nadvertised capabilities: {', '.join(found) or '(none)'}")
143
+
144
+ # Session trackers: which TUIs KanBot can see / revive.
145
+ from .runner.discovery import active_providers, builtin_providers
146
+ trackers = active_providers(cfg.discovery_sources)
147
+ active_names = {t["name"] for t in trackers}
148
+ print("\nsession trackers (TUIs KanBot watches):")
149
+ for p in builtin_providers():
150
+ mark = "✓" if p.name in active_names else "·"
151
+ state = "tracking" if p.name in active_names else "no sessions found"
152
+ print(f" {mark} {p.label:14} {p.root} [{state}]")
153
+ for t in trackers:
154
+ if t["name"] not in ("claude", "codex"):
155
+ print(f" ✓ {t['label']:14} {t['root']} [custom]")
156
+ print("\nTrack another agent: add to discovery_sources in "
157
+ f"{config_path()}, e.g.\n"
158
+ ' {"name": "hermes", "label": "Hermes", "root": "~/.hermes/sessions",\n'
159
+ ' "pattern": "*.jsonl", "recursive": true, "fmt": "claude"}')
160
+ return 0
161
+
162
+
163
+ def cmd_config(args) -> int:
164
+ cfg = Config.load()
165
+ changed = False
166
+ if args.server:
167
+ cfg.server_url = args.server; changed = True
168
+ if args.token is not None:
169
+ cfg.token = args.token; changed = True
170
+ if args.name:
171
+ cfg.runner_name = args.name; changed = True
172
+ if args.concurrency:
173
+ cfg.max_concurrency = args.concurrency; changed = True
174
+ if args.disable:
175
+ for a in args.disable:
176
+ if a not in cfg.disabled_agents:
177
+ cfg.disabled_agents.append(a)
178
+ changed = True
179
+ if args.enable:
180
+ cfg.disabled_agents = [a for a in cfg.disabled_agents if a not in args.enable]
181
+ changed = True
182
+ if changed:
183
+ cfg.save()
184
+ print(f"saved {config_path()}")
185
+ print(f"server_url : {cfg.server_url}")
186
+ print(f"token : {'(set)' if cfg.token else '(none)'}")
187
+ print(f"runner_name : {cfg.runner_name}")
188
+ print(f"runner_id : {cfg.runner_id}")
189
+ print(f"max_concurrency : {cfg.max_concurrency}")
190
+ print(f"disabled_agents : {', '.join(cfg.disabled_agents) or '(none)'}")
191
+ return 0
192
+
193
+
194
+ def cmd_open(args) -> int:
195
+ cfg = Config.load()
196
+ url = args.server or cfg.server_url
197
+ print(f"opening {url}")
198
+ webbrowser.open(url)
199
+ return 0
200
+
201
+
202
+ def build_parser() -> argparse.ArgumentParser:
203
+ p = argparse.ArgumentParser(prog="kanbot", description=__doc__,
204
+ formatter_class=argparse.RawDescriptionHelpFormatter)
205
+ p.add_argument("--version", action="version", version=f"kanbot {__version__}")
206
+ sub = p.add_subparsers(dest="cmd")
207
+
208
+ sp = sub.add_parser("up", help="start server + local runner (recommended first run)")
209
+ sp.add_argument("--host", default="127.0.0.1")
210
+ sp.add_argument("--port", type=int, default=8787)
211
+ sp.add_argument("--db", default=None)
212
+ sp.add_argument("--name", default=None, help="runner name")
213
+ sp.add_argument("--concurrency", type=int, default=None)
214
+ sp.add_argument("--no-open", action="store_true", help="don't open the browser")
215
+ sp.set_defaults(func=cmd_up)
216
+
217
+ sp = sub.add_parser("server", help="run the web server / API only")
218
+ sp.add_argument("--host", default="127.0.0.1")
219
+ sp.add_argument("--port", type=int, default=8787)
220
+ sp.add_argument("--db", default=None)
221
+ sp.add_argument("--log-level", default="info")
222
+ sp.set_defaults(func=cmd_server)
223
+
224
+ sp = sub.add_parser("runner", help="run the background runner only")
225
+ sp.add_argument("--server", default=None, help="server URL (e.g. http://host:8787)")
226
+ sp.add_argument("--token", default=None)
227
+ sp.add_argument("--name", default=None)
228
+ sp.add_argument("--concurrency", type=int, default=None)
229
+ sp.set_defaults(func=cmd_runner)
230
+
231
+ sp = sub.add_parser("agents", help="show detected CLI agents")
232
+ sp.set_defaults(func=cmd_agents)
233
+
234
+ sp = sub.add_parser("config", help="view or set configuration")
235
+ sp.add_argument("--server", default=None)
236
+ sp.add_argument("--token", default=None)
237
+ sp.add_argument("--name", default=None)
238
+ sp.add_argument("--concurrency", type=int, default=None)
239
+ sp.add_argument("--disable", nargs="*", help="agent names to disable")
240
+ sp.add_argument("--enable", nargs="*", help="agent names to re-enable")
241
+ sp.set_defaults(func=cmd_config)
242
+
243
+ sp = sub.add_parser("open", help="open the board in a browser")
244
+ sp.add_argument("--server", default=None)
245
+ sp.set_defaults(func=cmd_open)
246
+
247
+ return p
248
+
249
+
250
+ def main(argv=None) -> int:
251
+ parser = build_parser()
252
+ args = parser.parse_args(argv)
253
+ if not getattr(args, "cmd", None):
254
+ parser.print_help()
255
+ return 0
256
+ return args.func(args)
257
+
258
+
259
+ if __name__ == "__main__":
260
+ sys.exit(main())