Metadata-Version: 2.4
Name: marketr-installer
Version: 0.1.10
Summary: Installer and license manager for the marketr-* family of Claude Code skills
Project-URL: Homepage, https://github.com/marketr/marketr-installer
Project-URL: Issues, https://github.com/marketr/marketr-installer/issues
Author-email: marketr maintainers <dobrinmdobrev@yahoo.com>
License-Expression: MIT
License-File: LICENSE
Keywords: claude-code,installer,marketr,skills
Classifier: Development Status :: 3 - Alpha
Classifier: Intended Audience :: Developers
Classifier: License :: OSI Approved :: MIT License
Classifier: Programming Language :: Python :: 3.11
Classifier: Programming Language :: Python :: 3.12
Classifier: Topic :: System :: Software Distribution
Requires-Python: >=3.11
Requires-Dist: click>=8.1
Requires-Dist: cryptography>=42
Requires-Dist: email-validator>=2.0
Requires-Dist: packaging>=23.0
Requires-Dist: pydantic>=2.6
Requires-Dist: pyyaml>=6.0
Requires-Dist: requests>=2.31
Provides-Extra: dev
Requires-Dist: mypy>=1.8; extra == 'dev'
Requires-Dist: pytest-cov>=4.1; extra == 'dev'
Requires-Dist: pytest>=7.4; extra == 'dev'
Requires-Dist: ruff>=0.4; extra == 'dev'
Provides-Extra: r2
Requires-Dist: boto3>=1.34; extra == 'r2'
Description-Content-Type: text/markdown

<!-- SPDX-License-Identifier: MIT -->
<!-- Copyright (c) 2026 SkillsForMarketing -->

# marketr-installer

Installer and license manager for the marketr-* family of Claude Code skills.

**What's in this pack →** [INDEX.md](INDEX.md) · [Skills](SKILLS-INDEX.md) · [Agents](AGENTS-INDEX.md) · [Workflows](WORKFLOWS-INDEX.md) · [Scripts](SCRIPTS-INDEX.md)

## What this does

`marketr-installer` is the bridge between a customer's purchased licenses and a working installation of marketr skills in their Claude Code environment. It:

1. Reads per-purchase license files from `~/.marketr/licenses/`
2. Computes the merged entitlement (union by SKU)
3. Installs each owned SKU as a Python package via `pip`
4. Copies each SKU's `SKILL.md` + references into `~/.claude/skills/marketr/<sku>/` so Claude Code discovers them

It is also the home of the `marketr` CLI (`install`, `update`, `add-license`, `status`, `setup`, `auth`, `init`, `doctor`).

`marketr update` enforces the per-license `updates_until` field, and `marketr status` shows days-until-expiry per SKU. Lapsed-window SKUs skip the wheel fetch with a friendly renewal nudge; the installed version keeps working. See [`docs/plans/2026-05-10-perpetual-license-launch.md` §6.1 to §6.3](../docs/plans/2026-05-10-perpetual-license-launch.md#61-how-updates-work-at-runtime-phase-a-wiring) for the full runtime contract, the 7-day signed-URL footgun, and the manual renewal flow.

## Status

Alpha. Phase 1 of the marketplace launch plan (`docs/plans/2026-05-09-marketplace-launch.md`).
R2 URL signing wired 2026-05-09 (see "Operator setup — R2 signing" below).

## Operator setup — R2 signing

`mkt-admin license issue --method wheel` signs R2 URLs at issuance time so
the customer's `marketr install` can `pip install` the wheel directly.
**Only the operator needs R2 credentials** — the signed URL embedded in the
license YAML is self-authenticating until expiry.

### One-time install

```sh
pip install 'marketr-installer[r2]'   # adds boto3
```

### Required environment variables

Set these in your shell rc (`~/.zshrc` / `~/.bash_profile`) so `mkt-admin`
can read them. They never end up in git, license YAMLs, or customer
artifacts.

| Var | Example | Where to find it |
|---|---|---|
| `MARKETR_R2_ACCOUNT_ID` | `2f6b6043df01b12524c5752df11ffb22` | Cloudflare Dashboard → R2 → bucket details (or the hex prefix in the bucket URL) |
| `MARKETR_R2_BUCKET` | `skillsformarketing` | The bucket name |
| `MARKETR_R2_ENDPOINT` | `https://2f6b6043df01b12524c5752df11ffb22.r2.cloudflarestorage.com` | The S3 API endpoint URL **without** the bucket suffix |
| `MARKETR_R2_ACCESS_KEY` | `(redacted)` | Cloudflare Dashboard → R2 → Manage API Tokens → create with **"Object Read & Write"** scoped to the bucket |
| `MARKETR_R2_SECRET_KEY` | `(redacted)` | Same step as above. Cloudflare shows the secret **once at creation** — save to a password manager immediately. |

### How it behaves

| Scenario | Result |
|---|---|
| All 5 env vars set, URL bucket matches `MARKETR_R2_BUCKET` | URL signed with `X-Amz-Signature=…` query string, valid 7 days (AWS Sig v4 max) |
| Any env var missing/empty | Unsigned URL emitted + WARNING printed to stderr. License YAML is still issuable for dev/testing; install will fail at the actual download step. |
| URL bucket prefix doesn't match `MARKETR_R2_BUCKET` | `R2SigningError` — operator misconfigured something. Update `skus.yaml` URL templates or the env var. |

### Customer perspective

Customers don't install `[r2]` and don't set any env vars. Their license YAML
contains a pre-signed URL; `marketr install` runs `pip install <url>` and
that's it.

### URL expiry

Signed URLs are valid for **7 days** (the AWS Sig v4 maximum for static
credentials). If a customer takes longer than 7 days to install after
receiving a license, run `mkt-admin license issue` again for the same SKU
and re-email the YAML.

## Customer install (eventually)

```sh
curl -L https://marketr.<tld>/install | sh
```

This script will install `marketr-installer` (currently from a private wheel URL or git+ssh) and run `marketr setup` to drop the Claude Code skill into `~/.claude/skills/marketr/`.

To opt into voice/sound feedback during install:

```sh
curl -L https://marketr.<tld>/install | MARKETR_WITH_VOICE=1 sh
```

To skip the third-party bundles entirely (minimal install):

```sh
curl -L https://marketr.<tld>/install | MARKETR_SKIP_BUNDLES=1 sh
```

## License redemption

After a paid purchase, the operator (or, eventually, the marketr-store
backend) issues a signed license YAML and uploads it to R2 under
`marketr/_redeem/<token>/license.yaml`. The customer receives a
**presigned GET URL** (AWS Sig v4, 7-day max TTL) by email and
redeems it with one command:

```sh
marketr install --redeem 'https://<account>.r2.cloudflarestorage.com/marketr/_redeem/<token>/license.yaml?X-Amz-Signature=...'
```

This:

1. Downloads the YAML over HTTPS (30s timeout, identifies as
   `User-Agent: marketr-installer/<version>` so we're greppable in
   R2 access logs).
2. **Verifies the embedded Ed25519 signature locally** against the
   bundled public keys (dev + prod). The URL alone is NOT trusted —
   a compromised email or a typo'd URL must not be able to plant
   arbitrary YAML on the customer's machine.
3. Writes the file atomically to `~/.marketr/licenses/<license_id>.yaml`.
4. Continues with the normal install (same code path as bare
   `marketr install`).

If anything fails (HTTP error, expired URL, bad signature, a license
with the same `license_id` is already redeemed) the command exits with
status 1 and **does not** run any pip install — redemption is
all-or-nothing.

The 7-day TTL is the operator's policy; if a customer's URL has
expired, the operator re-issues a fresh one (`mkt-admin license issue`
or, eventually, the store-backend re-signs). The redeem flow does
NOT extend or refresh URLs on its own.

### Security model in one paragraph

The presigned URL is just a delivery mechanism — the trust anchor is
the Ed25519 signature inside the YAML, verified against public keys
shipped in the wheel under `marketr_installer/_keys/`. As long as
the operator's private key stays vaulted (`~/.marketr/signing/` by
convention), any tampering anywhere along the email/network path
fails the signature check at redeem time, so the customer cannot
end up with an unauthorized license.

## What `marketr setup` installs

| Component | Source | Lands at | Default |
|---|---|---|---|
| `marketr` CLI + installer skill | this package | `~/.claude/skills/marketr-installer/` + pip user bin | always |
| `~/.marketr/` scaffold | this package | `~/.marketr/{licenses,credentials,vendor,templates}/` | always |
| Best-practice agents/commands/rules/skills | [shanraisshan/claude-code-best-practice](https://github.com/shanraisshan/claude-code-best-practice) | `~/.claude/{agents,commands,rules,skills}/best-practice/` | on, `--skip-bundles` to opt out |
| Best-practice CLAUDE.md template | same | `~/.marketr/templates/best-practice-CLAUDE.md` (staged, never auto-applied) | on |
| Voice/sound hooks | [shanraisshan/claude-code-hooks](https://github.com/shanraisshan/claude-code-hooks) | `~/.claude/hooks/marketr-voice/` | **off**, `--with-voice` to enable |

Bundles are namespaced under `best-practice/` and `marketr-voice/` subdirs so we never collide with files the user has already authored.

## Voice feedback (opt-in)

```sh
marketr voice enable    # fetch + install hooks, merge bindings into ~/.claude/settings.json
marketr voice status    # show enabled/disabled + binding count
marketr voice disable   # remove bindings (keeps files cached, fast re-enable)
```

Bindings are tagged `marketr-voice:<HookEvent>` so disable removes only our entries — your other hooks stay intact.

## Activation (online seat enforcement)

A license can grant N concurrent machine activations. The mechanism:

1. The license YAML carries a `seats: N` field (default 1; bundles like
   `agency_multi_client` ship `seats: 5`).
2. On `marketr install`, the CLI POSTs `{license_id, fingerprint}` to
   `${MARKETR_ACTIVATION_URL}/activate`. The server records the
   activation if the cap allows, refuses with HTTP 409 if full.
3. Successful activations are cached at
   `~/.marketr/activation_<license_id>.json` for **30 days by
   default**, so subsequent installs on the SAME machine don't
   re-call. The cache stores ONLY the activation token + seat
   metadata — never license content. Operators who want a tighter
   window (e.g. faster seat-revocation propagation) set
   `MARKETR_ACTIVATION_CACHE_DAYS=<N>` where `N` is an integer in
   `[1, 90]`. Out-of-range values raise a clear error at
   `ActivationClient()` construction time — a typo in a deployment
   env file fails loud at startup instead of silently leaking seats.
4. The machine fingerprint is the `install_id` already used for
   telemetry (`~/.marketr/telemetry/.install-id`). No new per-machine
   identifier is introduced; if you opted out of telemetry, the
   activation flow still uses this anonymous random id but doesn't
   send any telemetry events.

### Backwards-compat

If `MARKETR_ACTIVATION_URL` is unset, activation is **skipped** and
`marketr install` prints a stderr note: "activation skipped
(MARKETR_ACTIVATION_URL not set)". Customer flows keep working until
the operator deploys the activation server. Once deployed, the env
var flips enforcement on with no client-side code change.

### Customer commands

```sh
# Start using the license on this machine — happens automatically
# inside `marketr install`, no manual activation step required.
marketr install

# Free up a seat (e.g. retiring an old laptop):
marketr deactivate MKT-AGCY-MULT-CLNT
# Or release a different machine's seat (you'll need its fingerprint;
# the cap-exceeded message lists the active ones):
marketr deactivate MKT-AGCY-MULT-CLNT --fingerprint <fp>

# Inspect current usage:
marketr seats MKT-AGCY-MULT-CLNT
```

### Cap-exceeded message

When the seat cap is full and you try to activate a new machine,
`marketr install` exits with:

```
Seat limit reached for MKT-AGCY-MULT-CLNT: 5/5 in use.
Deactivate one of: <fp_a, fp_b, fp_c, fp_d, fp_e> via
`marketr deactivate MKT-AGCY-MULT-CLNT --fingerprint <fp>`, or
contact sales for a higher-seat license.
```

### Cache, rotation, and growth

The customer-side cache is a single small JSON file per license. The
server-side log is one `<DATA_DIR>/activations/<license_id>.jsonl`
per license that grows by one line per (re)activation or
deactivation. At v1 scale (a handful of machines per license over a
multi-year window) growth is negligible. Operators who want hard
caps on file size can run `marketr_store.activation._compact_inplace`
on a daily cron — it rewrites each file with only the live records.

## Vendored bundles

Third-party content is fetched into `~/.marketr/vendor/<bundle-name>/` at frozen SHAs (see `marketr_installer/bundle_fetcher.py` for current pins). We don't ship the 100MB+ payload in the wheel — it's pulled at `marketr setup` time. Bumping a pin in code requires customers to re-run `marketr setup` to pick up the change.

## Local development

```sh
cd marketr-installer
python -m venv .venv && source .venv/bin/activate
pip install -e ".[dev]"
pytest
```

## Layout

```
marketr-installer/
├── pyproject.toml
├── marketr_installer/        # the Python package
│   ├── cli.py                # `marketr` CLI
│   ├── admin.py              # `mkt-admin` operator CLI
│   ├── schema.py             # Pydantic models for license YAML
│   ├── license_loader.py     # reads + merges ~/.marketr/licenses/
│   ├── pip_runner.py         # subprocess wrapper for pip
│   ├── skill_copier.py       # copies SKILL.md from installed packages
│   ├── bundle_fetcher.py     # clones third-party bundles to ~/.marketr/vendor/
│   └── bundle_installer.py   # deploys bundles into ~/.claude/ (namespaced)
├── skill/
│   └── SKILL.md              # the Claude Code skill that delegates to `marketr`
├── scripts/
│   ├── install.sh            # the curl|sh one-liner
│   └── skus.yaml             # SKU registry (operator-side)
└── tests/
    ├── conftest.py
    ├── fixtures/licenses/    # sample license YAMLs for tests
    └── test_*.py
```
