letz-cli 0.2.0__tar.gz → 0.3.1__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.
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.5
2
2
  Name: letz-cli
3
- Version: 0.2.0
3
+ Version: 0.3.1
4
4
  Summary: Letz Brain CLI - pull campaigns from the Letz platform, draft email sequences locally with Claude, and push them back staged for review.
5
5
  Project-URL: Homepage, https://sam.weareletz.com
6
6
  Author: Letz
@@ -28,12 +28,15 @@ the [Letz](https://sam.weareletz.com) platform staged for review, and let the cl
28
28
  ## 60-second start
29
29
 
30
30
  ```bash
31
- pip install letz-cli # or: pipx install letz-cli
32
- export LETZ_API_URL="https://your-letz-instance.example.com"
33
- letz campaigns # list your campaigns + ids
34
- letz prompt <CAMPAIGN_ID> # print the playbook for Claude
31
+ pip install letz-cli # or: pipx install letz-cli
32
+ export LETZ_API_KEY="letz_sk_..." # create one in Settings → Claude Code Tools → API Keys
33
+ letz campaigns # list your campaigns + ids
34
+ letz prompt <CAMPAIGN_ID> # print the playbook for Claude
35
35
  ```
36
36
 
37
+ The CLI talks to the Letz cloud by default. Self-hosted or local instance? Point it
38
+ elsewhere with `export LETZ_API_URL="https://your-instance.example.com"` or `--api`.
39
+
37
40
  Paste the output of `letz prompt` into a [Claude Code](https://docs.claude.com/en/docs/claude-code/overview)
38
41
  session signed in with a **Claude Pro or Max subscription** (not a metered API key — the
39
42
  subscription is what makes generation free). Claude runs the whole loop below for you,
@@ -84,8 +87,9 @@ letz status <campaign_id>
84
87
  | `push <id> --activate` | push **and** flip the campaign active (begins sending) | no |
85
88
  | `status <id>` | staged enrollments / previews / sends | no |
86
89
 
87
- Flags: `--api <url>` (or `$LETZ_API_URL`), `--work <dir>` (or `$LETZ_WORK`, default
88
- `./letz_work`), `--version`. Global flags go before the command: `letz --api <url> status <id>`.
90
+ Flags: `--api <url>` (or `$LETZ_API_URL`), `--key <api key>` (or `$LETZ_API_KEY`),
91
+ `--work <dir>` (or `$LETZ_WORK`, default `./letz_work`), `--version`. Global flags go
92
+ before the command: `letz --api <url> status <id>`.
89
93
 
90
94
  Requires Python 3.10+. Zero dependencies (pure standard library).
91
95
 
@@ -7,12 +7,15 @@ the [Letz](https://sam.weareletz.com) platform staged for review, and let the cl
7
7
  ## 60-second start
8
8
 
9
9
  ```bash
10
- pip install letz-cli # or: pipx install letz-cli
11
- export LETZ_API_URL="https://your-letz-instance.example.com"
12
- letz campaigns # list your campaigns + ids
13
- letz prompt <CAMPAIGN_ID> # print the playbook for Claude
10
+ pip install letz-cli # or: pipx install letz-cli
11
+ export LETZ_API_KEY="letz_sk_..." # create one in Settings → Claude Code Tools → API Keys
12
+ letz campaigns # list your campaigns + ids
13
+ letz prompt <CAMPAIGN_ID> # print the playbook for Claude
14
14
  ```
15
15
 
16
+ The CLI talks to the Letz cloud by default. Self-hosted or local instance? Point it
17
+ elsewhere with `export LETZ_API_URL="https://your-instance.example.com"` or `--api`.
18
+
16
19
  Paste the output of `letz prompt` into a [Claude Code](https://docs.claude.com/en/docs/claude-code/overview)
17
20
  session signed in with a **Claude Pro or Max subscription** (not a metered API key — the
18
21
  subscription is what makes generation free). Claude runs the whole loop below for you,
@@ -63,8 +66,9 @@ letz status <campaign_id>
63
66
  | `push <id> --activate` | push **and** flip the campaign active (begins sending) | no |
64
67
  | `status <id>` | staged enrollments / previews / sends | no |
65
68
 
66
- Flags: `--api <url>` (or `$LETZ_API_URL`), `--work <dir>` (or `$LETZ_WORK`, default
67
- `./letz_work`), `--version`. Global flags go before the command: `letz --api <url> status <id>`.
69
+ Flags: `--api <url>` (or `$LETZ_API_URL`), `--key <api key>` (or `$LETZ_API_KEY`),
70
+ `--work <dir>` (or `$LETZ_WORK`, default `./letz_work`), `--version`. Global flags go
71
+ before the command: `letz --api <url> status <id>`.
68
72
 
69
73
  Requires Python 3.10+. Zero dependencies (pure standard library).
70
74
 
@@ -2,4 +2,4 @@
2
2
  sequences locally with Claude, and push them back staged for review.
3
3
  Nothing sends until a human clicks Activate.
4
4
  """
5
- __version__ = "0.2.0"
5
+ __version__ = "0.3.1"
@@ -23,12 +23,12 @@ import sys
23
23
 
24
24
  try:
25
25
  from . import __version__
26
- from .api import LetzAPI, LetzAPIError
26
+ from .api import DEFAULT_API_URL, LetzAPI, LetzAPIError
27
27
  from .gauntlet import audit_sequence
28
28
  except ImportError: # allow running as a plain script: python cli/letz/__main__.py
29
29
  sys.path.insert(0, os.path.dirname(os.path.dirname(os.path.abspath(__file__))))
30
30
  from letz import __version__
31
- from letz.api import LetzAPI, LetzAPIError
31
+ from letz.api import DEFAULT_API_URL, LetzAPI, LetzAPIError
32
32
  from letz.gauntlet import audit_sequence
33
33
 
34
34
 
@@ -65,7 +65,7 @@ def _save(path, obj):
65
65
 
66
66
  # ---------------------------------------------------------------- commands ----
67
67
  def cmd_campaigns(args):
68
- api = LetzAPI(args.api)
68
+ api = LetzAPI(args.api, api_key=args.key)
69
69
  camps = api.campaigns()
70
70
  if not camps:
71
71
  print(f"No campaigns found at {args.api}")
@@ -84,7 +84,7 @@ def cmd_prompt(args):
84
84
 
85
85
 
86
86
  def cmd_pull(args):
87
- api = LetzAPI(args.api)
87
+ api = LetzAPI(args.api, api_key=args.key)
88
88
  camp = api.campaign(args.campaign_id)
89
89
  if not camp:
90
90
  print(f"✗ Campaign {args.campaign_id} not found at {args.api}")
@@ -197,7 +197,7 @@ def cmd_push(args):
197
197
  # Push in batches so a large campaign never trips the request timeout.
198
198
  # The endpoint is idempotent + atomic per request, so a failed batch can be
199
199
  # safely retried by re-running push.
200
- api = LetzAPI(args.api)
200
+ api = LetzAPI(args.api, api_key=args.key)
201
201
  batch = max(1, args.batch)
202
202
  total_imported = total_skipped = 0
203
203
  last_status = None
@@ -222,7 +222,7 @@ def cmd_push(args):
222
222
 
223
223
 
224
224
  def cmd_status(args):
225
- api = LetzAPI(args.api)
225
+ api = LetzAPI(args.api, api_key=args.key)
226
226
  camp = api.campaign(args.campaign_id) or {}
227
227
  enrs = api.enrollments(args.campaign_id)
228
228
  prev = api.previews(args.campaign_id)
@@ -239,7 +239,9 @@ def cmd_status(args):
239
239
  def main(argv=None):
240
240
  p = argparse.ArgumentParser(prog="letz", description="Letz Brain CLI — off-platform generation, persist, click send.")
241
241
  p.add_argument("--version", action="version", version=f"%(prog)s {__version__}")
242
- p.add_argument("--api", default=os.environ.get("LETZ_API_URL", "http://localhost:8000"), help="platform base URL")
242
+ p.add_argument("--api", default=os.environ.get("LETZ_API_URL") or DEFAULT_API_URL,
243
+ help="platform base URL (default: the Letz cloud; override via $LETZ_API_URL)")
244
+ p.add_argument("--key", default=os.environ.get("LETZ_API_KEY", ""), help="API key (or $LETZ_API_KEY) — create one in Settings → API Keys")
243
245
  p.add_argument("--work", default=None, help="work dir (default ./letz_work)")
244
246
  sub = p.add_subparsers(dest="cmd", required=True)
245
247
 
@@ -16,23 +16,34 @@ class LetzAPIError(Exception):
16
16
  super().__init__(f"HTTP {status}: {str(body)[:300]}")
17
17
 
18
18
 
19
+ DEFAULT_API_URL = "https://letz-auto-sdr-production.up.railway.app"
20
+
21
+
19
22
  class LetzAPI:
20
- def __init__(self, base_url: str = "http://localhost:8000", timeout: int = 120):
23
+ def __init__(self, base_url: str = DEFAULT_API_URL, timeout: int = 120,
24
+ api_key: str = ""):
21
25
  self.base = base_url.rstrip("/")
22
26
  self.timeout = timeout
27
+ self.api_key = (api_key or "").strip()
23
28
 
24
29
  def _req(self, method: str, path: str, body=None):
25
30
  url = self.base + ("/api/v1" if not path.startswith("/api/") else "") + path
26
31
  data = json.dumps(body).encode() if body is not None else None
27
- req = urllib.request.Request(
28
- url, data=data, method=method,
29
- headers={"Content-Type": "application/json"},
30
- )
32
+ headers = {"Content-Type": "application/json"}
33
+ if self.api_key:
34
+ headers["X-API-Key"] = self.api_key
35
+ req = urllib.request.Request(url, data=data, method=method, headers=headers)
31
36
  try:
32
37
  with urllib.request.urlopen(req, timeout=self.timeout) as resp:
33
38
  raw = resp.read().decode() or "{}"
34
39
  return json.loads(raw)
35
40
  except urllib.error.HTTPError as e:
41
+ if e.code == 401:
42
+ raise LetzAPIError(401, {
43
+ "detail": "Unauthorized. Create an API key in the platform under "
44
+ "Settings → Claude Code Tools → API Keys, then "
45
+ 'export LETZ_API_KEY="letz_sk_..." (or pass --key).'
46
+ })
36
47
  try:
37
48
  payload = json.loads(e.read().decode() or "{}")
38
49
  except Exception:
File without changes
File without changes
File without changes
File without changes