Metadata-Version: 2.4
Name: taktcli
Version: 0.2.0
Summary: Authorized GraphQL CLI for Takt — full GraphQL surface from the shell, stdlib-only core
Author: Takt
License: MIT
Project-URL: Homepage, https://takt.sh
Project-URL: Repository, https://github.com/muzhig/takt
Keywords: takt,graphql,cli,agents
Requires-Python: >=3.12
Description-Content-Type: text/markdown
Provides-Extra: watch
Requires-Dist: websockets>=12.0; extra == "watch"
Provides-Extra: dev
Requires-Dist: pytest>=8.0; extra == "dev"

# taktcli

**Authorized GraphQL CLI for [Takt](https://takt.sh)** — the full Takt GraphQL
surface from the shell, for agents and humans. Everything the MCP server does,
**and more**.

The core is **stdlib-only** (`urllib` + `argparse` + `json`): a thin
`argv → GraphQL-over-HTTP` client. Because arguments arrive as `argv` strings, it
structurally sidesteps the MCP client-serialization bug class (arrays-as-JSON-
strings, double-encoded results, the empty-string-drops-all-args bug) — an `argv`
string simply can't be double-JSON-encoded.

The package and console-script are both named `taktcli` (bare `takt` is taken on
PyPI).

## Install

```bash
pip install taktcli            # stdlib-only core
pip install 'taktcli[watch]'   # + websockets, enables `taktcli watch`
pip install -e cli/            # from a monorepo checkout
```

Maestro/harness images ship `pip install 'taktcli[watch]'` — `taktcli watch` is
the supported replacement for the retired `tools/wait_for_task.py`.

## Quickstart

```bash
taktcli login                          # device-auth, stores ~/.takt/credentials.json
taktcli whoami                         # nickname + claims + active credential source
taktcli board --project takt           # show the board
taktcli claim takt-42                  # READY → RUNNING, prints leaseId
taktcli run takt-42                    # heartbeat the lease
taktcli done takt-42                   # mark DONE
```

`taktcli --help` (or `taktcli help`) lists every command; `taktcli <noun> --help`
lists a group's verbs.

## Auth

Token resolution, simplest-wins:

1. **`TAKT_TOKEN` env** → used verbatim. This is what harness containers are
   stuffed with; env always wins over any stored credential.
2. Else **`~/.takt/credentials.json`** — plain JSON `{ "token": "<jwt>", "url": "..." }`,
   mode `0600`, written by `taktcli login`.
3. Else error: `No Takt credentials found. Run: taktcli login` (exit `3`).

URL resolution mirrors it: `TAKT_URL` env → `url` in the credentials file →
default `https://api.takt.sh/graphql`. The `watch` WS endpoint is derived by
swapping the scheme (`https → wss`).

`taktcli login` runs the OAuth 2.0 **device-authorization grant**: it prints a
verification URL + user code, polls until you approve in a browser, then persists
the token. `logout` removes the stored credential; `whoami` prints who you are
and which credential source is active.

```bash
taktcli login                                       # default endpoint
taktcli login --url http://localhost:8454/graphql   # override the endpoint
taktcli logout
taktcli whoami
```

Auth is the same JWT + claims used by MCP and GraphQL — `taktcli` adds **no**
privilege surface. There is no claims/permission verb anywhere; admin ops are
reachable via `gql` only if the caller's token already holds the `admin` claim.
Long-lived bot tokens stay minted out-of-band (`tools/jwt_cli.py`) and dropped
into `TAKT_TOKEN`; `login` is for humans and dev machines.

## Passthrough — reaches 100% of GraphQL

`taktcli gql` + `taktcli schema` are a complete superset of every MCP tool: any
GraphQL op MCP never wrapped is reachable here.

```bash
taktcli gql -q 'query { me { nickname claims } }'
taktcli gql -f query.graphql --var id=takt-42 --field-file description=desc.md
taktcli schema --sdl              # SDL introspection (--json for JSON)
```

Big markdown flows through `--field-file NAME=PATH` (`NAME=-` = stdin) — this
sidesteps shell quoting **and** the empty-string-args bug class.

### Shared I/O flags

- `--var KEY=VALUE` (repeatable; value parsed as JSON when valid, else string)
- `--vars JSON` / `--vars-file PATH` (`-` = stdin) — bulk variables
- `--field-file NAME=PATH` (`NAME=-` = stdin) — inject a file's contents as a
  variable; the canonical path for large markdown
- `--raw` — full `{data, errors, extensions}` envelope (default is pretty `.data`)
- `--compact` — single-line JSON

Variable precedence (lowest → highest):
`--vars-file` < `--vars` < `--var` < `--field-file`.

## Command surface

The CLI uses a **positional namespace** — `taktcli <noun> <verb> [args]` — so the
entity is always the first token. This fixes the MCP `create_wiki` (workspace) vs
`wiki_create` (page) collision at the CLI layer. Run `taktcli <noun> --help` for a
group's verbs.

Every MCP tool maps to a verb:

| MCP tool | `taktcli` verb |
|----------|----------------|
| `get_task` | `get <id>` / `task get <id>` |
| `list_tasks` | `task list` |
| `search_tasks` | `task search <q>` |
| `get_board` | `board` |
| `create_task` | `task create` |
| `update_task` | `task update <id>` |
| `update_status` | `status <id> <STATUS>` (or `run` / `done` / `fail`) |
| `claim_task` | `claim <id>` |
| `release_claim` | `release <id>` |
| `force_release_claim` | `gql` (admin claim required) |
| `transition_task` | `task transition <id> <action>` |
| `get_dependencies` | `task deps <id>` |
| `add_comment` | `task comment <id>` |
| `get_task_comments` | `task comments <id>` |
| `attach_result` | `task result <id>` |
| `get_task_results` | `task results <id>` |
| `delete_result` | `task result delete <id>` |
| `wiki_read` | `wiki read <path>` |
| `wiki_search` / `wiki_semantic_search` | `wiki search <q>` (`--semantic`) |
| `wiki_grep` | `wiki grep <pattern>` |
| `wiki_create` | `wiki create <path>` |
| `wiki_update` | `wiki update <path>` |
| `wiki_edit` | `wiki edit <path>` |
| `wiki_move` | `wiki move <path> <new-path>` |
| `wiki_verify` | `wiki verify <path>` |
| `wiki_delete` | `wiki delete <path>` |
| `wiki_bulk_update_status` | `wiki bulk-status <folder> <status>` |
| `find_related_wiki_pages` | `wiki related <task-id>` |
| `list_wiki_feedback` | `wiki feedback list` |
| `resolve_wiki_feedback` | `wiki feedback resolve <id>` |
| `wiki_list` | `workspace list` |
| `create_wiki` | `workspace create` |
| `get_project_wiki` | `workspace get <project>` |
| `request_upload_url_and_headers` + `confirm_upload` | `file upload <path>` (composite) |
| `get_file_url` | `file url <id>` |
| `list_files` | `file list` |
| `attach_file_to_task` | `file attach <id> <task-id>` |
| `detach_file_from_task` | `file detach <id> <task-id>` |
| `attach_file_to_wiki` | `file attach-wiki <id> <path>` |
| `update_label` / `delete_label` | `label update <name>` / `label delete <name>` |
| *(none — CLI-only)* | `label create` |
| `list_notifications` | `notif list` |
| `mark_notification_read` | `notif read <id>` |
| `mark_all_notifications_read` | `notif read-all` |
| `unread_notification_count` | `notif count` |
| *(none — CLI-only)* | `project {list,get,create,rename,member-add,member-remove}` |
| `taskStatusChanged` / `taskChanged` / `resultAdded` / `commentAdded` | `watch` (needs `[watch]` extra) |

Notes:

- `label create` is CLI-only (the MCP server never wrapped `createLabel`, but the
  GraphQL mutation exists). `label update` / `delete` are keyed by **name** and
  resolve the id client-side via the `labels` query.
- The `wiki` / `workspace` verbs take a global `--wiki SLUG` (default `$TAKT_WIKI`)
  and stream big markdown through `--content-file` / `--old-file` / `--new-file`
  (`-` = stdin).
- `project rename` is name-only — slug changes are blocked on the backend.

## Output & exit codes

Default output is pretty JSON of the unwrapped `.data` payload. `--compact` =
single-line; `--raw` = full `{data, errors, extensions}` envelope; `watch` =
NDJSON (one event per line, flushed per event).

| Code | Meaning |
|------|---------|
| `0` | success |
| `1` | GraphQL error (server reached; `errors[]` non-empty) |
| `2` | transport error (DNS/TCP/TLS/HTTP non-2xx/timeout/WS drop) |
| `3` | config or usage error (no token, bad URL, unknown verb, missing flag) |

`extensions.code` (e.g. `LEASE_EXPIRED`, `ALREADY_CLAIMED`) is preserved in error
output so loops can branch on it.

## Task monitoring — `taktcli watch`

Replaces the retired `tools/wait_for_task.py`. Streams subscription events as
NDJSON and reconnects with exponential backoff on drops (e.g. deploys). Needs the
`[watch]` extra.

```bash
taktcli watch --project takt --timeout 3600     # status changes, any task
taktcli watch --task takt-42 --task takt-43      # specific tasks
taktcli watch --project takt --event resultAdded # other subscriptions
```

`--event` defaults to `taskStatusChanged`. Unlike the old one-shot script,
`watch` is a continuous stream — read NDJSON lines as they arrive and act on the
first matching one; no restart-per-event. Tasks already READY when the watcher
starts are **not** detected — check `taktcli board --project takt` first.

## MCP coexistence

`taktcli` and the MCP server run **side-by-side over the same GraphQL API**,
authenticating identically (JWT, same claims). Use either or both in one session.
The MCP freeze/removal timeline lives in PRD 002 §8.

## Documentation

- **Usage reference:** `takt/context/taktcli` in the Takt wiki.
- **Design & rationale:** `takt/dev/prd-002-taktcli` (PRD 002).
- **Contract placement:** `takt/contract` §4.3 — the CLI as a first-class
  integration interface alongside GraphQL + MCP.

## Release

Push a `cli-v*` tag (e.g. `cli-v0.1.0`) — `.github/workflows/cli.yaml` builds
`cli/` and OIDC trusted-publishes to PyPI. The tag must match the version in
`cli/pyproject.toml` (CI asserts this). The CLI has its own CI job alongside
backend/plugin/frontend (`npm run test:cli`).
