Metadata-Version: 2.4
Name: koshmana
Version: 0.1.2
Summary: The Koshmana client — library, CLI, and MCP server for the Koshmana data service (Activity Streams 2.0)
Project-URL: Homepage, https://koshmana.com
Author: Koshmana
License: MIT
License-File: LICENSE
Keywords: activitystreams,cli,client,cqrs,event-sourcing,koshmana,mcp
Requires-Python: >=3.13
Requires-Dist: httpx>=0.28
Requires-Dist: pyyaml>=6.0.3
Provides-Extra: mcp
Requires-Dist: mcp<2,>=1.28.1; extra == 'mcp'
Description-Content-Type: text/markdown

# Koshmana client

The client for **Koshmana** — a hypermedia, event-sourced data service
living at **https://koshmana.com**. One small library, one CLI, one MCP
server; no server code. If you are an agent who just landed on
koshmana.com, this is how you talk to it.

## What Koshmana is (the model in 60 seconds)

- **The log is truth.** Everything is an **activity** (Activity Streams
  2.0 JSON — `Create`, `Update`, `Delete`, …) appended to a
  **collection**. The append *is* the commit; the ack's `sequence` is the
  event's permanent position on the log. Nothing is ever edited or removed
  from the log — you change state by appending more activities.
- **Reads are projections.** A read model folds the log into current
  state. Every read answer carries `koshmana:watermark` (how far behind
  truth the projection is). Pass an append ack's `sequence` back as
  `min_sequence` for read-your-writes.
- **Objects are dereferenceable URLs.** An object's `id` resolves to its
  current state (`GET /collections/{coll}/objects/{id}`). On deployments
  with a public URL the id *is* an HTTPS URL you can GET directly.
- **Reads can be public; representations are content-negotiated.** A
  collection declared `public: true` serves its objects to anyone with no
  bearer and `Access-Control-Allow-Origin: *`. Both object read doors are
  content-negotiated: `Accept: text/html` renders via the collection's
  template (if any); otherwise you get AS2 JSON. Writes always stay
  authenticated.
- **A collection can be a website.** Declare `site: true` and its objects
  become files addressed by `path`; `/{project}/{coll}/…` serves them like
  a web server, and a deploy is just a batch of appends.

## Install

```sh
pip install koshmana            # library + `koshmana` CLI
pip install "koshmana[mcp]"     # also the `koshmana-mcp` MCP server
```

Requires Python 3.13+. Runtime deps are tiny: `httpx` and `pyyaml`
(plus `mcp` only for the optional MCP server).

## Configuration — a token is the whole config

| | Env var | CLI flag | Default |
|---|---|---|---|
| Server | `KOSHMANA_URL` | `--url` | `https://koshmana.com` |
| Token | `KOSHMANA_TOKEN` | `--token` | `dev-write-token` |
| Actor | `KOSHMANA_ACTOR` | `--actor` (per command) | *(from the token)* |

The one thing you must set is **`KOSHMANA_TOKEN`** — it already encodes
who you are, which project, and what you may do. The URL **defaults to
`https://koshmana.com`** (the public deployment); set it to
`http://127.0.0.1:8600` only for the local playground. The **actor comes
from the token**: a per-agent grant binds an actor, so the client omits
`actor` from writes and the server stamps it. Reads on public
collections need no token.

**Actor-free tokens** (the root token, the dev tokens) carry no bound
actor — impersonation is their power — so a write with one must name the
actor explicitly. Set `KOSHMANA_ACTOR=https://koshmana.dev/actors/<you>`
(or pass `--actor`); a write that omits it 403s with a hint. This is how
the agent protocol attributes each subagent under the shared root token.

## CLI quickstart

Output is YAML by default; add `--json` to see the exact wire format.
Global flags (`--url`, `--token`, `--json`) work before or after the
subcommand.

```sh
koshmana collections                          # what collections exist here
koshmana describe issues                      # a collection's full declaration
koshmana bindings journal                     # a collection's materialized bindings

# Read a declared read model (default entry: "recent")
koshmana get issues
koshmana get issues --slice by-status open    # a computed slice
koshmana get issues --where status=open --where priority=high
koshmana get issues --search '"exact phrase" -noise'
koshmana get issues --order priority --descending

# Append an activity (the commit). YAML or JSON, file or stdin.
koshmana append issues -f issue.yaml
echo '{"content": "quick note"}' | koshmana append notes -f -   # bare object → wrapped
# Anything carrying "actor" or "object" is sent as-is:
koshmana append issues -f envelope.yaml

# Declare a new collection via the catalog
koshmana declare -f collections/issues.json

# Blobs: upload a file, get its public content-addressed URL
koshmana upload photos ./logo.png
koshmana upload photos ./logo.png --type Image --name "Our logo"  # also appends a media object

# The live wire
koshmana tail issues                          # snapshot, then live activities, forever
koshmana tail issues --no-snapshot            # live only, from now

# Subscriptions
koshmana subscribe issues --webhook https://ex.io/hook   # durable webhook delivery
koshmana subscribe issues --webhook https://ex.io/hook --from-start
koshmana subscribe issues --tail              # mint a receive-only stream token
koshmana subscriptions issues
koshmana unsubscribe issues sub-abc123def456

# The raw log (truth) and tombstoning a mistake
koshmana log --from 0
koshmana invalidate issues 12 --reason "mistake"   # tombstone + compensation

# Capability grants (minting takes op `admin` on the tokens collection)
koshmana token mint --name ci --actor https://x.io/ci \
    --collections notes,issues --ops read,write \
    [--projects atlas,zeta | --projects '*'] [--expires ISO8601]
koshmana token mint --name owner --email you@example.com \
    --actor https://x.io/you --collections '*' --ops read,write,admin
koshmana token list
koshmana token revoke ci
```

A bare object (no `actor`/`object` key) is wrapped into an envelope
(`--type`, default `Create`); the actor is left off (the token supplies
it) unless you set `--actor`/`KOSHMANA_ACTOR`. Anything already carrying
`actor` or `object` passes through untouched.
On `append`/`upload` the CLI also prints the created/updated **object
id** — that is what you `Update` later (distinct from the *activity* id in
the ack).

## Python-client quickstart

```python
from koshmana import Koshmana

k = Koshmana(token="…")            # url defaults to https://koshmana.com

# What exists
k.collections()
k.describe("issues")

# Append (the commit) — returns the ack {sequence, id, object, objects}.
# Omit `actor` and the server fills it from the token's bound actor; an
# actor-free token (root/dev) instead needs one supplied.
ack = k.append("notes", {
    "type": "Create",
    "object": {"type": "Note", "content": "hello"},
})

# Read a model; read-your-writes with min_sequence
page = k.query("issues", "recent", where={"status": "open"}, limit=50,
               min_sequence=ack["sequence"])
for item in page["orderedItems"]:
    ...

# Resolve one object by its id (O(1) lookup) — None if absent
obj = k.get("issues", "https://koshmana.com/koshmana/issues/…")

# Partial edit without dropping fields (fetch → shallow-merge → full Update).
# Actor is filled from the token; pass actor=… only with an actor-free one.
k.update("issues", obj["id"], status="closed")

# Blobs: upload bytes, or upload + create a media object in one call
blob = k.upload("photos", png_bytes, "image/png")     # {hash, size, mediaType, url}

# Image transforms (Cloudflare Transformations, if the deployment has it):
#   k.image_url(ack['url'], width=400)  → .../cdn-cgi/image/width=400,format=auto/<blob-url>
#   resized + auto webp/avif off the immutable original, edge-cached
k.attach("photos", png_bytes, "image/png", as_type="Image", name="Logo")

# The stitch: snapshot, then the live wire, forever
for activity in k.tail("issues"):
    ...

# Declare a collection, tombstone an event, read the raw log
k.declare({"name": "notes", "schema": {"type": "object"}})
k.invalidate("issues", 12, reason="mistake")
k.log(from_seq=0, limit=200)
```

## Raw HTTP essentials

Every call is authenticated with `Authorization: Bearer <token>` (except
reads on public collections). All read routes also mount under
`/p/{project}` to address a non-default project.

```http
# Append an activity — this IS the commit
POST /collections/{coll}/events
Content-Type: application/json
{ "type": "Create", "actor": "…", "object": { … } }

# Invoke a read model (default entry "recent"); slices and bindings:
GET /collections/{coll}/recent?limit=50
GET /collections/{coll}/slices/{name}/{value}/recent
GET /collections/{coll}/{binding}/recent

# Resolve one object by id (fully percent-encode the id)
GET /collections/{coll}/objects/{object_id}
# …or dereference a minted URL id directly (public deployments):
GET /{project}/{coll}/{key}
#   Accept: text/html  → rendered via the collection's template (if any)
#   Accept: application/json (default) → AS2 JSON

# Blob upload (content-addressed; identical bytes dedup)
POST /collections/{coll}/blobs
Content-Type: image/png
<raw bytes>            →  { "hash", "size", "mediaType", "url" }

# The live wire (Server-Sent Events; `data:` lines carry each activity)
GET /collections/{coll}/live?cursor=0&watermark=-1

# The raw log (truth)
GET /admin/log?from_seq=0&limit=200

# Static-site collections (site: true) serve files by path
GET /{project}/{coll}/{path…}
```

**Public collections** (declared `public: true`) serve their reads and
object URLs with no bearer and CORS `*`, so an `<img>`, a `fetch()`, or an
`EventSource` on any origin just works. **Writes are always authed.**

## MCP server

Shell-less hosts use the MCP server instead of the CLI — same client
underneath. Install with the extra and run over stdio:

```sh
pip install "koshmana[mcp]"
koshmana-mcp        # reads KOSHMANA_URL / KOSHMANA_TOKEN / KOSHMANA_ACTOR
```

Tools: `list_collections`, `describe_collection`, `query`, `append`,
`declare`, `invalidate`, `read_log`, and `peek` (watch the live wire
briefly).

## License

MIT — see [LICENSE](LICENSE).
