maiactl 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,112 @@
1
+ # ── Python ──────────────────────────────────────────────────
2
+ __pycache__/
3
+ *.py[cod]
4
+ *.egg-info/
5
+ *.egg
6
+ build/
7
+
8
+ # ── Test / lint / type caches ──────────────────────────────
9
+ .mypy_cache/
10
+ .pytest_cache/
11
+ .ruff_cache/
12
+ .coverage
13
+ .coverage.*
14
+ htmlcov/
15
+ coverage/
16
+ .tox/
17
+
18
+ # ── Virtual environments ───────────────────────────────────
19
+ .venv/
20
+ venv/
21
+ env/
22
+
23
+ # ── IDE / editor ───────────────────────────────────────────
24
+ .idea/
25
+ .vscode/
26
+ *.swp
27
+ *.swo
28
+ *~
29
+
30
+ # ── OS ─────────────────────────────────────────────────────
31
+ .DS_Store
32
+ Thumbs.db
33
+
34
+ # ── Environment files (keep examples + frontend prod) ─────
35
+ # .env.production for the frontend has no secrets — it points at the
36
+ # same-origin Firebase Hosting rewrite. Keeping it tracked means anyone
37
+ # redeploying gets the prod config without manually recreating the file.
38
+ .env
39
+ .env.*
40
+ !.env.example
41
+ !frontend/.env.production
42
+
43
+ # ── Docker / databases ─────────────────────────────────────
44
+ pgdata/
45
+ *.db
46
+ *.sqlite
47
+ *.sqlite3
48
+
49
+ # ── uv ─────────────────────────────────────────────────────
50
+ backend/.python-version
51
+
52
+ # ── Node / Vite / Next ─────────────────────────────────────
53
+ node_modules/
54
+ .next/
55
+ dist/
56
+ .vite/
57
+ *.tsbuildinfo
58
+
59
+ # ── Logs ───────────────────────────────────────────────────
60
+ *.log
61
+ logs/
62
+
63
+ # ── Archives / bundles (not source) ────────────────────────
64
+ *.zip
65
+ *.tar
66
+ *.tar.gz
67
+ *.tgz
68
+
69
+ # ── Firebase ───────────────────────────────────────────────
70
+ # Per-machine deploy cache; firebase.json + .firebaserc stay tracked.
71
+ .firebase/
72
+ firebase-debug.log
73
+ firestore-debug.log
74
+ ui-debug.log
75
+
76
+ # ── Claude Code per-machine state ──────────────────────────
77
+ # scheduled_tasks.lock + settings.local.json are local to the operator,
78
+ # not part of the repo's shared config.
79
+ .claude/
80
+
81
+ # ── Misc ───────────────────────────────────────────────────
82
+ .cache/
83
+ tmp/
84
+ *.pid
85
+ messages.json
86
+
87
+ # ── Design handoff (generated static mockups) ──────────────
88
+ design-handoff/
89
+
90
+ # Google service-account JSON keys — NEVER commit. Local dev keys live in
91
+ # backend/.secrets/ (vertex-sa.json, maia-sa.json) and are gitignored as a
92
+ # directory so a new key dropped in won't accidentally leak. Prod uses
93
+ # Secret Manager / workload identity instead.
94
+ backend/.secrets/
95
+ # Legacy patterns kept so any straggler key at the repo root still gets caught.
96
+ /trusty-vim-*.json
97
+ /maia-493810-*.json
98
+ *-service-account*.json
99
+ *-gcp-key*.json
100
+ *-vertex*.json
101
+
102
+ client_secret_497788162142-r2a24nb8hl679nloc43jiqctpvmbb5v8.apps.googleusercontent.com.json
103
+ # Claude Code skill artifacts (local tooling, not app code)
104
+ .agents/
105
+ skills-lock.json
106
+ scratchpad.md
107
+
108
+ # ── Desktop (Electron) ──────────────────────────────────────
109
+ # build/ above is a Python pattern; desktop/build holds source-controlled
110
+ # packaging resources (icon, entitlements). dist output stays ignored.
111
+ !desktop/build/
112
+ desktop/dist/
maiactl-0.1.0/PKG-INFO ADDED
@@ -0,0 +1,111 @@
1
+ Metadata-Version: 2.5
2
+ Name: maiactl
3
+ Version: 0.1.0
4
+ Summary: Drive the M.AI.A board from a terminal (and from terminal agents).
5
+ Requires-Python: >=3.10
6
+ Requires-Dist: httpx>=0.28.1
7
+ Description-Content-Type: text/markdown
8
+
9
+ # maia
10
+
11
+ The M.AI.A board from a terminal, for people and for the coding agents they
12
+ run there.
13
+
14
+ It talks to the same public HTTP API the web app uses, so it inherits the
15
+ server's rules rather than restating them: any active project member can
16
+ move, edit, assign or reassign any ticket, and observers are read-only.
17
+
18
+ ## Install
19
+
20
+ ```bash
21
+ uv tool install --from ./cli maiactl # from a checkout
22
+ maia login # opens your browser
23
+ maia projects
24
+ maia use MVP
25
+ ```
26
+
27
+ `login` runs the same brokered PKCE flow the desktop app uses: it opens
28
+ Google in your real browser and polls until the backend releases a token.
29
+ The token is written to `~/.config/maia/config.json` with mode 0600.
30
+
31
+ It is a normal app session token, not a scoped API key, so it carries your
32
+ full access and expires on the server's schedule. Treat the file as a
33
+ credential. `maia logout` removes it.
34
+
35
+ For CI or a throwaway shell, skip the file entirely:
36
+
37
+ ```bash
38
+ export MAIA_TOKEN=... MAIA_TENANT_ID=...
39
+ export MAIA_PROJECT=MVP MAIA_API_URL=https://maia.cydratech.com
40
+ ```
41
+
42
+ ## Reading
43
+
44
+ ```bash
45
+ maia ls # active tickets, doing first
46
+ maia ls --mine --status doing
47
+ maia ls --stage "In QA"
48
+ maia ls -s payment # title/description search
49
+ maia show MVP-42 # detail + comments
50
+ maia show MVP-42 --history
51
+ maia board # stages and card counts
52
+ maia members # who's on it, and their roles
53
+ maia stories
54
+ maia files MVP-42 --download ./tmp
55
+ ```
56
+
57
+ Tickets are addressed by key (`MVP-42`), bare number (`42`), or a title
58
+ fragment. A fragment matching more than one ticket is an error, never a
59
+ guess.
60
+
61
+ ## Writing
62
+
63
+ ```bash
64
+ maia new "Payment webhook retries" -d "Stripe retries land as duplicates." \
65
+ --assign priya --stage "In Progress" --story "Billing"
66
+
67
+ git log -1 --format=%B | maia new "Ship the retry fix" -d -
68
+
69
+ maia mv MVP-42 doing # a status…
70
+ maia mv MVP-42 "In QA" # …or a stage by name
71
+ maia assign MVP-42 priya
72
+ maia story MVP-42 "Billing" # file under an existing story
73
+ maia comment MVP-42 "Deployed to staging." --mention priya
74
+ maia comment MVP-42 - < notes.md
75
+ maia attach MVP-42 screenshot.png trace.pdf
76
+ maia drop MVP-42 --reason "superseded by MVP-51"
77
+ ```
78
+
79
+ Every command takes `--json` for programmatic use, and `-p CODE` to act on
80
+ a project other than the selected one.
81
+
82
+ ## What it deliberately doesn't do
83
+
84
+ - **No stage (column) create, rename or delete.** Board structure is
85
+ admin-curated in the app.
86
+ - **No story create, edit or delete.** Same reason. You can file a ticket
87
+ into an existing story with `maia story`.
88
+ - **No ticket delete.** The API has none. `maia drop` sets the reversible
89
+ `dropped` status, which is what "scrap that" means on this board;
90
+ `maia mv <key> next` puts it back.
91
+
92
+ ## For agents
93
+
94
+ Point your terminal agent at `maia --help` and the per-command
95
+ `maia <command> --help`; both are written to be read once and remembered.
96
+ Two habits worth putting in a project's `CLAUDE.md`:
97
+
98
+ - Prefer `--json` when parsing, plain output when showing a human.
99
+ - Narrow before listing. `maia ls` caps at 40 rows by default; on a big
100
+ board use `--mine`, `--stage`, or `-s` rather than `--limit 0`.
101
+
102
+ ## Exit codes
103
+
104
+ | code | meaning |
105
+ |------|---------|
106
+ | 0 | fine |
107
+ | 1 | generic failure |
108
+ | 2 | not authenticated, or nothing selected |
109
+ | 3 | forbidden (e.g. you're an observer) |
110
+ | 4 | not found |
111
+ | 5 | conflict; refetch and retry |
@@ -0,0 +1,103 @@
1
+ # maia
2
+
3
+ The M.AI.A board from a terminal, for people and for the coding agents they
4
+ run there.
5
+
6
+ It talks to the same public HTTP API the web app uses, so it inherits the
7
+ server's rules rather than restating them: any active project member can
8
+ move, edit, assign or reassign any ticket, and observers are read-only.
9
+
10
+ ## Install
11
+
12
+ ```bash
13
+ uv tool install --from ./cli maiactl # from a checkout
14
+ maia login # opens your browser
15
+ maia projects
16
+ maia use MVP
17
+ ```
18
+
19
+ `login` runs the same brokered PKCE flow the desktop app uses: it opens
20
+ Google in your real browser and polls until the backend releases a token.
21
+ The token is written to `~/.config/maia/config.json` with mode 0600.
22
+
23
+ It is a normal app session token, not a scoped API key, so it carries your
24
+ full access and expires on the server's schedule. Treat the file as a
25
+ credential. `maia logout` removes it.
26
+
27
+ For CI or a throwaway shell, skip the file entirely:
28
+
29
+ ```bash
30
+ export MAIA_TOKEN=... MAIA_TENANT_ID=...
31
+ export MAIA_PROJECT=MVP MAIA_API_URL=https://maia.cydratech.com
32
+ ```
33
+
34
+ ## Reading
35
+
36
+ ```bash
37
+ maia ls # active tickets, doing first
38
+ maia ls --mine --status doing
39
+ maia ls --stage "In QA"
40
+ maia ls -s payment # title/description search
41
+ maia show MVP-42 # detail + comments
42
+ maia show MVP-42 --history
43
+ maia board # stages and card counts
44
+ maia members # who's on it, and their roles
45
+ maia stories
46
+ maia files MVP-42 --download ./tmp
47
+ ```
48
+
49
+ Tickets are addressed by key (`MVP-42`), bare number (`42`), or a title
50
+ fragment. A fragment matching more than one ticket is an error, never a
51
+ guess.
52
+
53
+ ## Writing
54
+
55
+ ```bash
56
+ maia new "Payment webhook retries" -d "Stripe retries land as duplicates." \
57
+ --assign priya --stage "In Progress" --story "Billing"
58
+
59
+ git log -1 --format=%B | maia new "Ship the retry fix" -d -
60
+
61
+ maia mv MVP-42 doing # a status…
62
+ maia mv MVP-42 "In QA" # …or a stage by name
63
+ maia assign MVP-42 priya
64
+ maia story MVP-42 "Billing" # file under an existing story
65
+ maia comment MVP-42 "Deployed to staging." --mention priya
66
+ maia comment MVP-42 - < notes.md
67
+ maia attach MVP-42 screenshot.png trace.pdf
68
+ maia drop MVP-42 --reason "superseded by MVP-51"
69
+ ```
70
+
71
+ Every command takes `--json` for programmatic use, and `-p CODE` to act on
72
+ a project other than the selected one.
73
+
74
+ ## What it deliberately doesn't do
75
+
76
+ - **No stage (column) create, rename or delete.** Board structure is
77
+ admin-curated in the app.
78
+ - **No story create, edit or delete.** Same reason. You can file a ticket
79
+ into an existing story with `maia story`.
80
+ - **No ticket delete.** The API has none. `maia drop` sets the reversible
81
+ `dropped` status, which is what "scrap that" means on this board;
82
+ `maia mv <key> next` puts it back.
83
+
84
+ ## For agents
85
+
86
+ Point your terminal agent at `maia --help` and the per-command
87
+ `maia <command> --help`; both are written to be read once and remembered.
88
+ Two habits worth putting in a project's `CLAUDE.md`:
89
+
90
+ - Prefer `--json` when parsing, plain output when showing a human.
91
+ - Narrow before listing. `maia ls` caps at 40 rows by default; on a big
92
+ board use `--mine`, `--stage`, or `-s` rather than `--limit 0`.
93
+
94
+ ## Exit codes
95
+
96
+ | code | meaning |
97
+ |------|---------|
98
+ | 0 | fine |
99
+ | 1 | generic failure |
100
+ | 2 | not authenticated, or nothing selected |
101
+ | 3 | forbidden (e.g. you're an observer) |
102
+ | 4 | not found |
103
+ | 5 | conflict; refetch and retry |
@@ -0,0 +1,40 @@
1
+ [project]
2
+ name = "maiactl"
3
+ version = "0.1.0"
4
+ description = "Drive the M.AI.A board from a terminal (and from terminal agents)."
5
+ readme = "README.md"
6
+ requires-python = ">=3.10"
7
+ # httpx only, on purpose. This package is installed by developers on their
8
+ # own machines (and run via `uvx`), so every dependency is a thing that can
9
+ # break someone's shell. The backend package is NOT a dependency: the CLI
10
+ # talks to the same public HTTP API a browser does.
11
+ dependencies = ["httpx>=0.28.1"]
12
+
13
+ [project.scripts]
14
+ maia = "maiactl.cli:main"
15
+
16
+ [build-system]
17
+ requires = ["hatchling"]
18
+ build-backend = "hatchling.build"
19
+
20
+ [tool.hatch.build.targets.wheel]
21
+ packages = ["src/maiactl"]
22
+
23
+ [dependency-groups]
24
+ dev = ["pytest>=9.0.3", "ruff>=0.15.11"]
25
+
26
+ [tool.ruff]
27
+ line-length = 100
28
+ target-version = "py310"
29
+
30
+ # Same ruleset the backend uses, so a contributor moving between the two
31
+ # doesn't hit different lint opinions.
32
+ [tool.ruff.lint]
33
+ select = ["E", "F", "I", "N", "W", "UP"]
34
+ ignore = ["E501"]
35
+
36
+ [tool.ruff.lint.isort]
37
+ known-first-party = ["maiactl"]
38
+
39
+ [tool.pytest.ini_options]
40
+ testpaths = ["tests"]
@@ -0,0 +1,10 @@
1
+ """maia — the M.AI.A board from a terminal.
2
+
3
+ Layering, deliberately: ``client.py`` holds every operation and knows
4
+ nothing about argv, stdout or exit codes; ``cli.py`` is the argument
5
+ parsing and rendering shell over it. A future MCP server is a second
6
+ shell over the same ``MaiaClient`` rather than a reimplementation, which
7
+ is the whole reason the split exists.
8
+ """
9
+
10
+ __version__ = "0.1.0"
@@ -0,0 +1,8 @@
1
+ """``python -m maiactl`` runs the same entry point as the ``maia`` script."""
2
+
3
+ import sys
4
+
5
+ from .cli import main
6
+
7
+ if __name__ == "__main__":
8
+ sys.exit(main())
@@ -0,0 +1,109 @@
1
+ """Browser login for a terminal, over the backend's brokered poll flow.
2
+
3
+ This is the same flow the desktop app uses, and it exists because a CLI
4
+ can't safely host an OAuth redirect. We generate a PKCE verifier, ask the
5
+ backend to start a session, open the user's real browser at Google, and
6
+ poll until the backend releases a JWT. The verifier never leaves this
7
+ process except as a header on the poll, and it's what binds redemption to
8
+ us: a leaked session id on its own buys nothing.
9
+ """
10
+
11
+ from __future__ import annotations
12
+
13
+ import base64
14
+ import hashlib
15
+ import secrets
16
+ import time
17
+ import webbrowser
18
+
19
+ import httpx
20
+
21
+ from .config import Config
22
+ from .errors import MaiaError
23
+
24
+ # The server allows 43-128 chars; 32 random bytes lands at 43 base64url chars.
25
+ _VERIFIER_BYTES = 32
26
+ _MAX_WAIT_SECONDS = 300
27
+
28
+
29
+ def _pkce_pair() -> tuple[str, str]:
30
+ verifier = base64.urlsafe_b64encode(secrets.token_bytes(_VERIFIER_BYTES)).rstrip(b"=").decode()
31
+ challenge = (
32
+ base64.urlsafe_b64encode(hashlib.sha256(verifier.encode()).digest()).rstrip(b"=").decode()
33
+ )
34
+ return verifier, challenge
35
+
36
+
37
+ def login(
38
+ config: Config,
39
+ *,
40
+ open_browser: bool = True,
41
+ echo=print,
42
+ transport: httpx.BaseTransport | None = None,
43
+ sleep=time.sleep,
44
+ ) -> Config:
45
+ """Run the flow to completion and return the config with a token on it.
46
+
47
+ Does not save — the caller decides, because `maia login` also needs to
48
+ pick a workspace before the file is worth writing.
49
+
50
+ ``transport`` and ``sleep`` exist so tests can drive the poll loop
51
+ without a server and without waiting; nothing in the CLI passes them.
52
+ """
53
+ base = f"{config.api_url}/api/v1"
54
+ verifier, challenge = _pkce_pair()
55
+
56
+ with httpx.Client(timeout=httpx.Timeout(30.0, connect=10.0), transport=transport) as http:
57
+ try:
58
+ r = http.post(f"{base}/auth/login/session", json={"code_challenge": challenge})
59
+ except httpx.HTTPError as e:
60
+ raise MaiaError(f"Couldn't reach {config.api_url}: {e}") from e
61
+ if not r.is_success:
62
+ detail = ""
63
+ try:
64
+ detail = r.json().get("detail", "")
65
+ except ValueError:
66
+ detail = r.text[:200]
67
+ raise MaiaError(f"Couldn't start a login session: {detail or r.status_code}")
68
+
69
+ started = r.json()
70
+ session_id = started["session_id"]
71
+ url = started["url"]
72
+ interval = float(started.get("interval") or 2)
73
+ expires_in = float(started.get("expires_in") or _MAX_WAIT_SECONDS)
74
+
75
+ echo("Opening your browser to sign in with Google.")
76
+ echo(f"If it doesn't open, paste this into a browser:\n\n {url}\n")
77
+ if open_browser:
78
+ try:
79
+ webbrowser.open(url)
80
+ except Exception:
81
+ # A headless box has no browser; the printed URL is the fallback.
82
+ pass
83
+
84
+ echo("Waiting for you to finish signing in… (Ctrl-C to cancel)")
85
+ deadline = time.monotonic() + min(expires_in, _MAX_WAIT_SECONDS)
86
+ while time.monotonic() < deadline:
87
+ sleep(interval)
88
+ try:
89
+ poll = http.get(
90
+ f"{base}/auth/login/session/{session_id}",
91
+ headers={"X-Pkce-Verifier": verifier},
92
+ )
93
+ except httpx.HTTPError:
94
+ # Transient network blip mid-flow: keep polling until the
95
+ # session itself expires rather than failing the login.
96
+ continue
97
+ if poll.status_code == 410:
98
+ raise MaiaError("That login session expired.", hint="Run `maia login` again.")
99
+ if poll.status_code == 403:
100
+ raise MaiaError("Login verification failed (PKCE mismatch).")
101
+ if poll.status_code == 404:
102
+ raise MaiaError("That login session is gone.", hint="Run `maia login` again.")
103
+ if not poll.is_success:
104
+ continue
105
+ body = poll.json()
106
+ if body.get("status") == "complete" and body.get("token"):
107
+ config.token = body["token"]
108
+ return config
109
+ raise MaiaError("Timed out waiting for the browser sign-in.")