commune-cli 0.1.3__tar.gz → 0.1.5__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.
Files changed (28) hide show
  1. {commune_cli-0.1.3 → commune_cli-0.1.5}/PKG-INFO +1 -1
  2. {commune_cli-0.1.3 → commune_cli-0.1.5}/commune_cli/__init__.py +1 -1
  3. {commune_cli-0.1.3 → commune_cli-0.1.5}/commune_cli/banner.py +1 -1
  4. {commune_cli-0.1.3 → commune_cli-0.1.5}/commune_cli/commands/config_cmd.py +5 -125
  5. commune_cli-0.1.5/commune_cli/commands/credits.py +66 -0
  6. commune_cli-0.1.5/commune_cli/commands/phone_numbers.py +92 -0
  7. commune_cli-0.1.5/commune_cli/commands/sms.py +187 -0
  8. {commune_cli-0.1.3 → commune_cli-0.1.5}/commune_cli/main.py +6 -0
  9. {commune_cli-0.1.3 → commune_cli-0.1.5}/pyproject.toml +1 -1
  10. {commune_cli-0.1.3 → commune_cli-0.1.5}/.gitignore +0 -0
  11. {commune_cli-0.1.3 → commune_cli-0.1.5}/README.md +0 -0
  12. {commune_cli-0.1.3 → commune_cli-0.1.5}/commune_cli/client.py +0 -0
  13. {commune_cli-0.1.3 → commune_cli-0.1.5}/commune_cli/commands/__init__.py +0 -0
  14. {commune_cli-0.1.3 → commune_cli-0.1.5}/commune_cli/commands/attachments.py +0 -0
  15. {commune_cli-0.1.3 → commune_cli-0.1.5}/commune_cli/commands/data.py +0 -0
  16. {commune_cli-0.1.3 → commune_cli-0.1.5}/commune_cli/commands/delivery.py +0 -0
  17. {commune_cli-0.1.3 → commune_cli-0.1.5}/commune_cli/commands/dmarc.py +0 -0
  18. {commune_cli-0.1.3 → commune_cli-0.1.5}/commune_cli/commands/domains.py +0 -0
  19. {commune_cli-0.1.3 → commune_cli-0.1.5}/commune_cli/commands/inboxes.py +0 -0
  20. {commune_cli-0.1.3 → commune_cli-0.1.5}/commune_cli/commands/messages.py +0 -0
  21. {commune_cli-0.1.3 → commune_cli-0.1.5}/commune_cli/commands/search.py +0 -0
  22. {commune_cli-0.1.3 → commune_cli-0.1.5}/commune_cli/commands/threads.py +0 -0
  23. {commune_cli-0.1.3 → commune_cli-0.1.5}/commune_cli/commands/webhooks.py +0 -0
  24. {commune_cli-0.1.3 → commune_cli-0.1.5}/commune_cli/config.py +0 -0
  25. {commune_cli-0.1.3 → commune_cli-0.1.5}/commune_cli/errors.py +0 -0
  26. {commune_cli-0.1.3 → commune_cli-0.1.5}/commune_cli/output.py +0 -0
  27. {commune_cli-0.1.3 → commune_cli-0.1.5}/commune_cli/state.py +0 -0
  28. {commune_cli-0.1.3 → commune_cli-0.1.5}/uv.lock +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: commune-cli
3
- Version: 0.1.3
3
+ Version: 0.1.5
4
4
  Summary: Official CLI for the Commune email API — agent-native, pipe-friendly, covers every API surface.
5
5
  Project-URL: Homepage, https://commune.email
6
6
  Project-URL: Documentation, https://docs.commune.email
@@ -1,3 +1,3 @@
1
1
  """Commune CLI — official command-line interface for the Commune email API."""
2
2
 
3
- __version__ = "0.1.3"
3
+ __version__ = "0.1.4"
@@ -47,7 +47,7 @@ _COMMANDS = [
47
47
  ("attachments", "upload · get · url"),
48
48
  ("dmarc", "list · summary"),
49
49
  ("data", "deletion-request · confirm"),
50
- ("config", "set · show · register · status · keys"),
50
+ ("config", "set · show · register · status · keys list · keys revoke"),
51
51
  ]
52
52
 
53
53
  _TAGLINE = "email infrastructure for agents"
@@ -6,7 +6,6 @@ import base64
6
6
  import os
7
7
  import re
8
8
  import stat
9
- import time
10
9
  from pathlib import Path
11
10
  from typing import Optional
12
11
 
@@ -244,43 +243,6 @@ def config_register(
244
243
  agent_id: str = verify_data["agentId"]
245
244
  inbox_email: str = verify_data["inboxEmail"]
246
245
 
247
- # 8. Create API key using agent signing
248
- # Authorization: Agent {agentId}:{base64_signature}
249
- # X-Commune-Timestamp: {timestampMs}
250
- # signature = Ed25519("{agentId}:{timestampMs}")
251
- if not json_output:
252
- print_status("[cyan]→[/cyan] Creating API key...")
253
-
254
- api_key: Optional[str] = None
255
- timestamp_ms = int(time.time() * 1000)
256
- req_sig_msg = f"{agent_id}:{timestamp_ms}"
257
- req_sig = base64.b64encode(private_key.sign(req_sig_msg.encode())).decode()
258
-
259
- try:
260
- r3 = httpx.post(
261
- f"{base_url}/v1/agent/api-keys",
262
- json={"name": f"{org_slug}-cli"},
263
- headers={
264
- "Authorization": f"Agent {agent_id}:{req_sig}",
265
- "X-Commune-Timestamp": str(timestamp_ms),
266
- "Content-Type": "application/json",
267
- },
268
- timeout=30.0,
269
- )
270
- if r3.is_success:
271
- key_data = r3.json()
272
- api_key = (
273
- key_data.get("key")
274
- or key_data.get("apiKey")
275
- or key_data.get("api_key")
276
- )
277
- except Exception:
278
- pass # Non-fatal: agent is registered even if key creation fails
279
-
280
- # 9. Persist to config
281
- if api_key:
282
- set_value("api_key", api_key)
283
-
284
246
  # Save private key + agent ID to ~/.commune/agent.key (owner-only permissions)
285
247
  agent_key_path = config_dir() / "agent.key"
286
248
  agent_key_path.write_text(
@@ -292,27 +254,21 @@ def config_register(
292
254
  except OSError:
293
255
  pass
294
256
 
295
- # 10. Output
257
+ # Output
296
258
  if json_output:
297
259
  print_json({
298
260
  "agentId": agent_id,
299
261
  "inboxEmail": inbox_email,
300
- "apiKey": api_key,
301
- "configSaved": bool(api_key),
302
262
  "keyFile": str(agent_key_path),
303
263
  })
304
264
  return
305
265
 
306
266
  print_success(f"Agent registered: [bold]{agent_id}[/bold]")
307
267
  print_success(f"Inbox ready: [bold]{inbox_email}[/bold]")
308
- if api_key:
309
- print_success(f"API key saved to: [bold]{config_path()}[/bold]")
310
- print_success("Run [bold cyan]commune inboxes list[/bold cyan] to confirm everything is working.")
311
- else:
312
- print_warning(
313
- "Could not auto-create API key. Create one at commune.email/dashboard, then run:\n"
314
- " commune config set api_key comm_..."
315
- )
268
+ print_warning(
269
+ "Get your API key at commune.email/dashboard, then run:\n"
270
+ " commune config set api_key comm_..."
271
+ )
316
272
  from rich.console import Console
317
273
  Console(stderr=True).print(f"\n[dim]Private key stored at: {agent_key_path}[/dim]")
318
274
 
@@ -409,45 +365,6 @@ def keys_list(
409
365
  )
410
366
 
411
367
 
412
- @keys_app.command("create")
413
- def keys_create(
414
- ctx: typer.Context,
415
- name: str = typer.Option(..., "--name", help="Descriptive name for this key (e.g. 'agent-prod')."),
416
- json_output: bool = typer.Option(False, "--json", help="Output JSON."),
417
- ) -> None:
418
- """Create a new API key. POST /v1/agent/api-keys.
419
-
420
- The full key (comm_...) is only returned once — save it immediately.
421
- """
422
- from ..client import CommuneClient
423
- from ..errors import api_error, auth_required_error, network_error
424
- from ..state import AppState
425
-
426
- state: AppState = ctx.obj or AppState()
427
- if not state.has_any_auth():
428
- auth_required_error(json_output=json_output or state.should_json())
429
-
430
- client = CommuneClient.from_state(state)
431
- try:
432
- r = client.post("/v1/agent/api-keys", json={"name": name})
433
- except Exception as exc:
434
- network_error(exc, json_output=json_output or state.should_json())
435
-
436
- if not r.is_success:
437
- api_error(r, json_output=json_output or state.should_json())
438
-
439
- data = r.json()
440
- if json_output or state.should_json():
441
- print_json(data)
442
- return
443
-
444
- key = data.get("key") or data.get("apiKey") or data.get("api_key", "")
445
- key_id = data.get("id", "")
446
- print_success(f"API key created: [bold]{key_id}[/bold]")
447
- if key:
448
- from rich.console import Console
449
- Console(stderr=True).print(f"\n[bold yellow]Key (save this — shown once):[/bold yellow]\n {key}\n")
450
-
451
368
 
452
369
  @keys_app.command("revoke")
453
370
  def keys_revoke(
@@ -488,40 +405,3 @@ def keys_revoke(
488
405
  print_success(f"API key [bold]{key_id}[/bold] revoked.")
489
406
 
490
407
 
491
- @keys_app.command("rotate")
492
- def keys_rotate(
493
- ctx: typer.Context,
494
- key_id: str = typer.Argument(..., help="API key ID to rotate."),
495
- json_output: bool = typer.Option(False, "--json", help="Output JSON."),
496
- ) -> None:
497
- """Rotate an API key — generates a new secret, invalidates the old one. POST /v1/agent/api-keys/{keyId}/rotate.
498
-
499
- The new full key (comm_...) is returned once — save it immediately.
500
- """
501
- from ..client import CommuneClient
502
- from ..errors import api_error, auth_required_error, network_error
503
- from ..state import AppState
504
-
505
- state: AppState = ctx.obj or AppState()
506
- if not state.has_any_auth():
507
- auth_required_error(json_output=json_output or state.should_json())
508
-
509
- client = CommuneClient.from_state(state)
510
- try:
511
- r = client.post(f"/v1/agent/api-keys/{key_id}/rotate")
512
- except Exception as exc:
513
- network_error(exc, json_output=json_output or state.should_json())
514
-
515
- if not r.is_success:
516
- api_error(r, json_output=json_output or state.should_json())
517
-
518
- data = r.json()
519
- if json_output or state.should_json():
520
- print_json(data)
521
- return
522
-
523
- key = data.get("key") or data.get("apiKey") or data.get("api_key", "")
524
- print_success(f"API key [bold]{key_id}[/bold] rotated.")
525
- if key:
526
- from rich.console import Console
527
- Console(stderr=True).print(f"\n[bold yellow]New key (save this — shown once):[/bold yellow]\n {key}\n")
@@ -0,0 +1,66 @@
1
+ """commune credits — credit balance and bundle management."""
2
+
3
+ from __future__ import annotations
4
+
5
+ import typer
6
+
7
+ from ..client import CommuneClient
8
+ from ..errors import api_error, auth_required_error, network_error
9
+ from ..output import print_list, print_record
10
+ from ..state import AppState
11
+
12
+ app = typer.Typer(help="Credit balance and available bundles.", no_args_is_help=True)
13
+
14
+
15
+ @app.command("balance")
16
+ def credits_balance(
17
+ ctx: typer.Context,
18
+ json_output: bool = typer.Option(False, "--json", help="Output JSON."),
19
+ ) -> None:
20
+ """Get current credit balance. GET /v1/credits."""
21
+ state: AppState = ctx.obj or AppState()
22
+ if not state.has_any_auth():
23
+ auth_required_error(json_output=json_output or state.should_json())
24
+
25
+ client = CommuneClient.from_state(state)
26
+ try:
27
+ r = client.get("/v1/credits")
28
+ except Exception as exc:
29
+ network_error(exc, json_output=json_output or state.should_json())
30
+
31
+ if not r.is_success:
32
+ api_error(r, json_output=json_output or state.should_json())
33
+
34
+ print_record(r.json(), json_output=json_output or state.should_json(), title="Credit Balance")
35
+
36
+
37
+ @app.command("bundles")
38
+ def credits_bundles(
39
+ ctx: typer.Context,
40
+ json_output: bool = typer.Option(False, "--json", help="Output JSON."),
41
+ ) -> None:
42
+ """List available credit bundles. GET /v1/credits/bundles."""
43
+ state: AppState = ctx.obj or AppState()
44
+ if not state.has_any_auth():
45
+ auth_required_error(json_output=json_output or state.should_json())
46
+
47
+ client = CommuneClient.from_state(state)
48
+ try:
49
+ r = client.get("/v1/credits/bundles")
50
+ except Exception as exc:
51
+ network_error(exc, json_output=json_output or state.should_json())
52
+
53
+ if not r.is_success:
54
+ api_error(r, json_output=json_output or state.should_json())
55
+
56
+ print_list(
57
+ r.json(),
58
+ json_output=json_output or state.should_json(),
59
+ title="Credit Bundles",
60
+ columns=[
61
+ ("ID", "id"),
62
+ ("Credits", "credits"),
63
+ ("Price", "price"),
64
+ ("Description", "description"),
65
+ ],
66
+ )
@@ -0,0 +1,92 @@
1
+ """commune phone-numbers — phone number management."""
2
+
3
+ from __future__ import annotations
4
+
5
+ from typing import Optional
6
+
7
+ import typer
8
+
9
+ from ..client import CommuneClient
10
+ from ..errors import api_error, auth_required_error, network_error
11
+ from ..output import print_list, print_record
12
+ from ..state import AppState
13
+
14
+ app = typer.Typer(help="Phone number management: list, get, settings.", no_args_is_help=True)
15
+
16
+
17
+ @app.command("list")
18
+ def phone_numbers_list(
19
+ ctx: typer.Context,
20
+ json_output: bool = typer.Option(False, "--json", help="Output JSON."),
21
+ ) -> None:
22
+ """List all phone numbers in your organization. GET /v1/phone-numbers."""
23
+ state: AppState = ctx.obj or AppState()
24
+ if not state.has_any_auth():
25
+ auth_required_error(json_output=json_output or state.should_json())
26
+
27
+ client = CommuneClient.from_state(state)
28
+ try:
29
+ r = client.get("/v1/phone-numbers")
30
+ except Exception as exc:
31
+ network_error(exc, json_output=json_output or state.should_json())
32
+
33
+ if not r.is_success:
34
+ api_error(r, json_output=json_output or state.should_json())
35
+
36
+ print_list(
37
+ r.json(),
38
+ json_output=json_output or state.should_json(),
39
+ title="Phone Numbers",
40
+ columns=[
41
+ ("ID", "id"),
42
+ ("Number", "phoneNumber"),
43
+ ("Friendly Name", "friendlyName"),
44
+ ("Status", "status"),
45
+ ("Created", "createdAt"),
46
+ ],
47
+ )
48
+
49
+
50
+ @app.command("get")
51
+ def phone_numbers_get(
52
+ ctx: typer.Context,
53
+ phone_number_id: str = typer.Argument(..., help="Phone number ID."),
54
+ json_output: bool = typer.Option(False, "--json", help="Output JSON."),
55
+ ) -> None:
56
+ """Get a specific phone number. GET /v1/phone-numbers/{phoneNumberId}."""
57
+ state: AppState = ctx.obj or AppState()
58
+ if not state.has_any_auth():
59
+ auth_required_error(json_output=json_output or state.should_json())
60
+
61
+ client = CommuneClient.from_state(state)
62
+ try:
63
+ r = client.get(f"/v1/phone-numbers/{phone_number_id}")
64
+ except Exception as exc:
65
+ network_error(exc, json_output=json_output or state.should_json())
66
+
67
+ if not r.is_success:
68
+ api_error(r, json_output=json_output or state.should_json())
69
+
70
+ print_record(r.json(), json_output=json_output or state.should_json(), title="Phone Number")
71
+
72
+
73
+ @app.command("settings")
74
+ def phone_numbers_settings(
75
+ ctx: typer.Context,
76
+ json_output: bool = typer.Option(False, "--json", help="Output JSON."),
77
+ ) -> None:
78
+ """Get SMS quota settings for your organization. GET /v1/phone-settings."""
79
+ state: AppState = ctx.obj or AppState()
80
+ if not state.has_any_auth():
81
+ auth_required_error(json_output=json_output or state.should_json())
82
+
83
+ client = CommuneClient.from_state(state)
84
+ try:
85
+ r = client.get("/v1/phone-settings")
86
+ except Exception as exc:
87
+ network_error(exc, json_output=json_output or state.should_json())
88
+
89
+ if not r.is_success:
90
+ api_error(r, json_output=json_output or state.should_json())
91
+
92
+ print_record(r.json(), json_output=json_output or state.should_json(), title="SMS Settings")
@@ -0,0 +1,187 @@
1
+ """commune sms — send SMS and manage conversations."""
2
+
3
+ from __future__ import annotations
4
+
5
+ from typing import Optional
6
+ from urllib.parse import quote
7
+
8
+ import typer
9
+
10
+ from ..client import CommuneClient
11
+ from ..errors import api_error, auth_required_error, network_error, validation_error
12
+ from ..output import print_json, print_list, print_record, print_success
13
+ from ..state import AppState
14
+
15
+ app = typer.Typer(help="Send SMS and manage conversations.", no_args_is_help=True)
16
+
17
+
18
+ @app.command("send")
19
+ def sms_send(
20
+ ctx: typer.Context,
21
+ to: str = typer.Option(..., "--to", help="Recipient phone number in E.164 format (e.g. +1234567890)."),
22
+ body: str = typer.Option(..., "--body", help="SMS message body."),
23
+ phone_number_id: Optional[str] = typer.Option(
24
+ None,
25
+ "--phone-number-id",
26
+ help="Phone number ID to send from. Uses org default if omitted.",
27
+ ),
28
+ json_output: bool = typer.Option(False, "--json", help="Output JSON."),
29
+ ) -> None:
30
+ """Send an SMS message. POST /v1/sms/send."""
31
+ state: AppState = ctx.obj or AppState()
32
+ if not state.has_any_auth():
33
+ auth_required_error(json_output=json_output or state.should_json())
34
+
35
+ payload: dict = {"to": to, "body": body}
36
+ if phone_number_id:
37
+ payload["phone_number_id"] = phone_number_id
38
+
39
+ client = CommuneClient.from_state(state)
40
+ try:
41
+ r = client.post("/v1/sms/send", json=payload)
42
+ except Exception as exc:
43
+ network_error(exc, json_output=json_output or state.should_json())
44
+
45
+ if not r.is_success:
46
+ api_error(r, json_output=json_output or state.should_json())
47
+
48
+ data = r.json()
49
+ if json_output or state.should_json():
50
+ print_json(data)
51
+ return
52
+
53
+ msg_id = data.get("id") or data.get("messageId", "")
54
+ print_success(f"SMS sent. ID: [bold]{msg_id}[/bold]")
55
+
56
+
57
+ @app.command("conversations")
58
+ def sms_conversations(
59
+ ctx: typer.Context,
60
+ phone_number_id: Optional[str] = typer.Option(
61
+ None,
62
+ "--phone-number-id",
63
+ help="Filter by phone number ID.",
64
+ ),
65
+ limit: Optional[int] = typer.Option(20, "--limit", help="Maximum results to return."),
66
+ json_output: bool = typer.Option(False, "--json", help="Output JSON."),
67
+ ) -> None:
68
+ """List SMS conversations. GET /v1/sms/conversations."""
69
+ state: AppState = ctx.obj or AppState()
70
+ if not state.has_any_auth():
71
+ auth_required_error(json_output=json_output or state.should_json())
72
+
73
+ client = CommuneClient.from_state(state)
74
+ try:
75
+ r = client.get("/v1/sms/conversations", params={
76
+ "phone_number_id": phone_number_id,
77
+ "limit": limit,
78
+ })
79
+ except Exception as exc:
80
+ network_error(exc, json_output=json_output or state.should_json())
81
+
82
+ if not r.is_success:
83
+ api_error(r, json_output=json_output or state.should_json())
84
+
85
+ print_list(
86
+ r.json(),
87
+ json_output=json_output or state.should_json(),
88
+ title="SMS Conversations",
89
+ columns=[
90
+ ("Remote Number", "remoteNumber"),
91
+ ("Last Message", "lastMessage"),
92
+ ("Direction", "lastDirection"),
93
+ ("Updated", "updatedAt"),
94
+ ],
95
+ )
96
+
97
+
98
+ @app.command("thread")
99
+ def sms_thread(
100
+ ctx: typer.Context,
101
+ remote_number: str = typer.Argument(..., help="Remote phone number (E.164, e.g. +1234567890)."),
102
+ phone_number_id: str = typer.Option(
103
+ ...,
104
+ "--phone-number-id",
105
+ help="Phone number ID for the conversation.",
106
+ ),
107
+ json_output: bool = typer.Option(False, "--json", help="Output JSON."),
108
+ ) -> None:
109
+ """Get the message thread with a specific number. GET /v1/sms/conversations/{remoteNumber}."""
110
+ state: AppState = ctx.obj or AppState()
111
+ if not state.has_any_auth():
112
+ auth_required_error(json_output=json_output or state.should_json())
113
+
114
+ encoded = quote(remote_number, safe="")
115
+ client = CommuneClient.from_state(state)
116
+ try:
117
+ r = client.get(
118
+ f"/v1/sms/conversations/{encoded}",
119
+ params={"phone_number_id": phone_number_id},
120
+ )
121
+ except Exception as exc:
122
+ network_error(exc, json_output=json_output or state.should_json())
123
+
124
+ if not r.is_success:
125
+ api_error(r, json_output=json_output or state.should_json())
126
+
127
+ data = r.json()
128
+ if json_output or state.should_json():
129
+ print_json(data)
130
+ return
131
+
132
+ messages = data if isinstance(data, list) else data.get("data", data)
133
+ print_list(
134
+ messages,
135
+ json_output=False,
136
+ title=f"Thread with {remote_number}",
137
+ columns=[
138
+ ("ID", "id"),
139
+ ("Direction", "direction"),
140
+ ("Body", "body"),
141
+ ("Sent At", "createdAt"),
142
+ ],
143
+ )
144
+
145
+
146
+ @app.command("search")
147
+ def sms_search(
148
+ ctx: typer.Context,
149
+ query: str = typer.Argument(..., help="Search query."),
150
+ phone_number_id: Optional[str] = typer.Option(
151
+ None,
152
+ "--phone-number-id",
153
+ help="Scope search to a specific phone number.",
154
+ ),
155
+ limit: Optional[int] = typer.Option(20, "--limit", help="Maximum results to return."),
156
+ json_output: bool = typer.Option(False, "--json", help="Output JSON."),
157
+ ) -> None:
158
+ """Search SMS messages. GET /v1/sms/search."""
159
+ state: AppState = ctx.obj or AppState()
160
+ if not state.has_any_auth():
161
+ auth_required_error(json_output=json_output or state.should_json())
162
+
163
+ client = CommuneClient.from_state(state)
164
+ try:
165
+ r = client.get("/v1/sms/search", params={
166
+ "q": query,
167
+ "phone_number_id": phone_number_id,
168
+ "limit": limit,
169
+ })
170
+ except Exception as exc:
171
+ network_error(exc, json_output=json_output or state.should_json())
172
+
173
+ if not r.is_success:
174
+ api_error(r, json_output=json_output or state.should_json())
175
+
176
+ print_list(
177
+ r.json(),
178
+ json_output=json_output or state.should_json(),
179
+ title=f"SMS Search: {query}",
180
+ columns=[
181
+ ("ID", "id"),
182
+ ("From", "from"),
183
+ ("To", "to"),
184
+ ("Body", "body"),
185
+ ("Date", "createdAt"),
186
+ ],
187
+ )
@@ -24,6 +24,9 @@ from .commands import (
24
24
  webhooks,
25
25
  dmarc,
26
26
  data,
27
+ phone_numbers,
28
+ sms,
29
+ credits,
27
30
  )
28
31
 
29
32
  app = typer.Typer(
@@ -49,6 +52,9 @@ app.add_typer(delivery.app, name="delivery", help="Delivery metrics, even
49
52
  app.add_typer(webhooks.app, name="webhooks", help="Webhook delivery log: list, retry, health.")
50
53
  app.add_typer(dmarc.app, name="dmarc", help="DMARC reports and summary.")
51
54
  app.add_typer(data.app, name="data", help="Data deletion requests (GDPR / destructive).")
55
+ app.add_typer(phone_numbers.app, name="phone-numbers", help="Phone number management: list, get, settings.")
56
+ app.add_typer(sms.app, name="sms", help="Send SMS and manage conversations.")
57
+ app.add_typer(credits.app, name="credits", help="Credit balance and available bundles.")
52
58
 
53
59
 
54
60
  @app.callback(invoke_without_command=True)
@@ -4,7 +4,7 @@ build-backend = "hatchling.build"
4
4
 
5
5
  [project]
6
6
  name = "commune-cli"
7
- version = "0.1.3"
7
+ version = "0.1.5"
8
8
  description = "Official CLI for the Commune email API — agent-native, pipe-friendly, covers every API surface."
9
9
  readme = "README.md"
10
10
  requires-python = ">=3.9"
File without changes
File without changes
File without changes