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.
- {letz_cli-0.2.0 → letz_cli-0.3.1}/PKG-INFO +11 -7
- {letz_cli-0.2.0 → letz_cli-0.3.1}/README.md +10 -6
- {letz_cli-0.2.0 → letz_cli-0.3.1}/letz/__init__.py +1 -1
- {letz_cli-0.2.0 → letz_cli-0.3.1}/letz/__main__.py +9 -7
- {letz_cli-0.2.0 → letz_cli-0.3.1}/letz/api.py +16 -5
- {letz_cli-0.2.0 → letz_cli-0.3.1}/.gitignore +0 -0
- {letz_cli-0.2.0 → letz_cli-0.3.1}/LICENSE +0 -0
- {letz_cli-0.2.0 → letz_cli-0.3.1}/letz/gauntlet.py +0 -0
- {letz_cli-0.2.0 → letz_cli-0.3.1}/pyproject.toml +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.5
|
|
2
2
|
Name: letz-cli
|
|
3
|
-
Version: 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
|
|
32
|
-
export
|
|
33
|
-
letz campaigns
|
|
34
|
-
letz prompt <CAMPAIGN_ID>
|
|
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`), `--
|
|
88
|
-
`./letz_work`), `--version`. Global flags go
|
|
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
|
|
11
|
-
export
|
|
12
|
-
letz campaigns
|
|
13
|
-
letz prompt <CAMPAIGN_ID>
|
|
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`), `--
|
|
67
|
-
`./letz_work`), `--version`. Global flags go
|
|
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
|
|
|
@@ -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"
|
|
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 =
|
|
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
|
-
|
|
28
|
-
|
|
29
|
-
headers
|
|
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
|