{"id":50542835,"url":"https://github.com/sepehr071/pm-assistant","last_synced_at":"2026-06-03T21:30:27.572Z","repository":{"id":353767628,"uuid":"1220832398","full_name":"sepehr071/pm-assistant","owner":"sepehr071","description":"Local-first AI copilot for Project Managers. 9 MCP integrations, approval-gated tool calls, proactive rules engine.","archived":false,"fork":false,"pushed_at":"2026-04-25T12:06:28.000Z","size":1227,"stargazers_count":0,"open_issues_count":0,"forks_count":0,"subscribers_count":0,"default_branch":"main","last_synced_at":"2026-04-25T13:31:59.172Z","etag":null,"topics":["agent","ai-assistant","fastapi","llm","local-first","mcp","openrouter","project-management","react","smithery"],"latest_commit_sha":null,"homepage":null,"language":"Python","has_issues":true,"has_wiki":null,"has_pages":null,"mirror_url":null,"source_name":null,"license":"mit","status":null,"scm":"git","pull_requests_enabled":true,"icon_url":"https://github.com/sepehr071.png","metadata":{"files":{"readme":"README.md","changelog":"CHANGELOG.md","contributing":"CONTRIBUTING.md","funding":null,"license":"LICENSE","code_of_conduct":null,"threat_model":null,"audit":null,"citation":null,"codeowners":null,"security":null,"support":null,"governance":null,"roadmap":null,"authors":null,"dei":null,"publiccode":null,"codemeta":null,"zenodo":null,"notice":null,"maintainers":null,"copyright":null,"agents":null,"dco":null,"cla":null}},"created_at":"2026-04-25T11:51:06.000Z","updated_at":"2026-04-25T12:06:33.000Z","dependencies_parsed_at":null,"dependency_job_id":null,"html_url":"https://github.com/sepehr071/pm-assistant","commit_stats":null,"previous_names":["sepehr071/pm-assistant"],"tags_count":null,"template":false,"template_full_name":null,"purl":"pkg:github/sepehr071/pm-assistant","repository_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/sepehr071%2Fpm-assistant","tags_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/sepehr071%2Fpm-assistant/tags","releases_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/sepehr071%2Fpm-assistant/releases","manifests_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/sepehr071%2Fpm-assistant/manifests","owner_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners/sepehr071","download_url":"https://codeload.github.com/sepehr071/pm-assistant/tar.gz/refs/heads/main","sbom_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/sepehr071%2Fpm-assistant/sbom","scorecard":null,"host":{"name":"GitHub","url":"https://github.com","kind":"github","repositories_count":286080680,"owners_count":33881107,"icon_url":"https://github.com/github.png","version":null,"created_at":"2022-05-30T11:31:42.601Z","updated_at":"2026-05-26T15:22:16.424Z","status":"online","status_checked_at":"2026-06-03T02:00:06.370Z","response_time":59,"last_error":null,"robots_txt_status":"success","robots_txt_updated_at":"2025-07-24T06:49:26.215Z","robots_txt_url":"https://github.com/robots.txt","online":true,"can_crawl_api":true,"host_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub","repositories_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories","repository_names_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repository_names","owners_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners"}},"keywords":["agent","ai-assistant","fastapi","llm","local-first","mcp","openrouter","project-management","react","smithery"],"created_at":"2026-06-03T21:30:26.873Z","updated_at":"2026-06-03T21:30:27.563Z","avatar_url":"https://github.com/sepehr071.png","language":"Python","funding_links":[],"categories":[],"sub_categories":[],"readme":"# PM Assistant\n\n\u003e A local-first, single-user AI copilot for Project Managers. Chat with an LLM that can read and act across Jira, GitHub, Slack, Notion, Linear, Gmail, Google Calendar, Figma and Confluence — with every write gated behind an explicit approval click.\n\nPM Assistant is an open-source desktop-style web app you run on your own machine. It streams responses from any model on [OpenRouter](https://openrouter.ai), routes tool calls through [Smithery Connect](https://smithery.ai) (a hosted MCP gateway that handles third-party OAuth), and persists everything to a local SQLite file. There is no multi-tenant cloud, no shared backend, no telemetry. The agent loop is reactive (chat-driven) **and** proactive (natural-language rules that poll on a schedule). A built-in Telegram bridge lets you talk to the same agent from your phone.\n\n---\n\n## Screenshots\n\n| | |\n|---|---|\n| ![Chat surface — first message](docs/screenshots/glass-chat-initial.png) | ![Chained tool calls grouped into one card](docs/screenshots/island-group-open.png) |\n| Chat island with aurora background, glass composer. | Two or more tool calls collapse into a single group card with status rollup. |\n\n![Settings page](docs/screenshots/settings-glass.png)\n\n---\n\n## Features\n\n- **9 first-party integrations** out of the box — Jira, GitHub, Slack, Notion, Linear, Gmail, Google Calendar, Figma, Confluence. Add or remove servers by editing `backend/integrations.json`; any MCP server published on Smithery works.\n- **Approval-gated tool calls** — every write is paused mid-stream and surfaced as a card you accept or reject. Reads auto-dispatch.\n- **Default-deny tool gate** — verb-token classifier in `backend/agent/policies.py` (75 write tokens, 42 read tokens). New integrations inherit the policy with zero config.\n- **YOLO mode** — opt-in toggle that auto-approves every tool call for the session. Off by default.\n- **Proactive rules engine** — describe a trigger in plain English (\"ping me on Slack when my GitHub PR gets a review request\"). An OpenRouter call compiles it to a typed spec, APScheduler polls the source tool on an interval (≥ 60 s), matches stream into a pinned **Rules activity** chat. Hot path is LLM-free.\n- **Telegram bridge** — long-poll client mirrors a private Telegram chat into the agent. Approve writes from your phone.\n- **Glassmorphic UI** — animated aurora background, frosted panels, centered chat island. Designed to look like a real product.\n- **Streaming SSE everything** — token-by-token model output, tool-call lifecycle events, all over a single channel.\n- **Tool grouping** — chained tool calls collapse into a single expandable group card so the timeline stays readable.\n- **Smooth pending → success transitions** — the same card cross-fades from amber to emerald when a tool finishes, no flicker.\n- **Local-first persistence** — async SQLite via SQLModel; chats, settings, rules, firings all live in `backend/data/pm.db`. Smithery owns third-party tokens.\n\n---\n\n## Architecture\n\n```\n┌──────────────────────────────────────────────────────────────────────┐\n│                         Browser (localhost:5173)                     │\n│  React 19 · Vite 8 · Tailwind v4 · Zustand · TanStack Query 5        │\n│                                                                      │\n│  ChatView ──── SSE ─────┐                                            │\n│  ToolApproval ─ POST ──┐│                                            │\n│  Integrations ─ POST ──┘│                                            │\n└─────────────────────────┼────────────────────────────────────────────┘\n                          │ /api  /sse   (Vite proxy)\n┌─────────────────────────▼────────────────────────────────────────────┐\n│                    FastAPI backend (localhost:8000)                  │\n│                                                                      │\n│  ┌──────────────┐    ┌────────────────┐    ┌─────────────────────┐   │\n│  │ AgentSession │───▶│  MCPManager    │───▶│ Smithery Connect    │   │\n│  │  (loop.py)   │    │  + builtins    │    │ (server-side OAuth) │   │\n│  └──────┬───────┘    └────────────────┘    └─────────────────────┘   │\n│         │                                                            │\n│         ▼                                                            │\n│  ┌──────────────┐    ┌────────────────┐    ┌─────────────────────┐   │\n│  │  OpenRouter  │    │  APScheduler   │    │  Telegram bridge    │   │\n│  │  (streaming) │    │  rule_engine   │    │  (long-poll)        │   │\n│  └──────────────┘    └────────────────┘    └─────────────────────┘   │\n│                                                                      │\n│                     SQLModel  ──▶  data/pm.db                        │\n└──────────────────────────────────────────────────────────────────────┘\n```\n\nThe control plane lives in `agent/loop.py`: stream OpenRouter → assemble tool-call fragments → consult `agent/policies.py` → either dispatch immediately (read) or pause and emit `tool_call_request` (write). Approval comes back through `POST /api/chats/{chat_id}/approve`, which resumes the same in-memory `AgentSession`. Every Smithery call is plain JSON-RPC over `httpx`; backend never sees a Jira/Slack/Google token.\n\n---\n\n## Quick start\n\n### Prerequisites\n\n| Tool | Version |\n|---|---|\n| Python | 3.13 |\n| [`uv`](https://github.com/astral-sh/uv) | 0.5+ |\n| Node | 20+ |\n| [`pnpm`](https://pnpm.io) | 9+ |\n| OpenRouter API key | https://openrouter.ai/keys |\n| Smithery API key | https://smithery.ai → Account → API Keys |\n\n### Install\n\n```bash\ngit clone https://github.com/\u003cyou\u003e/pm-assistant.git\ncd pm-assistant\n\ncp .env.example .env\n# edit .env, set OPENROUTER_API_KEY and SMITHERY_API_KEY\n\ncd backend  \u0026\u0026 uv sync   \u0026\u0026 cd ..\ncd frontend \u0026\u0026 pnpm install \u0026\u0026 cd ..\n```\n\n### Run (two terminals)\n\n```bash\n# Terminal 1 — backend on :8000\ncd backend\nuv run uvicorn main:app --reload --port 8000\n```\n\n```bash\n# Terminal 2 — frontend on :5173 (proxies /api and /sse to :8000)\ncd frontend\npnpm dev\n```\n\nOpen \u003chttp://localhost:5173\u003e. First boot creates `backend/data/pm.db` automatically.\n\n### Environment variables\n\n| Variable | Required | Default | Notes |\n|---|---|---|---|\n| `OPENROUTER_API_KEY` | yes | — | Used for chat completions and rule compilation. |\n| `SMITHERY_API_KEY` | yes | — | Bearer token for `api.smithery.ai`. |\n| `OPENROUTER_DEFAULT_MODEL` | no | `google/gemini-3-flash-preview` | Override per-chat in Settings. |\n| `SMITHERY_NAMESPACE` | no | `pm-assistant` | Logical namespace for connections. |\n| `DATABASE_URL` | no | `sqlite+aiosqlite:///./data/pm.db` | Anything SQLModel-async-compatible. |\n| `SMITHERY_API_BASE` | no | `https://api.smithery.ai` | For self-hosted Smithery. |\n| `TELEGRAM_BOT_TOKEN` | no | — | Enables the Telegram bridge. |\n| `TELEGRAM_ALLOWED_CHAT_ID` | no | — | Restrict bot to a single chat ID. |\n\n`.env` lives at the **repo root**, not under `backend/`. `backend/config.py` loads `../.env` explicitly.\n\n---\n\n## Configuring integrations\n\nSmithery Connect handles OAuth for every third-party service so this codebase never touches a Jira/Google/Slack token.\n\n1. Open **Settings → Integrations**.\n2. Click **Connect** on the row you want.\n3. A popup opens to Smithery, which redirects to the upstream service for OAuth.\n4. After you authorize, the popup closes; the row flips to **Connected** within ~2 s (frontend polls `POST /api/integrations/{name}/refresh`).\n5. The agent's system prompt is rebuilt on the next message — its capability table now includes that integration's tools.\n\nClick **Disconnect** to revoke. Smithery deletes the encrypted refresh token; tools disappear from the next agent turn.\n\n\u003cdetails\u003e\n\u003csummary\u003e\u003cstrong\u003eAdding a new integration\u003c/strong\u003e\u003c/summary\u003e\n\nEdit `backend/integrations.json`:\n\n```json\n{\n  \"linear\": {\n    \"label\": \"Linear\",\n    \"mcpUrl\": \"https://server.smithery.ai/linear/mcp\"\n  }\n}\n```\n\nRestart the backend. The new row appears in **Settings → Integrations**. The MCP URL must be the canonical one shown on that server's Smithery page.\n\n\u003c/details\u003e\n\n---\n\n## Proactive rules\n\nRules let the agent *do work while you sleep*.\n\n```\nYou: every weekday at 9am, summarize unread Slack DMs from my team and post the digest\n     into #standup\n```\n\nThe flow:\n\n1. **Compile** — one OpenRouter call with `response_format=json_object` produces a typed `CompiledSpec` (source tool, filter, action prompt). Retries once on parse failure, then surfaces the error in the UI.\n2. **Schedule** — `APScheduler` (`AsyncIOScheduler`, `max_instances=1`, default 5-minute interval) polls. The hot path is **zero LLM calls** when nothing matches — only the source tool runs.\n3. **Match** — when the filter fires, an `AgentSession` runs against the pinned **Rules activity** chat, seeded with `[rule trigger] \u003caction_prompt\u003e. Context: \u003csummary\u003e`.\n4. **Approve** — `auto_approve=True` rules run in YOLO mode; otherwise the same approval modal appears in the activity chat.\n5. **Self-heal** — five consecutive failures auto-disables the rule and stamps `last_error` for inspection.\n\nCreate rules from `/rules` (manual two-step editor) or from chat (`rules__create_rule` builtin tool — the LLM is instructed to detect \"whenever / every / if someone…\" intents and ask you for interval + auto-approve before calling).\n\nSupported filter kinds: `message_from_user_contains`, `new_issue_mentions`, `new_pr_review_request`, `schedule_only`. Adding a new kind is documented in `CLAUDE.md`.\n\n---\n\n## Telegram bridge\n\nSet `TELEGRAM_BOT_TOKEN` (and optionally `TELEGRAM_ALLOWED_CHAT_ID`) in `.env`, then restart. A long-poll client subscribes to the bot's updates; messages from the allowed chat are mirrored into a dedicated chat in the UI and run through the same agent loop. The bot replies on the same Telegram thread once the turn finishes — including any approval prompts (you tap a button on Telegram to approve).\n\nThis makes the assistant reachable from your phone without exposing the FastAPI server to the public internet.\n\n---\n\n## Settings\n\nOpen the gear icon. Everything here is persisted to `UserSetting` rows in SQLite.\n\n| Setting | Effect |\n|---|---|\n| **Model** | Any OpenRouter model ID. Falls back to `OPENROUTER_DEFAULT_MODEL`. |\n| **YOLO mode** | Auto-approve every tool call. Use sparingly. |\n| **Auto-approve tools** | Per-tool allowlist for finer-grained YOLO. |\n| **Show tool details** | Expand tool args/results inline by default. |\n| **Additional instructions** | Free-form text appended to the system prompt under `## Additional instructions`. **Not** a system-prompt override. |\n\nMutating settings drops the in-memory `AgentSession` so the next message rebuilds the system prompt with new flags.\n\n---\n\n## Tech stack\n\n| Layer | Tech |\n|---|---|\n| Frontend framework | React 19, Vite 8, TypeScript |\n| Styling | Tailwind v4 (via `@tailwindcss/vite`, no config file) |\n| Frontend state | Zustand (`src/store.ts`) + TanStack Query 5 |\n| Backend framework | FastAPI, Python 3.13, async throughout |\n| ORM / DB | SQLModel + `aiosqlite` |\n| LLM client | `openai` SDK pointed at OpenRouter |\n| MCP transport | `httpx.AsyncClient` against Smithery Connect (Streamable HTTP JSON-RPC) |\n| Scheduler | APScheduler (`AsyncIOScheduler`) |\n| Tests | `pytest` + `respx` (backend), `vitest` + `jsdom` (frontend) |\n| Package managers | `uv` (Python), `pnpm` (Node) |\n\n---\n\n## Project structure\n\n```\nproject-manager/\n├── backend/              FastAPI app\n│   ├── agent/            AgentSession loop, policies, system prompt assembly\n│   ├── api/              HTTP routers: chats, settings, integrations, rules, sse\n│   ├── db/               SQLModel models, async session, migration patches\n│   ├── services/         rule_compiler, rule_engine, rule_scheduler, rule_tool, telegram\n│   ├── integrations.json Declarative list of Smithery MCP servers\n│   ├── smithery_client.py Thin httpx wrapper for Smithery Connect\n│   ├── data/             SQLite file lives here (gitignored)\n│   ├── tests/            175 pytest tests\n│   └── main.py           FastAPI lifespan: scheduler, builtin tool register, telegram start\n├── frontend/             Vite app\n│   ├── src/\n│   │   ├── components/   ChatView, MessageList, Composer, ToolApproval, RuleEditor, Sidebar\n│   │   ├── lib/          stream.ts (SSE parser), api.ts\n│   │   ├── store.ts      Zustand store — chats, approvals, settings\n│   │   └── index.css     Tailwind v4 @theme tokens, glass utilities, aurora animation\n│   └── tests/            23 vitest tests\n├── docs/screenshots/     PNG/GIF assets referenced from this README\n├── .env.example          Template — copy to .env and fill in keys\n└── CHANGELOG.md          Release notes\n```\n\n---\n\n## Tests\n\n```bash\ncd backend  \u0026\u0026 uv run pytest -v          # 175 tests\ncd frontend \u0026\u0026 pnpm test                  # 23 tests (vitest)\n\n# single test\ncd backend  \u0026\u0026 uv run pytest tests/test_agent_loop.py::test_approval_flow -v\n\n# lint / typecheck\ncd frontend \u0026\u0026 pnpm lint\ncd frontend \u0026\u0026 pnpm build                 # tsc -b \u0026\u0026 vite build\n```\n\n`asyncio_mode = \"auto\"` is set in `pyproject.toml`, so `async def test_…` runs without the `@pytest.mark.asyncio` decorator. Smithery is fully mocked via `respx`; tests never hit the network.\n\n---\n\n## Security model\n\n- **Default-deny tool gate.** `agent/policies.py` treats every tool as write-class unless its verb is on the read-token allowlist AND none of its segments are write tokens. Unknown tools are gated. The agent cannot bypass approval by inventing a tool name or burying a write verb after a read prefix.\n- **Smithery error-body redaction.** `Bearer \u003ctoken\u003e`, `setupUrl=…`, and credential-shaped substrings are stripped from upstream error bodies before they land in `last_error`, `RuleFiring.error`, or any SSE `error` event.\n- **No third-party tokens locally.** Jira / GitHub / Slack / Notion / Google / Linear / Figma / Confluence credentials live encrypted at Smithery. Disconnecting from the UI revokes them upstream.\n- **Single-user assumption.** There is no auth in front of the FastAPI server. Bind to `127.0.0.1` (the default) or put it behind a reverse proxy you control. Do not expose `:8000` to the internet.\n- **Telegram callback authorization.** Every approve/reject callback validates `from.id == chat.id == bound_chat_id` before mutating the gate.\n- **Rule auto-disable.** Five consecutive errors flip a rule to `enabled=False` so a misconfigured spec cannot quietly burn through your OpenRouter quota.\n\nIf you find a security issue, please email the maintainer rather than opening a public issue.\n\n---\n\n## Contributing\n\n1. Open an issue describing the change. For non-trivial work, wait for a maintainer ack before coding.\n2. Branch from `main`, name it `feat/...`, `fix/...`, `refactor/...`, etc.\n3. Conventional Commits: `\u003ctype\u003e(\u003cscope\u003e): \u003cdescription\u003e`.\n4. Add tests. Backend changes need pytest coverage; frontend changes need vitest where it makes sense.\n5. Run `pnpm lint`, `pnpm build`, `uv run pytest` before pushing.\n6. Open a PR against `main`. Reference the issue.\n\nSee [`CHANGELOG.md`](CHANGELOG.md) for release history.\n\n\u003cdetails\u003e\n\u003csummary\u003e\u003cstrong\u003eHouse rules (read before your first PR)\u003c/strong\u003e\u003c/summary\u003e\n\n- Backend is `snake_case`, type-hinted on every signature, async throughout — no sync DB calls.\n- Frontend is `camelCase`, Tailwind utility classes only — no CSS modules, no `tailwind.config.*`.\n- Use `uv` and `pnpm`. Do not introduce `pip` or `npm`.\n- Do not introduce Axios — `fetch` and `httpx` are the only HTTP clients.\n- Do not hard-code solid panel backgrounds; use the `.glass-*` utilities from `src/index.css`.\n- New SSE event type? Add handlers on **both** sides — the frontend silently drops unknown events.\n- Settings mutations drop the agent session by design. Don't try to keep one alive across a `PATCH /api/settings`.\n\n\u003c/details\u003e\n\n---\n\n## License\n\nMIT — see [`LICENSE`](LICENSE).\n\n---\n\n## Acknowledgements\n\n- [Smithery](https://smithery.ai) for the hosted MCP gateway.\n- [OpenRouter](https://openrouter.ai) for model routing.\n- The MCP working group for the protocol that makes this small.\n","project_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fsepehr071%2Fpm-assistant","html_url":"https://awesome.ecosyste.ms/projects/github.com%2Fsepehr071%2Fpm-assistant","lists_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fsepehr071%2Fpm-assistant/lists"}