@ktmcp-cli/billingo 1.0.0
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.
- package/.env.example +8 -0
- package/AGENT.md +447 -0
- package/CLI_SUMMARY.md +377 -0
- package/INDEX.md +364 -0
- package/INSTALL.sh +62 -0
- package/LICENSE +21 -0
- package/OPENCLAW.md +503 -0
- package/PROJECT_REPORT.md +462 -0
- package/QUICKSTART.md +212 -0
- package/README.md +378 -0
- package/STRUCTURE.txt +266 -0
- package/TESTING.md +513 -0
- package/banner.png +0 -0
- package/bin/billingo.js +75 -0
- package/examples/bank-account.json +8 -0
- package/examples/invoice.json +32 -0
- package/examples/partner.json +20 -0
- package/examples/product.json +10 -0
- package/logo.png +0 -0
- package/package.json +35 -0
- package/src/commands/bank-accounts.js +131 -0
- package/src/commands/config.js +73 -0
- package/src/commands/currencies.js +40 -0
- package/src/commands/document-blocks.js +40 -0
- package/src/commands/documents.js +248 -0
- package/src/commands/organization.js +35 -0
- package/src/commands/partners.js +130 -0
- package/src/commands/products.js +130 -0
- package/src/commands/utilities.js +34 -0
- package/src/lib/api.js +160 -0
- package/src/lib/auth.js +32 -0
- package/src/lib/config.js +87 -0
package/OPENCLAW.md
ADDED
|
@@ -0,0 +1,503 @@
|
|
|
1
|
+
# Billingo CLI - OpenClaw Integration Guide
|
|
2
|
+
|
|
3
|
+
This guide shows how to integrate the Billingo CLI with OpenClaw for AI-driven invoicing automation.
|
|
4
|
+
|
|
5
|
+
## What is OpenClaw?
|
|
6
|
+
|
|
7
|
+
OpenClaw is a framework for building AI agents with tool access. Instead of using MCP (Model Context Protocol), OpenClaw embraces the Unix philosophy: CLI tools are better than proprietary abstractions.
|
|
8
|
+
|
|
9
|
+
## Why This CLI Works Perfectly with OpenClaw
|
|
10
|
+
|
|
11
|
+
1. **Zero Configuration**: No server to run, no MCP gateway to configure
|
|
12
|
+
2. **Standard Interface**: Uses stdin/stdout/stderr like any Unix tool
|
|
13
|
+
3. **Structured Output**: JSON format for machine parsing, pretty format for humans
|
|
14
|
+
4. **Self-Documenting**: `--help` text provides complete API documentation
|
|
15
|
+
5. **Composable**: Works with pipes, scripts, and other CLI tools
|
|
16
|
+
|
|
17
|
+
## Installation for OpenClaw
|
|
18
|
+
|
|
19
|
+
```bash
|
|
20
|
+
# Install the CLI
|
|
21
|
+
npm install -g @ktmcp-cli/billingo
|
|
22
|
+
|
|
23
|
+
# Configure API key
|
|
24
|
+
billingo config set apiKey YOUR_API_KEY
|
|
25
|
+
|
|
26
|
+
# Verify installation
|
|
27
|
+
billingo organization get
|
|
28
|
+
```
|
|
29
|
+
|
|
30
|
+
## Tool Configuration
|
|
31
|
+
|
|
32
|
+
### Option 1: Direct Shell Access
|
|
33
|
+
|
|
34
|
+
If your OpenClaw agent has shell access, it can use the CLI directly:
|
|
35
|
+
|
|
36
|
+
```python
|
|
37
|
+
# In your OpenClaw agent
|
|
38
|
+
result = await shell.execute("billingo documents list --format json")
|
|
39
|
+
documents = json.loads(result.stdout)
|
|
40
|
+
```
|
|
41
|
+
|
|
42
|
+
### Option 2: Wrapped Tool
|
|
43
|
+
|
|
44
|
+
Create a Python wrapper for type-safe access:
|
|
45
|
+
|
|
46
|
+
```python
|
|
47
|
+
# tools/billingo.py
|
|
48
|
+
import json
|
|
49
|
+
import subprocess
|
|
50
|
+
from typing import List, Dict, Any, Optional
|
|
51
|
+
|
|
52
|
+
class BillingoTool:
|
|
53
|
+
"""Billingo API access via CLI"""
|
|
54
|
+
|
|
55
|
+
def __init__(self, api_key: Optional[str] = None):
|
|
56
|
+
if api_key:
|
|
57
|
+
subprocess.run(["billingo", "config", "set", "apiKey", api_key])
|
|
58
|
+
|
|
59
|
+
def _run(self, args: List[str], format: str = "json") -> Any:
|
|
60
|
+
"""Execute CLI command and return parsed result"""
|
|
61
|
+
cmd = ["billingo"] + args + ["--format", format]
|
|
62
|
+
result = subprocess.run(cmd, capture_output=True, text=True)
|
|
63
|
+
|
|
64
|
+
if result.returncode != 0:
|
|
65
|
+
raise Exception(f"Billingo CLI error: {result.stderr}")
|
|
66
|
+
|
|
67
|
+
if format == "json":
|
|
68
|
+
return json.loads(result.stdout)
|
|
69
|
+
return result.stdout
|
|
70
|
+
|
|
71
|
+
# Documents
|
|
72
|
+
def list_documents(self, page: int = 1, per_page: int = 25, **filters) -> Dict:
|
|
73
|
+
"""List all documents with optional filters"""
|
|
74
|
+
args = ["documents", "list", "--page", str(page), "--per-page", str(per_page)]
|
|
75
|
+
|
|
76
|
+
# Add filters
|
|
77
|
+
if "partner_id" in filters:
|
|
78
|
+
args += ["--partner-id", str(filters["partner_id"])]
|
|
79
|
+
if "payment_status" in filters:
|
|
80
|
+
args += ["--payment-status", filters["payment_status"]]
|
|
81
|
+
if "start_date" in filters:
|
|
82
|
+
args += ["--start-date", filters["start_date"]]
|
|
83
|
+
if "end_date" in filters:
|
|
84
|
+
args += ["--end-date", filters["end_date"]]
|
|
85
|
+
|
|
86
|
+
return self._run(args)
|
|
87
|
+
|
|
88
|
+
def get_document(self, doc_id: int) -> Dict:
|
|
89
|
+
"""Get document by ID"""
|
|
90
|
+
return self._run(["documents", "get", str(doc_id)])
|
|
91
|
+
|
|
92
|
+
def create_document(self, data: Dict) -> Dict:
|
|
93
|
+
"""Create a new document"""
|
|
94
|
+
import tempfile
|
|
95
|
+
with tempfile.NamedTemporaryFile(mode='w', suffix='.json', delete=False) as f:
|
|
96
|
+
json.dump(data, f)
|
|
97
|
+
f.flush()
|
|
98
|
+
result = self._run(["documents", "create", "--file", f.name])
|
|
99
|
+
return result
|
|
100
|
+
|
|
101
|
+
def cancel_document(self, doc_id: int) -> Dict:
|
|
102
|
+
"""Cancel a document"""
|
|
103
|
+
return self._run(["documents", "cancel", str(doc_id)])
|
|
104
|
+
|
|
105
|
+
def download_document(self, doc_id: int, output_path: str) -> str:
|
|
106
|
+
"""Download document PDF"""
|
|
107
|
+
self._run(["documents", "download", str(doc_id), "--output", output_path], format="pretty")
|
|
108
|
+
return output_path
|
|
109
|
+
|
|
110
|
+
def send_document(self, doc_id: int, emails: List[str]) -> Dict:
|
|
111
|
+
"""Send document via email"""
|
|
112
|
+
return self._run(["documents", "send", str(doc_id), "--emails", ",".join(emails)])
|
|
113
|
+
|
|
114
|
+
# Partners
|
|
115
|
+
def list_partners(self, page: int = 1, per_page: int = 25) -> Dict:
|
|
116
|
+
"""List all partners"""
|
|
117
|
+
return self._run(["partners", "list", "--page", str(page), "--per-page", str(per_page)])
|
|
118
|
+
|
|
119
|
+
def get_partner(self, partner_id: int) -> Dict:
|
|
120
|
+
"""Get partner by ID"""
|
|
121
|
+
return self._run(["partners", "get", str(partner_id)])
|
|
122
|
+
|
|
123
|
+
def create_partner(self, data: Dict) -> Dict:
|
|
124
|
+
"""Create a new partner"""
|
|
125
|
+
import tempfile
|
|
126
|
+
with tempfile.NamedTemporaryFile(mode='w', suffix='.json', delete=False) as f:
|
|
127
|
+
json.dump(data, f)
|
|
128
|
+
f.flush()
|
|
129
|
+
result = self._run(["partners", "create", "--file", f.name])
|
|
130
|
+
return result
|
|
131
|
+
|
|
132
|
+
# Products
|
|
133
|
+
def list_products(self, page: int = 1, per_page: int = 25) -> Dict:
|
|
134
|
+
"""List all products"""
|
|
135
|
+
return self._run(["products", "list", "--page", str(page), "--per-page", str(per_page)])
|
|
136
|
+
|
|
137
|
+
def create_product(self, data: Dict) -> Dict:
|
|
138
|
+
"""Create a new product"""
|
|
139
|
+
import tempfile
|
|
140
|
+
with tempfile.NamedTemporaryFile(mode='w', suffix='.json', delete=False) as f:
|
|
141
|
+
json.dump(data, f)
|
|
142
|
+
f.flush()
|
|
143
|
+
result = self._run(["products", "create", "--file", f.name])
|
|
144
|
+
return result
|
|
145
|
+
|
|
146
|
+
# Bank Accounts
|
|
147
|
+
def list_bank_accounts(self, page: int = 1, per_page: int = 25) -> Dict:
|
|
148
|
+
"""List all bank accounts"""
|
|
149
|
+
return self._run(["bank-accounts", "list", "--page", str(page), "--per-page", str(per_page)])
|
|
150
|
+
|
|
151
|
+
# Currencies
|
|
152
|
+
def convert_currency(self, from_currency: str, to_currency: str) -> Dict:
|
|
153
|
+
"""Get currency conversion rate"""
|
|
154
|
+
return self._run(["currencies", "convert", "--from", from_currency, "--to", to_currency])
|
|
155
|
+
|
|
156
|
+
# Organization
|
|
157
|
+
def get_organization(self) -> Dict:
|
|
158
|
+
"""Get organization data"""
|
|
159
|
+
return self._run(["organization", "get"])
|
|
160
|
+
```
|
|
161
|
+
|
|
162
|
+
## Example OpenClaw Agent
|
|
163
|
+
|
|
164
|
+
Here's a complete example of an OpenClaw agent that handles invoicing:
|
|
165
|
+
|
|
166
|
+
```python
|
|
167
|
+
# agents/invoice_agent.py
|
|
168
|
+
from openclaw import Agent, tool
|
|
169
|
+
from tools.billingo import BillingoTool
|
|
170
|
+
|
|
171
|
+
class InvoiceAgent(Agent):
|
|
172
|
+
"""AI agent for managing invoices via Billingo"""
|
|
173
|
+
|
|
174
|
+
def __init__(self):
|
|
175
|
+
super().__init__()
|
|
176
|
+
self.billingo = BillingoTool()
|
|
177
|
+
|
|
178
|
+
@tool
|
|
179
|
+
async def create_invoice(
|
|
180
|
+
self,
|
|
181
|
+
customer_name: str,
|
|
182
|
+
customer_email: str,
|
|
183
|
+
items: List[Dict],
|
|
184
|
+
due_days: int = 15
|
|
185
|
+
) -> str:
|
|
186
|
+
"""
|
|
187
|
+
Create and send an invoice to a customer.
|
|
188
|
+
|
|
189
|
+
Args:
|
|
190
|
+
customer_name: Customer company name
|
|
191
|
+
customer_email: Customer email address
|
|
192
|
+
items: List of invoice items [{"name": "...", "quantity": 1, "unit_price": 100, ...}]
|
|
193
|
+
due_days: Days until payment is due (default: 15)
|
|
194
|
+
|
|
195
|
+
Returns:
|
|
196
|
+
Invoice ID and public URL
|
|
197
|
+
"""
|
|
198
|
+
from datetime import datetime, timedelta
|
|
199
|
+
|
|
200
|
+
# Find or create partner
|
|
201
|
+
partners = self.billingo.list_partners(per_page=100)
|
|
202
|
+
partner = None
|
|
203
|
+
for p in partners.get("data", []):
|
|
204
|
+
if p["name"] == customer_name:
|
|
205
|
+
partner = p
|
|
206
|
+
break
|
|
207
|
+
|
|
208
|
+
if not partner:
|
|
209
|
+
# Create new partner
|
|
210
|
+
partner = self.billingo.create_partner({
|
|
211
|
+
"name": customer_name,
|
|
212
|
+
"emails": [customer_email],
|
|
213
|
+
"address": {
|
|
214
|
+
"country_code": "HU",
|
|
215
|
+
"post_code": "1234",
|
|
216
|
+
"city": "Budapest",
|
|
217
|
+
"address": "Address TBD"
|
|
218
|
+
}
|
|
219
|
+
})
|
|
220
|
+
|
|
221
|
+
# Get organization details for vendor_id
|
|
222
|
+
org = self.billingo.get_organization()
|
|
223
|
+
vendor_id = org["id"]
|
|
224
|
+
|
|
225
|
+
# Get first document block
|
|
226
|
+
blocks = self.billingo._run(["document-blocks", "list"])
|
|
227
|
+
block_id = blocks["data"][0]["id"]
|
|
228
|
+
|
|
229
|
+
# Prepare invoice data
|
|
230
|
+
today = datetime.now().date()
|
|
231
|
+
due_date = today + timedelta(days=due_days)
|
|
232
|
+
|
|
233
|
+
invoice_data = {
|
|
234
|
+
"vendor_id": vendor_id,
|
|
235
|
+
"partner_id": partner["id"],
|
|
236
|
+
"block_id": block_id,
|
|
237
|
+
"type": "invoice",
|
|
238
|
+
"fulfillment_date": today.isoformat(),
|
|
239
|
+
"due_date": due_date.isoformat(),
|
|
240
|
+
"payment_method": "transfer",
|
|
241
|
+
"language": "en",
|
|
242
|
+
"currency": "HUF",
|
|
243
|
+
"items": items
|
|
244
|
+
}
|
|
245
|
+
|
|
246
|
+
# Create invoice
|
|
247
|
+
invoice = self.billingo.create_document(invoice_data)
|
|
248
|
+
invoice_id = invoice["id"]
|
|
249
|
+
|
|
250
|
+
# Send to customer
|
|
251
|
+
self.billingo.send_document(invoice_id, [customer_email])
|
|
252
|
+
|
|
253
|
+
# Get public URL
|
|
254
|
+
public_url = self.billingo._run(["documents", "public-url", str(invoice_id)])
|
|
255
|
+
|
|
256
|
+
return f"Invoice {invoice_id} created and sent to {customer_email}. " \
|
|
257
|
+
f"Public URL: {public_url['public_url']}"
|
|
258
|
+
|
|
259
|
+
@tool
|
|
260
|
+
async def list_unpaid_invoices(self) -> str:
|
|
261
|
+
"""List all unpaid invoices"""
|
|
262
|
+
docs = self.billingo.list_documents(
|
|
263
|
+
per_page=100,
|
|
264
|
+
payment_status="unpaid"
|
|
265
|
+
)
|
|
266
|
+
|
|
267
|
+
if not docs.get("data"):
|
|
268
|
+
return "No unpaid invoices found."
|
|
269
|
+
|
|
270
|
+
result = "Unpaid Invoices:\n\n"
|
|
271
|
+
for doc in docs["data"]:
|
|
272
|
+
result += f"- Invoice {doc['invoice_number']}: {doc['partner_name']} - " \
|
|
273
|
+
f"{doc['total_gross']} {doc['currency']} (Due: {doc['due_date']})\n"
|
|
274
|
+
|
|
275
|
+
return result
|
|
276
|
+
|
|
277
|
+
@tool
|
|
278
|
+
async def send_invoice_reminder(self, invoice_id: int) -> str:
|
|
279
|
+
"""Send a payment reminder for an invoice"""
|
|
280
|
+
# Get invoice details
|
|
281
|
+
doc = self.billingo.get_document(invoice_id)
|
|
282
|
+
|
|
283
|
+
# Send reminder
|
|
284
|
+
emails = [email for email in doc["partner"]["emails"]]
|
|
285
|
+
self.billingo.send_document(invoice_id, emails)
|
|
286
|
+
|
|
287
|
+
return f"Reminder sent for invoice {doc['invoice_number']} to {', '.join(emails)}"
|
|
288
|
+
|
|
289
|
+
@tool
|
|
290
|
+
async def get_invoice_status(self, invoice_number: str) -> str:
|
|
291
|
+
"""Get the status of an invoice by number"""
|
|
292
|
+
# Search for invoice
|
|
293
|
+
docs = self.billingo.list_documents(per_page=100)
|
|
294
|
+
|
|
295
|
+
for doc in docs.get("data", []):
|
|
296
|
+
if doc["invoice_number"] == invoice_number:
|
|
297
|
+
return f"Invoice {invoice_number}:\n" \
|
|
298
|
+
f"Customer: {doc['partner_name']}\n" \
|
|
299
|
+
f"Amount: {doc['total_gross']} {doc['currency']}\n" \
|
|
300
|
+
f"Status: {doc['payment_status']}\n" \
|
|
301
|
+
f"Due: {doc['due_date']}"
|
|
302
|
+
|
|
303
|
+
return f"Invoice {invoice_number} not found."
|
|
304
|
+
```
|
|
305
|
+
|
|
306
|
+
## Usage Example
|
|
307
|
+
|
|
308
|
+
```python
|
|
309
|
+
# main.py
|
|
310
|
+
from agents.invoice_agent import InvoiceAgent
|
|
311
|
+
|
|
312
|
+
async def main():
|
|
313
|
+
agent = InvoiceAgent()
|
|
314
|
+
|
|
315
|
+
# User request: "Create an invoice for Acme Corp"
|
|
316
|
+
response = await agent.run(
|
|
317
|
+
"Create an invoice for Acme Corp (acme@example.com) for 10 hours of consulting at 50000 HUF/hour with 27% VAT"
|
|
318
|
+
)
|
|
319
|
+
|
|
320
|
+
print(response)
|
|
321
|
+
# Output: Invoice 123 created and sent to acme@example.com. Public URL: https://...
|
|
322
|
+
|
|
323
|
+
if __name__ == "__main__":
|
|
324
|
+
import asyncio
|
|
325
|
+
asyncio.run(main())
|
|
326
|
+
```
|
|
327
|
+
|
|
328
|
+
## Environment Variables
|
|
329
|
+
|
|
330
|
+
Set these for containerized deployments:
|
|
331
|
+
|
|
332
|
+
```bash
|
|
333
|
+
export BILLINGO_API_KEY=your_api_key_here
|
|
334
|
+
export BILLINGO_BASE_URL=https://api.billingo.hu/v3
|
|
335
|
+
```
|
|
336
|
+
|
|
337
|
+
The CLI will automatically use these if no config is set.
|
|
338
|
+
|
|
339
|
+
## Docker Integration
|
|
340
|
+
|
|
341
|
+
```dockerfile
|
|
342
|
+
FROM node:18-alpine
|
|
343
|
+
|
|
344
|
+
# Install CLI
|
|
345
|
+
RUN npm install -g @ktmcp-cli/billingo
|
|
346
|
+
|
|
347
|
+
# Install Python for OpenClaw
|
|
348
|
+
RUN apk add --no-cache python3 py3-pip
|
|
349
|
+
|
|
350
|
+
# Copy your OpenClaw agent
|
|
351
|
+
COPY . /app
|
|
352
|
+
WORKDIR /app
|
|
353
|
+
|
|
354
|
+
# Install Python dependencies
|
|
355
|
+
RUN pip3 install -r requirements.txt
|
|
356
|
+
|
|
357
|
+
# Set environment
|
|
358
|
+
ENV BILLINGO_API_KEY=${BILLINGO_API_KEY}
|
|
359
|
+
|
|
360
|
+
# Run your agent
|
|
361
|
+
CMD ["python3", "main.py"]
|
|
362
|
+
```
|
|
363
|
+
|
|
364
|
+
## Error Handling
|
|
365
|
+
|
|
366
|
+
The wrapper should handle CLI errors gracefully:
|
|
367
|
+
|
|
368
|
+
```python
|
|
369
|
+
def _run(self, args: List[str], format: str = "json") -> Any:
|
|
370
|
+
"""Execute CLI command with error handling"""
|
|
371
|
+
cmd = ["billingo"] + args + ["--format", format]
|
|
372
|
+
result = subprocess.run(cmd, capture_output=True, text=True)
|
|
373
|
+
|
|
374
|
+
if result.returncode != 0:
|
|
375
|
+
error_msg = result.stderr
|
|
376
|
+
|
|
377
|
+
# Parse specific errors
|
|
378
|
+
if "API key not configured" in error_msg:
|
|
379
|
+
raise ValueError("Billingo API key not configured")
|
|
380
|
+
elif "Rate limit exceeded" in error_msg:
|
|
381
|
+
raise RateLimitError("API rate limit exceeded")
|
|
382
|
+
elif "Resource not found" in error_msg:
|
|
383
|
+
raise NotFoundError("Resource not found")
|
|
384
|
+
else:
|
|
385
|
+
raise Exception(f"Billingo API error: {error_msg}")
|
|
386
|
+
|
|
387
|
+
if format == "json":
|
|
388
|
+
try:
|
|
389
|
+
return json.loads(result.stdout)
|
|
390
|
+
except json.JSONDecodeError:
|
|
391
|
+
raise Exception(f"Invalid JSON response: {result.stdout}")
|
|
392
|
+
|
|
393
|
+
return result.stdout
|
|
394
|
+
```
|
|
395
|
+
|
|
396
|
+
## Advanced: Streaming Output
|
|
397
|
+
|
|
398
|
+
For long-running operations, stream output to the user:
|
|
399
|
+
|
|
400
|
+
```python
|
|
401
|
+
import subprocess
|
|
402
|
+
|
|
403
|
+
def create_invoice_with_progress(data: Dict) -> Dict:
|
|
404
|
+
"""Create invoice with progress updates"""
|
|
405
|
+
import tempfile
|
|
406
|
+
|
|
407
|
+
with tempfile.NamedTemporaryFile(mode='w', suffix='.json', delete=False) as f:
|
|
408
|
+
json.dump(data, f)
|
|
409
|
+
f.flush()
|
|
410
|
+
|
|
411
|
+
# Stream output line by line
|
|
412
|
+
process = subprocess.Popen(
|
|
413
|
+
["billingo", "documents", "create", "--file", f.name],
|
|
414
|
+
stdout=subprocess.PIPE,
|
|
415
|
+
stderr=subprocess.PIPE,
|
|
416
|
+
text=True
|
|
417
|
+
)
|
|
418
|
+
|
|
419
|
+
# Read stderr for progress (ora spinner output)
|
|
420
|
+
for line in process.stderr:
|
|
421
|
+
print(f"Progress: {line.strip()}")
|
|
422
|
+
|
|
423
|
+
stdout, _ = process.communicate()
|
|
424
|
+
|
|
425
|
+
if process.returncode != 0:
|
|
426
|
+
raise Exception("Invoice creation failed")
|
|
427
|
+
|
|
428
|
+
return json.loads(stdout)
|
|
429
|
+
```
|
|
430
|
+
|
|
431
|
+
## Testing
|
|
432
|
+
|
|
433
|
+
Test your integration without API calls:
|
|
434
|
+
|
|
435
|
+
```python
|
|
436
|
+
import unittest
|
|
437
|
+
from unittest.mock import patch, MagicMock
|
|
438
|
+
|
|
439
|
+
class TestBillingoTool(unittest.TestCase):
|
|
440
|
+
@patch('subprocess.run')
|
|
441
|
+
def test_list_documents(self, mock_run):
|
|
442
|
+
# Mock CLI response
|
|
443
|
+
mock_run.return_value = MagicMock(
|
|
444
|
+
returncode=0,
|
|
445
|
+
stdout='{"data":[{"id":123}]}'
|
|
446
|
+
)
|
|
447
|
+
|
|
448
|
+
tool = BillingoTool()
|
|
449
|
+
result = tool.list_documents()
|
|
450
|
+
|
|
451
|
+
self.assertEqual(result["data"][0]["id"], 123)
|
|
452
|
+
mock_run.assert_called_once()
|
|
453
|
+
```
|
|
454
|
+
|
|
455
|
+
## Best Practices
|
|
456
|
+
|
|
457
|
+
1. **Cache Organization Data**: Call `get_organization()` once and cache the result
|
|
458
|
+
2. **Reuse Partner IDs**: Search for existing partners before creating new ones
|
|
459
|
+
3. **Handle Rate Limits**: Implement exponential backoff for batch operations
|
|
460
|
+
4. **Validate Before Creating**: Check data structure before calling create commands
|
|
461
|
+
5. **Store Invoice PDFs**: Download and archive PDFs for record-keeping
|
|
462
|
+
6. **Use Transactions**: Group related operations (create partner → create invoice → send)
|
|
463
|
+
7. **Log Everything**: Keep audit logs of all invoice operations
|
|
464
|
+
|
|
465
|
+
## Troubleshooting
|
|
466
|
+
|
|
467
|
+
### CLI Not Found
|
|
468
|
+
|
|
469
|
+
```bash
|
|
470
|
+
# Check installation
|
|
471
|
+
which billingo
|
|
472
|
+
|
|
473
|
+
# Reinstall if needed
|
|
474
|
+
npm install -g @ktmcp-cli/billingo
|
|
475
|
+
```
|
|
476
|
+
|
|
477
|
+
### API Key Issues
|
|
478
|
+
|
|
479
|
+
```bash
|
|
480
|
+
# Verify API key is set
|
|
481
|
+
billingo config get apiKey
|
|
482
|
+
|
|
483
|
+
# Test connection
|
|
484
|
+
billingo organization get
|
|
485
|
+
```
|
|
486
|
+
|
|
487
|
+
### Permission Errors
|
|
488
|
+
|
|
489
|
+
Make sure the OpenClaw agent has execute permissions:
|
|
490
|
+
|
|
491
|
+
```bash
|
|
492
|
+
chmod +x $(which billingo)
|
|
493
|
+
```
|
|
494
|
+
|
|
495
|
+
## Further Reading
|
|
496
|
+
|
|
497
|
+
- [README.md](./README.md) - General CLI usage
|
|
498
|
+
- [AGENT.md](./AGENT.md) - AI agent usage patterns
|
|
499
|
+
- [Billingo API Docs](https://api.billingo.hu/v3/swagger)
|
|
500
|
+
|
|
501
|
+
## Support
|
|
502
|
+
|
|
503
|
+
For issues specific to OpenClaw integration, please check the OpenClaw documentation or open an issue in the CLI repository.
|