hevn-cli 0.1.2__tar.gz → 0.1.3__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 (46) hide show
  1. {hevn_cli-0.1.2 → hevn_cli-0.1.3}/PKG-INFO +7 -8
  2. {hevn_cli-0.1.2 → hevn_cli-0.1.3}/README.md +4 -5
  3. {hevn_cli-0.1.2 → hevn_cli-0.1.3}/pyproject.toml +3 -3
  4. {hevn_cli-0.1.2 → hevn_cli-0.1.3}/src/hevn_cli/commands/banks.py +64 -67
  5. {hevn_cli-0.1.2 → hevn_cli-0.1.3}/src/hevn_cli/commands/cards.py +95 -92
  6. {hevn_cli-0.1.2 → hevn_cli-0.1.3}/src/hevn_cli/commands/contracts.py +29 -28
  7. {hevn_cli-0.1.2 → hevn_cli-0.1.3}/src/hevn_cli/commands/invoices.py +1 -4
  8. {hevn_cli-0.1.2 → hevn_cli-0.1.3}/src/hevn_cli/commands/transfer.py +23 -22
  9. {hevn_cli-0.1.2 → hevn_cli-0.1.3}/src/hevn_cli/res/CLAUDE.md +2 -9
  10. {hevn_cli-0.1.2 → hevn_cli-0.1.3}/LICENSE +0 -0
  11. {hevn_cli-0.1.2 → hevn_cli-0.1.3}/src/hevn_cli/__init__.py +0 -0
  12. {hevn_cli-0.1.2 → hevn_cli-0.1.3}/src/hevn_cli/api/__init__.py +0 -0
  13. {hevn_cli-0.1.2 → hevn_cli-0.1.3}/src/hevn_cli/api/app.py +0 -0
  14. {hevn_cli-0.1.2 → hevn_cli-0.1.3}/src/hevn_cli/api/base.py +0 -0
  15. {hevn_cli-0.1.2 → hevn_cli-0.1.3}/src/hevn_cli/api/public.py +0 -0
  16. {hevn_cli-0.1.2 → hevn_cli-0.1.3}/src/hevn_cli/client.py +0 -0
  17. {hevn_cli-0.1.2 → hevn_cli-0.1.3}/src/hevn_cli/command_meta.py +0 -0
  18. {hevn_cli-0.1.2 → hevn_cli-0.1.3}/src/hevn_cli/commands/__init__.py +0 -0
  19. {hevn_cli-0.1.2 → hevn_cli-0.1.3}/src/hevn_cli/commands/account.py +0 -0
  20. {hevn_cli-0.1.2 → hevn_cli-0.1.3}/src/hevn_cli/commands/auth.py +0 -0
  21. {hevn_cli-0.1.2 → hevn_cli-0.1.3}/src/hevn_cli/commands/balance.py +0 -0
  22. {hevn_cli-0.1.2 → hevn_cli-0.1.3}/src/hevn_cli/commands/contacts.py +0 -0
  23. {hevn_cli-0.1.2 → hevn_cli-0.1.3}/src/hevn_cli/commands/contractors.py +0 -0
  24. {hevn_cli-0.1.2 → hevn_cli-0.1.3}/src/hevn_cli/commands/mcp.py +0 -0
  25. {hevn_cli-0.1.2 → hevn_cli-0.1.3}/src/hevn_cli/commands/status.py +0 -0
  26. {hevn_cli-0.1.2 → hevn_cli-0.1.3}/src/hevn_cli/config.py +0 -0
  27. {hevn_cli-0.1.2 → hevn_cli-0.1.3}/src/hevn_cli/env.py +0 -0
  28. {hevn_cli-0.1.2 → hevn_cli-0.1.3}/src/hevn_cli/errors.py +0 -0
  29. {hevn_cli-0.1.2 → hevn_cli-0.1.3}/src/hevn_cli/formatters/__init__.py +0 -0
  30. {hevn_cli-0.1.2 → hevn_cli-0.1.3}/src/hevn_cli/formatters/invoices.py +0 -0
  31. {hevn_cli-0.1.2 → hevn_cli-0.1.3}/src/hevn_cli/interactivity.py +0 -0
  32. {hevn_cli-0.1.2 → hevn_cli-0.1.3}/src/hevn_cli/main.py +0 -0
  33. {hevn_cli-0.1.2 → hevn_cli-0.1.3}/src/hevn_cli/manifest.py +0 -0
  34. {hevn_cli-0.1.2 → hevn_cli-0.1.3}/src/hevn_cli/mcp/__init__.py +0 -0
  35. {hevn_cli-0.1.2 → hevn_cli-0.1.3}/src/hevn_cli/mcp/first_run.py +0 -0
  36. {hevn_cli-0.1.2 → hevn_cli-0.1.3}/src/hevn_cli/mcp/install.py +0 -0
  37. {hevn_cli-0.1.2 → hevn_cli-0.1.3}/src/hevn_cli/mcp/server.py +0 -0
  38. {hevn_cli-0.1.2 → hevn_cli-0.1.3}/src/hevn_cli/mcp/tools.py +0 -0
  39. {hevn_cli-0.1.2 → hevn_cli-0.1.3}/src/hevn_cli/normalize.py +0 -0
  40. {hevn_cli-0.1.2 → hevn_cli-0.1.3}/src/hevn_cli/options.py +0 -0
  41. {hevn_cli-0.1.2 → hevn_cli-0.1.3}/src/hevn_cli/output.py +0 -0
  42. {hevn_cli-0.1.2 → hevn_cli-0.1.3}/src/hevn_cli/parsing.py +0 -0
  43. {hevn_cli-0.1.2 → hevn_cli-0.1.3}/src/hevn_cli/progress.py +0 -0
  44. {hevn_cli-0.1.2 → hevn_cli-0.1.3}/src/hevn_cli/prompts.py +0 -0
  45. {hevn_cli-0.1.2 → hevn_cli-0.1.3}/src/hevn_cli/render.py +0 -0
  46. {hevn_cli-0.1.2 → hevn_cli-0.1.3}/src/hevn_cli/res/__init__.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: hevn-cli
3
- Version: 0.1.2
3
+ Version: 0.1.3
4
4
  Summary: Command-line client for the HEVN backend API and MCP transfers.
5
5
  License: MIT
6
6
  License-File: LICENSE
@@ -31,8 +31,8 @@ Requires-Dist: questionary (==2.1.0)
31
31
  Requires-Dist: rich (==13.9.4)
32
32
  Requires-Dist: tomli-w (>=1.0,<2)
33
33
  Requires-Dist: typer[all] (==0.15.1)
34
- Project-URL: Homepage, https://github.com/hevn/hevn-cli
35
- Project-URL: Repository, https://github.com/hevn/hevn-cli
34
+ Project-URL: Homepage, https://gethevn.com
35
+ Project-URL: Repository, https://github.com/hevn-inc/hevn-cli
36
36
  Description-Content-Type: text/markdown
37
37
 
38
38
  # HEVN CLI
@@ -70,7 +70,7 @@ hevn --help
70
70
  Homebrew install, after a tap is published:
71
71
 
72
72
  ```bash
73
- brew tap hevn/hevn
73
+ brew tap hevn-inc/tap
74
74
  brew install hevn-cli
75
75
  hevn --help
76
76
  ```
@@ -84,7 +84,7 @@ class HevnCli < Formula
84
84
  include Language::Python::Virtualenv
85
85
 
86
86
  desc "Command-line client for the HEVN backend API and MCP transfers"
87
- homepage "https://github.com/hevn/hevn-cli"
87
+ homepage "https://gethevn.com"
88
88
  url "https://files.pythonhosted.org/packages/source/h/hevn-cli/hevn_cli-0.1.0.tar.gz"
89
89
  sha256 "<sha256>"
90
90
  license "MIT"
@@ -107,11 +107,10 @@ class HevnCli < Formula
107
107
  end
108
108
  ```
109
109
 
110
- Before the package is published to PyPI, install from GitHub so `pipx upgrade`
111
- can fetch newer commits:
110
+ Install with `pipx` from PyPI:
112
111
 
113
112
  ```bash
114
- pipx install "git+https://github.com/hevn/hevn-cli.git"
113
+ pipx install hevn-cli
115
114
  pipx upgrade hevn-cli
116
115
  ```
117
116
 
@@ -33,7 +33,7 @@ hevn --help
33
33
  Homebrew install, after a tap is published:
34
34
 
35
35
  ```bash
36
- brew tap hevn/hevn
36
+ brew tap hevn-inc/tap
37
37
  brew install hevn-cli
38
38
  hevn --help
39
39
  ```
@@ -47,7 +47,7 @@ class HevnCli < Formula
47
47
  include Language::Python::Virtualenv
48
48
 
49
49
  desc "Command-line client for the HEVN backend API and MCP transfers"
50
- homepage "https://github.com/hevn/hevn-cli"
50
+ homepage "https://gethevn.com"
51
51
  url "https://files.pythonhosted.org/packages/source/h/hevn-cli/hevn_cli-0.1.0.tar.gz"
52
52
  sha256 "<sha256>"
53
53
  license "MIT"
@@ -70,11 +70,10 @@ class HevnCli < Formula
70
70
  end
71
71
  ```
72
72
 
73
- Before the package is published to PyPI, install from GitHub so `pipx upgrade`
74
- can fetch newer commits:
73
+ Install with `pipx` from PyPI:
75
74
 
76
75
  ```bash
77
- pipx install "git+https://github.com/hevn/hevn-cli.git"
76
+ pipx install hevn-cli
78
77
  pipx upgrade hevn-cli
79
78
  ```
80
79
 
@@ -1,12 +1,12 @@
1
1
  [tool.poetry]
2
2
  name = "hevn-cli"
3
- version = "0.1.2"
3
+ version = "0.1.3"
4
4
  description = "Command-line client for the HEVN backend API and MCP transfers."
5
5
  authors = ["HEVN Team <team@hevn.finance>"]
6
6
  license = "MIT"
7
7
  readme = "README.md"
8
- homepage = "https://github.com/hevn/hevn-cli"
9
- repository = "https://github.com/hevn/hevn-cli"
8
+ homepage = "https://gethevn.com"
9
+ repository = "https://github.com/hevn-inc/hevn-cli"
10
10
  keywords = ["hevn", "cli", "fintech", "payments", "invoicing", "mcp", "stablecoin"]
11
11
  classifiers = [
12
12
  "Development Status :: 4 - Beta",
@@ -120,37 +120,37 @@ def list_banks(
120
120
  yaml_output: YamlOpt = False,
121
121
  ) -> None:
122
122
  data = AppApi().list_banks()
123
- if raw_json or yaml_output:
124
- emit(data, OutputOptions(raw_json, yaml_output))
125
- return
126
-
127
- banks = data.get("allOptions") or [] if all_options else data.get("banks", [])
128
- title = "Bank Options" if all_options else "Banks"
129
- table = Table(title=f"{title} ({len(banks)})", box=box.SIMPLE_HEAVY)
130
- table.add_column("ID", overflow="fold")
131
- table.add_column("Bank")
132
- table.add_column("Country")
133
- table.add_column("Currency")
134
- table.add_column("Method")
135
- table.add_column("Status")
136
- table.add_column("Account Holder")
137
- table.add_column("BIN")
138
- table.add_column("Routing Number")
139
- table.add_column("IBAN")
140
- for bank in banks:
141
- table.add_row(
142
- str(bank.get("id") or "-"),
143
- bank.get("bankName") or "-",
144
- bank.get("country") or "-",
145
- bank.get("currency") or "-",
146
- str(bank.get("method") or "-"),
147
- str(bank.get("status") or "-"),
148
- _account_holder(bank),
149
- _bank_bin(bank),
150
- _routing_number(bank),
151
- _account_summary(bank),
152
- )
153
- console.print(table)
123
+
124
+ def _render(_: dict) -> None:
125
+ banks = data.get("allOptions") or [] if all_options else data.get("banks", [])
126
+ title = "Bank Options" if all_options else "Banks"
127
+ table = Table(title=f"{title} ({len(banks)})", box=box.SIMPLE_HEAVY)
128
+ table.add_column("ID", overflow="fold")
129
+ table.add_column("Bank")
130
+ table.add_column("Country")
131
+ table.add_column("Currency")
132
+ table.add_column("Method")
133
+ table.add_column("Status")
134
+ table.add_column("Account Holder")
135
+ table.add_column("BIN")
136
+ table.add_column("Routing Number")
137
+ table.add_column("IBAN")
138
+ for bank in banks:
139
+ table.add_row(
140
+ str(bank.get("id") or "-"),
141
+ bank.get("bankName") or "-",
142
+ bank.get("country") or "-",
143
+ bank.get("currency") or "-",
144
+ str(bank.get("method") or "-"),
145
+ str(bank.get("status") or "-"),
146
+ _account_holder(bank),
147
+ _bank_bin(bank),
148
+ _routing_number(bank),
149
+ _account_summary(bank),
150
+ )
151
+ console.print(table)
152
+
153
+ emit(data, OutputOptions(raw_json, yaml_output), _render)
154
154
 
155
155
 
156
156
  @app.command("details", help="Show details for a single bank rail.")
@@ -162,39 +162,39 @@ def bank_details(
162
162
  rail = rail or typer.prompt("Bank rail").strip()
163
163
  data = AppApi().list_banks()
164
164
  bank = _find_bank_by_rail(data, rail)
165
- if raw_json or yaml_output:
166
- emit(bank, OutputOptions(raw_json, yaml_output))
167
- return
168
165
 
169
- iban_data = bank.get("ibanData") or {}
170
- lines = [
171
- f"Rail: [bold]{bank.get('rail') or '-'}[/bold]",
172
- f"Bank: {bank.get('bankName') or '-'}",
173
- f"Country: {bank.get('country') or '-'}",
174
- f"Currency: {bank.get('currency') or '-'}",
175
- f"Method: {bank.get('method') or '-'}",
176
- f"Status: [bold]{bank.get('status') or '-'}[/bold]",
177
- f"Pooled: {'yes' if bank.get('pooled') else 'no'}",
178
- f"Compliance: {bank.get('compliance') if bank.get('compliance') is not None else '-'}",
179
- f"Deposit fee: {bank.get('depositFee') if bank.get('depositFee') is not None else '-'}",
180
- f"Time to open: {bank.get('timeToOpen') or '-'}",
181
- f"Account: {_account_summary(bank)}",
182
- f"External account ID: {bank.get('externalAccountId') or '-'}",
183
- ]
184
- if iban_data:
185
- holder = iban_data.get("accountHolderBusinessName") or " ".join(
186
- part
187
- for part in [
188
- iban_data.get("accountHolderFirstName"),
189
- iban_data.get("accountHolderLastName"),
190
- ]
191
- if part
192
- )
193
- if holder:
194
- lines.append(f"Account holder: {holder}")
195
- if iban_data.get("paymentReference"):
196
- lines.append(f"Payment reference: [bold yellow]{iban_data['paymentReference']}[/bold yellow]")
197
- console.print(Panel.fit("\n".join(lines), title="Bank Details", border_style="cyan"))
166
+ def _render(_: dict) -> None:
167
+ iban_data = bank.get("ibanData") or {}
168
+ lines = [
169
+ f"Rail: [bold]{bank.get('rail') or '-'}[/bold]",
170
+ f"Bank: {bank.get('bankName') or '-'}",
171
+ f"Country: {bank.get('country') or '-'}",
172
+ f"Currency: {bank.get('currency') or '-'}",
173
+ f"Method: {bank.get('method') or '-'}",
174
+ f"Status: [bold]{bank.get('status') or '-'}[/bold]",
175
+ f"Pooled: {'yes' if bank.get('pooled') else 'no'}",
176
+ f"Compliance: {bank.get('compliance') if bank.get('compliance') is not None else '-'}",
177
+ f"Deposit fee: {bank.get('depositFee') if bank.get('depositFee') is not None else '-'}",
178
+ f"Time to open: {bank.get('timeToOpen') or '-'}",
179
+ f"Account: {_account_summary(bank)}",
180
+ f"External account ID: {bank.get('externalAccountId') or '-'}",
181
+ ]
182
+ if iban_data:
183
+ holder = iban_data.get("accountHolderBusinessName") or " ".join(
184
+ part
185
+ for part in [
186
+ iban_data.get("accountHolderFirstName"),
187
+ iban_data.get("accountHolderLastName"),
188
+ ]
189
+ if part
190
+ )
191
+ if holder:
192
+ lines.append(f"Account holder: {holder}")
193
+ if iban_data.get("paymentReference"):
194
+ lines.append(f"Payment reference: [bold yellow]{iban_data['paymentReference']}[/bold yellow]")
195
+ console.print(Panel.fit("\n".join(lines), title="Bank Details", border_style="cyan"))
196
+
197
+ emit(bank, OutputOptions(raw_json, yaml_output), _render)
198
198
 
199
199
 
200
200
  @app.command("validate", help="Validate bank requisites (ACH/SEPA/SWIFT/UAEFTS) before creating a contact.")
@@ -228,7 +228,4 @@ def validate_bank_details(
228
228
  "routingNumber": routing_number,
229
229
  }
230
230
  )
231
- if raw_json or yaml_output:
232
- emit(data, OutputOptions(raw_json, yaml_output))
233
- return
234
- _print_validation_result(data)
231
+ emit(data, OutputOptions(raw_json, yaml_output), lambda _: _print_validation_result(data))
@@ -135,30 +135,30 @@ def list_cards(
135
135
  "kycStatus": data.get("kycStatus"),
136
136
  "phoneVerified": data.get("phoneVerified"),
137
137
  }
138
- if raw_json or yaml_output:
139
- emit(wrapped, OutputOptions(raw_json, yaml_output))
140
- return
141
- if not data.get("cards"):
142
- print_cards_status(data)
143
- print_kyc_prompt(no_qr=no_qr)
144
- return
145
- table = Table(title=f"Cards ({data.get('total', 0)})", box=box.SIMPLE_HEAVY)
146
- table.add_column("ID", overflow="fold")
147
- table.add_column("Details")
148
- table.add_column("Daily")
149
- table.add_column("Activated")
150
- for card in data.get("cards", []):
151
- table.add_row(
152
- str(card.get("id")),
153
- card_details(card),
154
- format_limit(card.get("limit")),
155
- "yes" if card.get("activated") else "no",
138
+ def _render(_: dict) -> None:
139
+ if not data.get("cards"):
140
+ print_cards_status(data)
141
+ print_kyc_prompt(no_qr=no_qr)
142
+ return
143
+ table = Table(title=f"Cards ({data.get('total', 0)})", box=box.SIMPLE_HEAVY)
144
+ table.add_column("ID", overflow="fold")
145
+ table.add_column("Details")
146
+ table.add_column("Daily")
147
+ table.add_column("Activated")
148
+ for card in data.get("cards", []):
149
+ table.add_row(
150
+ str(card.get("id")),
151
+ card_details(card),
152
+ format_limit(card.get("limit")),
153
+ "yes" if card.get("activated") else "no",
154
+ )
155
+ console.print(table)
156
+ console.print(
157
+ f"KYC: [bold]{data.get('kycStatus')}[/bold] "
158
+ f"Phone verified: [bold]{'yes' if data.get('phoneVerified') else 'no'}[/bold]"
156
159
  )
157
- console.print(table)
158
- console.print(
159
- f"KYC: [bold]{data.get('kycStatus')}[/bold] "
160
- f"Phone verified: [bold]{'yes' if data.get('phoneVerified') else 'no'}[/bold]"
161
- )
160
+
161
+ emit(wrapped, OutputOptions(raw_json, yaml_output), _render)
162
162
 
163
163
 
164
164
  @app.command("status", help="Show card program status and KYC requirements.")
@@ -186,21 +186,21 @@ def pre_approve_cards(
186
186
  no_qr: bool = typer.Option(False, "--no-qr", help="Do not print terminal QR code."),
187
187
  ) -> None:
188
188
  data = AppApi().pre_approve_cards()
189
- if raw_json or yaml_output:
190
- emit(data, OutputOptions(raw_json, yaml_output))
191
- return
192
189
 
193
- lines = [
194
- f"KYC status: [bold]{data.get('kycStatus') or '-'}[/bold]",
195
- f"Requires phone: [bold]{'yes' if data.get('requirePhoneNumber') else 'no'}[/bold]",
196
- f"Requires address: [bold]{'yes' if data.get('requireAddress') else 'no'}[/bold]",
197
- ]
198
- console.print(Panel.fit("\n".join(lines), title="Card Authorization", border_style="cyan"))
199
- link = data.get("kycUrl")
200
- if link:
201
- print_link_qr("KYC Link", link, show_qr=not no_qr)
202
- else:
203
- console.print("[yellow]No KYC link returned.[/yellow]")
190
+ def _render(_: dict) -> None:
191
+ lines = [
192
+ f"KYC status: [bold]{data.get('kycStatus') or '-'}[/bold]",
193
+ f"Requires phone: [bold]{'yes' if data.get('requirePhoneNumber') else 'no'}[/bold]",
194
+ f"Requires address: [bold]{'yes' if data.get('requireAddress') else 'no'}[/bold]",
195
+ ]
196
+ console.print(Panel.fit("\n".join(lines), title="Card Authorization", border_style="cyan"))
197
+ link = data.get("kycUrl")
198
+ if link:
199
+ print_link_qr("KYC Link", link, show_qr=not no_qr)
200
+ else:
201
+ console.print("[yellow]No KYC link returned.[/yellow]")
202
+
203
+ emit(data, OutputOptions(raw_json, yaml_output), _render)
204
204
 
205
205
 
206
206
  @app.command("kyc-link", help="Get the cardholder KYC verification link.")
@@ -210,13 +210,14 @@ def card_kyc_link(
210
210
  no_qr: bool = typer.Option(False, "--no-qr", help="Do not print terminal QR code."),
211
211
  ) -> None:
212
212
  data = AppApi().card_kyc_link()
213
- if raw_json or yaml_output:
214
- emit(data, OutputOptions(raw_json, yaml_output))
215
- return
216
- link = data.get("link")
217
- if not link:
218
- raise HevnError("KYC link was not returned.", error_code='INTERNAL_ERROR')
219
- print_link_qr("KYC Link", link, show_qr=not no_qr)
213
+
214
+ def _render(_: dict) -> None:
215
+ link = data.get("link")
216
+ if not link:
217
+ raise HevnError("KYC link was not returned.", error_code='INTERNAL_ERROR')
218
+ print_link_qr("KYC Link", link, show_qr=not no_qr)
219
+
220
+ emit(data, OutputOptions(raw_json, yaml_output), _render)
220
221
 
221
222
 
222
223
  @app.command("issue", hidden=True)
@@ -279,23 +280,23 @@ def create_cards(
279
280
  created.append(_create_one_card(card_format, card_label))
280
281
 
281
282
  result = {"cards": created, "total": len(created)}
282
- if raw_json or yaml_output:
283
- emit(result, OutputOptions(raw_json, yaml_output))
284
- return
285
283
 
286
- table = Table(title=f"Created Cards ({len(created)})", box=box.SIMPLE_HEAVY)
287
- table.add_column("ID", overflow="fold")
288
- table.add_column("Details")
289
- table.add_column("Label")
290
- table.add_column("Status")
291
- for card in created:
292
- table.add_row(
293
- str(card.get("id")),
294
- card_details(card),
295
- card.get("label") or "-",
296
- str(card.get("status") or "-"),
297
- )
298
- console.print(table)
284
+ def _render(_: dict) -> None:
285
+ table = Table(title=f"Created Cards ({len(created)})", box=box.SIMPLE_HEAVY)
286
+ table.add_column("ID", overflow="fold")
287
+ table.add_column("Details")
288
+ table.add_column("Label")
289
+ table.add_column("Status")
290
+ for card in created:
291
+ table.add_row(
292
+ str(card.get("id")),
293
+ card_details(card),
294
+ card.get("label") or "-",
295
+ str(card.get("status") or "-"),
296
+ )
297
+ console.print(table)
298
+
299
+ emit(result, OutputOptions(raw_json, yaml_output), _render)
299
300
 
300
301
 
301
302
  @app.command("get", help="Show a card's details.")
@@ -308,28 +309,29 @@ def get_card(
308
309
  ) -> None:
309
310
  card_id = _card_id(ctx, card_id_option or card_id)
310
311
  data = AppApi().get_card(card_id)
311
- if raw_json or yaml_output:
312
- emit(data, OutputOptions(raw_json, yaml_output))
313
- return
314
- card_data = data.get("cardData") or {}
315
- billing = data.get("billingAddress") or {}
316
- console.print(
317
- Panel.fit(
318
- f"ID: [bold]{data.get('id')}[/bold]\n"
319
- f"Name: {card_title(data)}\n"
320
- f"Last 4: {card_last_four(data)}\n"
321
- f"Status: {data.get('status') or '-'}\n"
322
- f"Payment system: {card_data.get('paymentSystem') or '-'}\n"
323
- f"Expiry: {card_data.get('expiryDate') or '-'}\n"
324
- f"Daily limit: {format_limit(data.get('limit'))}\n"
325
- f"Daily spend: {money(data.get('dailySpend'), 'USD')}\n"
326
- f"Activated: {'yes' if data.get('activated') else 'no'}\n"
327
- f"Wallet: {data.get('walletAddress') or '-'}\n"
328
- f"Billing country: {billing.get('country') or '-'}",
329
- title="Card Details",
330
- border_style="cyan",
312
+
313
+ def _render(_: dict) -> None:
314
+ card_data = data.get("cardData") or {}
315
+ billing = data.get("billingAddress") or {}
316
+ console.print(
317
+ Panel.fit(
318
+ f"ID: [bold]{data.get('id')}[/bold]\n"
319
+ f"Name: {card_title(data)}\n"
320
+ f"Last 4: {card_last_four(data)}\n"
321
+ f"Status: {data.get('status') or '-'}\n"
322
+ f"Payment system: {card_data.get('paymentSystem') or '-'}\n"
323
+ f"Expiry: {card_data.get('expiryDate') or '-'}\n"
324
+ f"Daily limit: {format_limit(data.get('limit'))}\n"
325
+ f"Daily spend: {money(data.get('dailySpend'), 'USD')}\n"
326
+ f"Activated: {'yes' if data.get('activated') else 'no'}\n"
327
+ f"Wallet: {data.get('walletAddress') or '-'}\n"
328
+ f"Billing country: {billing.get('country') or '-'}",
329
+ title="Card Details",
330
+ border_style="cyan",
331
+ )
331
332
  )
332
- )
333
+
334
+ emit(data, OutputOptions(raw_json, yaml_output), _render)
333
335
 
334
336
 
335
337
  @app.command("reveal", help="Reveal sensitive card details (number, CVV, expiry) using a wallet signature.")
@@ -347,17 +349,18 @@ def reveal_card(
347
349
  if nonce is None:
348
350
  nonce = int(typer.prompt("Nonce").strip())
349
351
  data = AppApi().card_details(card_id, {"messageSignature": signature, "nonce": nonce})
350
- if raw_json or yaml_output:
351
- emit(data, OutputOptions(raw_json, yaml_output))
352
- return
353
- console.print(
354
- Panel.fit(
355
- f"Card number: [bold]{data.get('cardNumber')}[/bold]\n"
356
- f"CVV: [bold]{data.get('cvv')}[/bold]\n"
357
- f"Expiry: {data.get('expiryDate') or '-'}",
358
- title="Sensitive Card Details",
359
- border_style="yellow",
360
- )
352
+ emit(
353
+ data,
354
+ OutputOptions(raw_json, yaml_output),
355
+ lambda _: console.print(
356
+ Panel.fit(
357
+ f"Card number: [bold]{data.get('cardNumber')}[/bold]\n"
358
+ f"CVV: [bold]{data.get('cvv')}[/bold]\n"
359
+ f"Expiry: {data.get('expiryDate') or '-'}",
360
+ title="Sensitive Card Details",
361
+ border_style="yellow",
362
+ )
363
+ ),
361
364
  )
362
365
 
363
366
 
@@ -310,34 +310,35 @@ def get_contract(
310
310
  contract_id = _contract_id(ctx, contract_id_option or contract_id)
311
311
  api = AppApi()
312
312
  data = _with_income(api.get_contract(contract_id), str(api.current_user().get("id") or ""))
313
- if raw_json or yaml_output:
314
- emit(data, OutputOptions(raw_json, yaml_output))
315
- return
316
- # Contract-level metadata (the rest of the substance lives in `fields`).
317
- meta_keys = ("label", "status", "period", "activationAt", "expirationAt", "documentId", "paymentMethods")
318
- meta = Table(box=None, show_header=False, pad_edge=False)
319
- meta.add_column(style="bold", no_wrap=True)
320
- meta.add_column(overflow="fold")
321
- for key in meta_keys:
322
- meta.add_row(key, _render_field_value(data.get(key)))
323
-
324
- # Show every USER field the schema declares for this type even empty ones —
325
- # plus any extra stored fields, so the full expected set is visible.
326
- fields = data.get("fields") or {}
327
- template = next((t for t in api.list_contract_templates().get("templates", []) if t.get("type") == data.get("type")), None)
328
- declared = [f["key"] for f in (template or {}).get("fields", []) if f.get("source") == "user" and f.get("key")]
329
- ordered_keys = list(dict.fromkeys([*declared, *fields.keys()]))
330
-
331
- body: list[RenderableType] = [meta]
332
- if ordered_keys:
333
- fields_table = Table(title="Fields", title_justify="left", box=box.SIMPLE_HEAVY)
334
- fields_table.add_column("Field", no_wrap=True)
335
- fields_table.add_column("Value", overflow="fold")
336
- for key in ordered_keys:
337
- fields_table.add_row(str(key), _render_field_value(fields.get(key)))
338
- body.append(fields_table)
339
-
340
- console.print(Panel(Group(*body), title=f"Contract {contract_id}", border_style="cyan"))
313
+
314
+ def _render(_: dict[str, Any]) -> None:
315
+ # Contract-level metadata (the rest of the substance lives in `fields`).
316
+ meta_keys = ("label", "status", "period", "activationAt", "expirationAt", "documentId", "paymentMethods")
317
+ meta = Table(box=None, show_header=False, pad_edge=False)
318
+ meta.add_column(style="bold", no_wrap=True)
319
+ meta.add_column(overflow="fold")
320
+ for key in meta_keys:
321
+ meta.add_row(key, _render_field_value(data.get(key)))
322
+
323
+ # Show every USER field the schema declares for this type — even empty ones —
324
+ # plus any extra stored fields, so the full expected set is visible.
325
+ fields = data.get("fields") or {}
326
+ template = next((t for t in api.list_contract_templates().get("templates", []) if t.get("type") == data.get("type")), None)
327
+ declared = [f["key"] for f in (template or {}).get("fields", []) if f.get("source") == "user" and f.get("key")]
328
+ ordered_keys = list(dict.fromkeys([*declared, *fields.keys()]))
329
+
330
+ body: list[RenderableType] = [meta]
331
+ if ordered_keys:
332
+ fields_table = Table(title="Fields", title_justify="left", box=box.SIMPLE_HEAVY)
333
+ fields_table.add_column("Field", no_wrap=True)
334
+ fields_table.add_column("Value", overflow="fold")
335
+ for key in ordered_keys:
336
+ fields_table.add_row(str(key), _render_field_value(fields.get(key)))
337
+ body.append(fields_table)
338
+
339
+ console.print(Panel(Group(*body), title=f"Contract {contract_id}", border_style="cyan"))
340
+
341
+ emit(data, OutputOptions(raw_json, yaml_output), _render)
341
342
 
342
343
 
343
344
  def _render_field_value(value: Any) -> RenderableType:
@@ -492,10 +492,7 @@ def get_invoice(
492
492
  ) -> None:
493
493
  invoice_id = invoice_id_option or invoice_id or typer.prompt("Invoice id").strip()
494
494
  data = PublicApi().invoice(invoice_id) if public else AppApi().get_invoice(invoice_id)
495
- if raw_json or yaml_output:
496
- emit(data, OutputOptions(raw_json, yaml_output))
497
- return
498
- print_invoice_panel(invoice_id, data)
495
+ emit(data, OutputOptions(raw_json, yaml_output), lambda _: print_invoice_panel(invoice_id, data))
499
496
 
500
497
 
501
498
  @app.command("decline", help="Decline an incoming invoice.")
@@ -475,25 +475,26 @@ def list_transfers(
475
475
  ) -> None:
476
476
  data = AppApi().list_transactions(limit=limit, offset=offset)
477
477
  data = _mark_mcp_transactions(data, _mcp_wallet())
478
- if raw_json or yaml_output:
479
- emit(data, OutputOptions(raw_json, yaml_output))
480
- return
481
- table = Table(title="Transactions", box=box.SIMPLE_HEAVY)
482
- table.add_column("Time")
483
- table.add_column("Type")
484
- table.add_column("Amount")
485
- table.add_column("Counterparty")
486
- table.add_column("Status")
487
- table.add_column("MCP")
488
- table.add_column("Tx")
489
- for row in data.get("transactions", []):
490
- table.add_row(
491
- row.get("time", "-"),
492
- str(row.get("type") or "-"),
493
- _tx_amount(row),
494
- _tx_counterparty(row),
495
- str(row.get("status") or "-"),
496
- "true" if row.get("mcp") else "",
497
- str(row.get("txHash") or row.get("onchainTransactionId") or "-"),
498
- )
499
- console.print(table)
478
+
479
+ def _render(_: dict[str, Any]) -> None:
480
+ table = Table(title="Transactions", box=box.SIMPLE_HEAVY)
481
+ table.add_column("Time")
482
+ table.add_column("Type")
483
+ table.add_column("Amount")
484
+ table.add_column("Counterparty")
485
+ table.add_column("Status")
486
+ table.add_column("MCP")
487
+ table.add_column("Tx")
488
+ for row in data.get("transactions", []):
489
+ table.add_row(
490
+ row.get("time", "-"),
491
+ str(row.get("type") or "-"),
492
+ _tx_amount(row),
493
+ _tx_counterparty(row),
494
+ str(row.get("status") or "-"),
495
+ "true" if row.get("mcp") else "",
496
+ str(row.get("txHash") or row.get("onchainTransactionId") or "-"),
497
+ )
498
+ console.print(table)
499
+
500
+ emit(data, OutputOptions(raw_json, yaml_output), _render)
@@ -46,15 +46,8 @@ poetry run hevn --help
46
46
 
47
47
  If Poetry is configured without virtualenv creation, use a clean virtualenv or CI before release checks.
48
48
 
49
- For a user-facing `pipx` install before PyPI publication, use the GitHub VCS
50
- specifier instead of a local wheel:
51
-
52
- ```bash
53
- pipx install "git+https://github.com/hevn/hevn-cli.git"
54
- pipx upgrade hevn-cli
55
- ```
56
-
57
- After PyPI publication, use:
49
+ For a user-facing `pipx` install, use PyPI (the repository is private, so
50
+ GitHub VCS specifiers are not an option):
58
51
 
59
52
  ```bash
60
53
  pipx install hevn-cli
File without changes
File without changes
File without changes