progi 0.1.0__py3-none-any.whl
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.
- progi/__init__.py +3 -0
- progi/cli.py +80 -0
- progi/config.py +63 -0
- progi/db.py +1058 -0
- progi/logging_setup.py +28 -0
- progi/mcp_server.py +200 -0
- progi/models.py +124 -0
- progi/prompts/playbook.md +40 -0
- progi/prompts/workflow_skeleton.md +103 -0
- progi/seed.py +397 -0
- progi/web/__init__.py +1 -0
- progi/web/app.py +42 -0
- progi/web/routers/__init__.py +22 -0
- progi/web/routers/board.py +56 -0
- progi/web/routers/workflows.py +95 -0
- progi/web/static/app.js +293 -0
- progi/web/static/style.css +2 -0
- progi/web/static/vendor/alpine-ajax.min.js +1 -0
- progi/web/static/vendor/alpine.min.js +5 -0
- progi/web/static/vendor/marked.min.js +79 -0
- progi/web/static/vendor/mermaid.min.js +3405 -0
- progi/web/templates/base.html +65 -0
- progi/web/templates/base_partial.html +3 -0
- progi/web/templates/pages/board.html +44 -0
- progi/web/templates/pages/workflows.html +136 -0
- progi/web/templates/partials/board.html +85 -0
- progi/web/templates/partials/step_detail.html +196 -0
- progi/web/templates/partials/task_detail.html +116 -0
- progi-0.1.0.dist-info/METADATA +123 -0
- progi-0.1.0.dist-info/RECORD +33 -0
- progi-0.1.0.dist-info/WHEEL +4 -0
- progi-0.1.0.dist-info/entry_points.txt +3 -0
- progi-0.1.0.dist-info/licenses/LICENSE +21 -0
progi/__init__.py
ADDED
progi/cli.py
ADDED
|
@@ -0,0 +1,80 @@
|
|
|
1
|
+
"""Command-line entry point for `progi`.
|
|
2
|
+
|
|
3
|
+
Run modes
|
|
4
|
+
---------
|
|
5
|
+
progi -> MCP server (stdio) + web UI (background thread)
|
|
6
|
+
progi --no-web -> MCP server only
|
|
7
|
+
progi-web -> web UI only (separate entry point, see web.app:main)
|
|
8
|
+
|
|
9
|
+
Why the web server runs in a background daemon thread in bundled mode:
|
|
10
|
+
the MCP server speaks the protocol over stdout and must run in the foreground.
|
|
11
|
+
uvicorn is started on a separate thread with logging pinned to stderr so it
|
|
12
|
+
never writes to stdout and corrupts the MCP stream. The thread is a daemon, so
|
|
13
|
+
it exits when the MCP server (foreground) exits.
|
|
14
|
+
|
|
15
|
+
Flags override environment variables; environment variables override defaults.
|
|
16
|
+
"""
|
|
17
|
+
|
|
18
|
+
from __future__ import annotations
|
|
19
|
+
|
|
20
|
+
import argparse
|
|
21
|
+
import threading
|
|
22
|
+
|
|
23
|
+
from . import mcp_server
|
|
24
|
+
from .config import Config, load_config
|
|
25
|
+
from .logging_setup import configure_logging
|
|
26
|
+
|
|
27
|
+
|
|
28
|
+
def _start_web_in_thread(cfg: Config) -> threading.Thread:
|
|
29
|
+
import uvicorn
|
|
30
|
+
|
|
31
|
+
from .web.app import app
|
|
32
|
+
|
|
33
|
+
app.state.cfg = cfg
|
|
34
|
+
server = uvicorn.Server(
|
|
35
|
+
uvicorn.Config(
|
|
36
|
+
app,
|
|
37
|
+
host=cfg.web_host,
|
|
38
|
+
port=cfg.web_port,
|
|
39
|
+
# Keep uvicorn quiet on stdout; warnings/errors go to stderr via our
|
|
40
|
+
# logging config. log_config=None prevents uvicorn from installing
|
|
41
|
+
# its own stdout handlers.
|
|
42
|
+
log_config=None,
|
|
43
|
+
log_level="warning",
|
|
44
|
+
)
|
|
45
|
+
)
|
|
46
|
+
thread = threading.Thread(target=server.run, name="progi-web", daemon=True)
|
|
47
|
+
thread.start()
|
|
48
|
+
return thread
|
|
49
|
+
|
|
50
|
+
|
|
51
|
+
def main() -> None:
|
|
52
|
+
parser = argparse.ArgumentParser(prog="progi", description="progi MCP server + web UI")
|
|
53
|
+
parser.add_argument(
|
|
54
|
+
"--no-web",
|
|
55
|
+
action="store_true",
|
|
56
|
+
help="Run only the MCP server (skip the web UI).",
|
|
57
|
+
)
|
|
58
|
+
parser.add_argument("--web-host", default=None, help="Override web bind host.")
|
|
59
|
+
parser.add_argument("--web-port", type=int, default=None, help="Override web port.")
|
|
60
|
+
args = parser.parse_args()
|
|
61
|
+
|
|
62
|
+
base = load_config()
|
|
63
|
+
cfg = Config(
|
|
64
|
+
db_path=base.db_path,
|
|
65
|
+
web_host=args.web_host or base.web_host,
|
|
66
|
+
web_port=args.web_port or base.web_port,
|
|
67
|
+
no_web=args.no_web or base.no_web,
|
|
68
|
+
)
|
|
69
|
+
|
|
70
|
+
configure_logging()
|
|
71
|
+
|
|
72
|
+
if not cfg.no_web:
|
|
73
|
+
_start_web_in_thread(cfg)
|
|
74
|
+
|
|
75
|
+
# Foreground: MCP server over stdio. Blocks until the client disconnects.
|
|
76
|
+
mcp_server.run(cfg)
|
|
77
|
+
|
|
78
|
+
|
|
79
|
+
if __name__ == "__main__":
|
|
80
|
+
main()
|
progi/config.py
ADDED
|
@@ -0,0 +1,63 @@
|
|
|
1
|
+
"""Runtime configuration, resolved from environment variables with sane defaults.
|
|
2
|
+
|
|
3
|
+
All configuration funnels through here so the MCP server, the web app, and the
|
|
4
|
+
CLI agree on the same values (database location, web bind address, etc.).
|
|
5
|
+
"""
|
|
6
|
+
|
|
7
|
+
from __future__ import annotations
|
|
8
|
+
|
|
9
|
+
import os
|
|
10
|
+
from dataclasses import dataclass
|
|
11
|
+
from pathlib import Path
|
|
12
|
+
|
|
13
|
+
try:
|
|
14
|
+
# platformdirs gives us OS-appropriate data locations
|
|
15
|
+
# (~/.local/share/progi on Linux, ~/Library/Application Support/progi on
|
|
16
|
+
# macOS, %LOCALAPPDATA%\progi on Windows).
|
|
17
|
+
from platformdirs import user_data_dir
|
|
18
|
+
|
|
19
|
+
_DEFAULT_DATA_DIR = Path(user_data_dir("progi", appauthor=False))
|
|
20
|
+
except Exception: # pragma: no cover - platformdirs is a hard dependency, but be safe
|
|
21
|
+
_DEFAULT_DATA_DIR = Path.home() / ".progi"
|
|
22
|
+
|
|
23
|
+
|
|
24
|
+
def _env_bool(name: str, default: bool = False) -> bool:
|
|
25
|
+
raw = os.environ.get(name)
|
|
26
|
+
if raw is None:
|
|
27
|
+
return default
|
|
28
|
+
return raw.strip().lower() in {"1", "true", "yes", "on"}
|
|
29
|
+
|
|
30
|
+
|
|
31
|
+
def _resolve_db_path() -> Path:
|
|
32
|
+
"""Where the SQLite file lives. Override with PROGI_DB_PATH."""
|
|
33
|
+
override = os.environ.get("PROGI_DB_PATH")
|
|
34
|
+
if override:
|
|
35
|
+
return Path(override).expanduser().resolve()
|
|
36
|
+
return (_DEFAULT_DATA_DIR / "progi.db").resolve()
|
|
37
|
+
|
|
38
|
+
|
|
39
|
+
@dataclass(frozen=True)
|
|
40
|
+
class Config:
|
|
41
|
+
db_path: Path
|
|
42
|
+
web_host: str
|
|
43
|
+
web_port: int
|
|
44
|
+
# When True, the bundled run mode skips starting the web server.
|
|
45
|
+
no_web: bool
|
|
46
|
+
|
|
47
|
+
@property
|
|
48
|
+
def sqlalchemy_url(self) -> str:
|
|
49
|
+
# SQLite URL. The path is absolute so it does not depend on CWD.
|
|
50
|
+
return f"sqlite:///{self.db_path}"
|
|
51
|
+
|
|
52
|
+
def ensure_dirs(self) -> None:
|
|
53
|
+
self.db_path.parent.mkdir(parents=True, exist_ok=True)
|
|
54
|
+
|
|
55
|
+
|
|
56
|
+
def load_config() -> Config:
|
|
57
|
+
cfg = Config(
|
|
58
|
+
db_path=_resolve_db_path(),
|
|
59
|
+
web_host=os.environ.get("PROGI_WEB_HOST", "127.0.0.1"),
|
|
60
|
+
web_port=int(os.environ.get("PROGI_WEB_PORT", "8000")),
|
|
61
|
+
no_web=_env_bool("PROGI_NO_WEB", default=False),
|
|
62
|
+
)
|
|
63
|
+
return cfg
|