olympay 0.1.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/README.md +407 -0
- package/dist/cli.js +322 -0
- package/package.json +33 -0
package/README.md
ADDED
|
@@ -0,0 +1,407 @@
|
|
|
1
|
+
# olympay
|
|
2
|
+
|
|
3
|
+
> CLI for Olympay — financial control for autonomous AI agents.
|
|
4
|
+
|
|
5
|
+
```
|
|
6
|
+
██████╗ ██╗ ██╗ ██╗███╗ ███╗██████╗ █████╗ ██╗ ██╗
|
|
7
|
+
██╔═══██╗██║ ╚██╗ ██╔╝████╗ ████║██╔══██╗██╔══██╗╚██╗ ██╔╝
|
|
8
|
+
██║ ██║██║ ╚████╔╝ ██╔████╔██║██████╔╝███████║ ╚████╔╝
|
|
9
|
+
██║ ██║██║ ╚██╔╝ ██║╚██╔╝██║██╔═══╝ ██╔══██║ ╚██╔╝
|
|
10
|
+
╚██████╔╝███████╗ ██║ ██║ ╚═╝ ██║██║ ██║ ██║ ██║
|
|
11
|
+
╚═════╝ ╚══════╝ ╚═╝ ╚═╝ ╚═╝╚═╝ ╚═╝ ╚═╝ ╚═╝
|
|
12
|
+
Financial control for autonomous AI agents • olympay.tech
|
|
13
|
+
```
|
|
14
|
+
|
|
15
|
+
## Installation
|
|
16
|
+
|
|
17
|
+
```bash
|
|
18
|
+
npm install -g olympay
|
|
19
|
+
```
|
|
20
|
+
|
|
21
|
+
Requires Node.js 18 or later.
|
|
22
|
+
|
|
23
|
+
## Getting Started
|
|
24
|
+
|
|
25
|
+
### 1. Create an account
|
|
26
|
+
|
|
27
|
+
Sign up at [olympay.tech](https://olympay.tech), go to **API** in the sidebar, and click **Generate Key** to create a workspace API key.
|
|
28
|
+
|
|
29
|
+
### 2. Authenticate
|
|
30
|
+
|
|
31
|
+
```bash
|
|
32
|
+
olympay login --key olympay_ws_YOUR_KEY
|
|
33
|
+
```
|
|
34
|
+
|
|
35
|
+
### 3. Spawn an agent
|
|
36
|
+
|
|
37
|
+
```bash
|
|
38
|
+
olympay agent create --name "my-agent"
|
|
39
|
+
```
|
|
40
|
+
|
|
41
|
+
This returns your agent's ID and its dedicated API key. Store both securely.
|
|
42
|
+
|
|
43
|
+
---
|
|
44
|
+
|
|
45
|
+
## Command Reference
|
|
46
|
+
|
|
47
|
+
### `olympay login`
|
|
48
|
+
|
|
49
|
+
Authenticate with a workspace API key.
|
|
50
|
+
|
|
51
|
+
```bash
|
|
52
|
+
olympay login --key <workspace_key>
|
|
53
|
+
```
|
|
54
|
+
|
|
55
|
+
| Flag | Required | Description |
|
|
56
|
+
|---|---|---|
|
|
57
|
+
| `--key` | Yes | Workspace API key starting with `olympay_ws_` |
|
|
58
|
+
| `--api` | No | Override the API base URL (default: `https://api.olympay.tech/v1`) |
|
|
59
|
+
|
|
60
|
+
Credentials are saved to `~/.olympay/config.json`.
|
|
61
|
+
|
|
62
|
+
---
|
|
63
|
+
|
|
64
|
+
### `olympay logout`
|
|
65
|
+
|
|
66
|
+
Remove stored credentials.
|
|
67
|
+
|
|
68
|
+
```bash
|
|
69
|
+
olympay logout
|
|
70
|
+
```
|
|
71
|
+
|
|
72
|
+
---
|
|
73
|
+
|
|
74
|
+
### `olympay whoami`
|
|
75
|
+
|
|
76
|
+
Show the currently authenticated workspace API key (masked).
|
|
77
|
+
|
|
78
|
+
```bash
|
|
79
|
+
olympay whoami
|
|
80
|
+
```
|
|
81
|
+
|
|
82
|
+
---
|
|
83
|
+
|
|
84
|
+
### `olympay agent create`
|
|
85
|
+
|
|
86
|
+
Spawn a new AI agent. Returns the agent's ID and its dedicated API key.
|
|
87
|
+
|
|
88
|
+
```bash
|
|
89
|
+
olympay agent create --name "billing-bot" --description "Handles recurring billing"
|
|
90
|
+
```
|
|
91
|
+
|
|
92
|
+
| Flag | Required | Description |
|
|
93
|
+
|---|---|---|
|
|
94
|
+
| `--name` | Yes | Display name for the agent |
|
|
95
|
+
| `--description` | No | Optional description |
|
|
96
|
+
|
|
97
|
+
**Output:**
|
|
98
|
+
```
|
|
99
|
+
Agent spawned successfully!
|
|
100
|
+
────────────────────────────────────────────────
|
|
101
|
+
ID: agt_01j9k2...
|
|
102
|
+
Name: billing-bot
|
|
103
|
+
API Key: olympay_agt_xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx
|
|
104
|
+
Status: active
|
|
105
|
+
────────────────────────────────────────────────
|
|
106
|
+
Keep this API key safe - use it to authenticate transactions.
|
|
107
|
+
```
|
|
108
|
+
|
|
109
|
+
---
|
|
110
|
+
|
|
111
|
+
### `olympay agent list`
|
|
112
|
+
|
|
113
|
+
List all agents in your workspace.
|
|
114
|
+
|
|
115
|
+
```bash
|
|
116
|
+
olympay agent list
|
|
117
|
+
```
|
|
118
|
+
|
|
119
|
+
---
|
|
120
|
+
|
|
121
|
+
### `olympay agent suspend`
|
|
122
|
+
|
|
123
|
+
Suspend an agent. All transactions from a suspended agent are immediately blocked.
|
|
124
|
+
|
|
125
|
+
```bash
|
|
126
|
+
olympay agent suspend AGENT_ID
|
|
127
|
+
```
|
|
128
|
+
|
|
129
|
+
---
|
|
130
|
+
|
|
131
|
+
### `olympay agent activate`
|
|
132
|
+
|
|
133
|
+
Re-activate a previously suspended agent.
|
|
134
|
+
|
|
135
|
+
```bash
|
|
136
|
+
olympay agent activate AGENT_ID
|
|
137
|
+
```
|
|
138
|
+
|
|
139
|
+
---
|
|
140
|
+
|
|
141
|
+
### `olympay account create`
|
|
142
|
+
|
|
143
|
+
Open a ledger account for an agent.
|
|
144
|
+
|
|
145
|
+
```bash
|
|
146
|
+
olympay account create --agent AGENT_ID --name "main" --currency USD
|
|
147
|
+
```
|
|
148
|
+
|
|
149
|
+
| Flag | Required | Description |
|
|
150
|
+
|---|---|---|
|
|
151
|
+
| `--agent` | Yes | Agent ID to associate the account with |
|
|
152
|
+
| `--name` | Yes | Account name |
|
|
153
|
+
| `--currency` | No | Currency code (default: `USD`) |
|
|
154
|
+
|
|
155
|
+
---
|
|
156
|
+
|
|
157
|
+
### `olympay account list`
|
|
158
|
+
|
|
159
|
+
List all accounts in your workspace, including balances.
|
|
160
|
+
|
|
161
|
+
```bash
|
|
162
|
+
olympay account list
|
|
163
|
+
```
|
|
164
|
+
|
|
165
|
+
---
|
|
166
|
+
|
|
167
|
+
### `olympay card issue`
|
|
168
|
+
|
|
169
|
+
Issue a virtual card linked to an agent account.
|
|
170
|
+
|
|
171
|
+
```bash
|
|
172
|
+
olympay card issue --agent AGENT_ID --account ACCOUNT_ID --brand VISA
|
|
173
|
+
```
|
|
174
|
+
|
|
175
|
+
| Flag | Required | Description |
|
|
176
|
+
|---|---|---|
|
|
177
|
+
| `--agent` | Yes | Agent ID |
|
|
178
|
+
| `--account` | Yes | Account ID to link the card to |
|
|
179
|
+
| `--brand` | No | Card brand (default: `VISA`) |
|
|
180
|
+
| `--last4` | No | Optional last 4 digits |
|
|
181
|
+
|
|
182
|
+
---
|
|
183
|
+
|
|
184
|
+
### `olympay card list`
|
|
185
|
+
|
|
186
|
+
List all virtual cards in your workspace.
|
|
187
|
+
|
|
188
|
+
```bash
|
|
189
|
+
olympay card list
|
|
190
|
+
```
|
|
191
|
+
|
|
192
|
+
---
|
|
193
|
+
|
|
194
|
+
### `olympay policy list`
|
|
195
|
+
|
|
196
|
+
List all spending policies in your workspace.
|
|
197
|
+
|
|
198
|
+
```bash
|
|
199
|
+
olympay policy list
|
|
200
|
+
```
|
|
201
|
+
|
|
202
|
+
---
|
|
203
|
+
|
|
204
|
+
### `olympay policy create`
|
|
205
|
+
|
|
206
|
+
Create a new spending policy.
|
|
207
|
+
|
|
208
|
+
```bash
|
|
209
|
+
olympay policy create --name "daily-cap" --type SPEND_LIMIT --config '{"maxAmountMinor":10000}'
|
|
210
|
+
```
|
|
211
|
+
|
|
212
|
+
| Flag | Required | Description |
|
|
213
|
+
|---|---|---|
|
|
214
|
+
| `--name` | Yes | Policy name |
|
|
215
|
+
| `--type` | Yes | Policy type (see [Policy Types](#policy-types)) |
|
|
216
|
+
| `--config` | No | JSON config object (depends on policy type) |
|
|
217
|
+
| `--description` | No | Optional description |
|
|
218
|
+
|
|
219
|
+
**Config by type:**
|
|
220
|
+
|
|
221
|
+
| Type | Config keys |
|
|
222
|
+
|---|---|
|
|
223
|
+
| `SPEND_LIMIT` | `maxAmountMinor` (integer, minor currency units) |
|
|
224
|
+
| `MERCHANT_ALLOWLIST` | `merchantIds` (array of strings) |
|
|
225
|
+
| `MERCHANT_BLOCKLIST` | `merchantIds` (array of strings) |
|
|
226
|
+
| `APPROVAL_REQUIRED` | No config needed |
|
|
227
|
+
| `TIME_WINDOW` | `startHour`, `endHour` (0-23, UTC) |
|
|
228
|
+
|
|
229
|
+
---
|
|
230
|
+
|
|
231
|
+
### `olympay policy assign`
|
|
232
|
+
|
|
233
|
+
Assign a policy to an agent, account, or card.
|
|
234
|
+
|
|
235
|
+
```bash
|
|
236
|
+
olympay policy assign --policy POLICY_ID --target-type AGENT --target AGENT_ID
|
|
237
|
+
```
|
|
238
|
+
|
|
239
|
+
| Flag | Required | Description |
|
|
240
|
+
|---|---|---|
|
|
241
|
+
| `--policy` | Yes | Policy ID |
|
|
242
|
+
| `--target-type` | Yes | `AGENT`, `ACCOUNT`, or `CARD` |
|
|
243
|
+
| `--target` | Yes | ID of the target entity |
|
|
244
|
+
| `--priority` | No | Evaluation priority (lower = higher, default: `100`) |
|
|
245
|
+
|
|
246
|
+
---
|
|
247
|
+
|
|
248
|
+
### `olympay tx eval`
|
|
249
|
+
|
|
250
|
+
Submit a transaction attempt for real-time policy evaluation. Returns the decision (`ALLOW`, `DENY`, or `REVIEW`) and the transaction record.
|
|
251
|
+
|
|
252
|
+
```bash
|
|
253
|
+
olympay tx eval --agent AGENT_ID --account ACCOUNT_ID --amount 5000
|
|
254
|
+
```
|
|
255
|
+
|
|
256
|
+
| Flag | Required | Description |
|
|
257
|
+
|---|---|---|
|
|
258
|
+
| `--agent` | Yes | Agent ID |
|
|
259
|
+
| `--account` | Yes | Account ID |
|
|
260
|
+
| `--amount` | Yes | Amount in minor units (e.g. `1000` = $10.00) |
|
|
261
|
+
| `--card` | No | Card ID |
|
|
262
|
+
| `--merchant` | No | Merchant identifier |
|
|
263
|
+
| `--currency` | No | Currency code (default: `USD`) |
|
|
264
|
+
| `--direction` | No | `DEBIT` or `CREDIT` (default: `DEBIT`) |
|
|
265
|
+
|
|
266
|
+
**Output:**
|
|
267
|
+
```
|
|
268
|
+
Transaction DENY
|
|
269
|
+
────────────────────────────────────────────────────────
|
|
270
|
+
Transaction ID: 3cafac02-d2b9-4b1f-a210-ce38a64a01d6
|
|
271
|
+
Amount: 200.00 USD
|
|
272
|
+
Decision: DENY
|
|
273
|
+
Reason: Denied by policy: SPEND_LIMIT
|
|
274
|
+
Policies: SPEND_LIMIT=DENY
|
|
275
|
+
────────────────────────────────────────────────────────
|
|
276
|
+
```
|
|
277
|
+
|
|
278
|
+
If a policy of type `APPROVAL_REQUIRED` matches, the decision is `REVIEW` and an approval request is created — visible in the dashboard.
|
|
279
|
+
|
|
280
|
+
---
|
|
281
|
+
|
|
282
|
+
### `olympay workspace generate-key`
|
|
283
|
+
|
|
284
|
+
Generate a new workspace API key. The full key value is shown only once.
|
|
285
|
+
|
|
286
|
+
```bash
|
|
287
|
+
olympay workspace generate-key --name "CI pipeline"
|
|
288
|
+
```
|
|
289
|
+
|
|
290
|
+
| Flag | Required | Description |
|
|
291
|
+
|---|---|---|
|
|
292
|
+
| `--name` | No | Label for the key (default: `CLI Key`) |
|
|
293
|
+
|
|
294
|
+
---
|
|
295
|
+
|
|
296
|
+
### `olympay workspace keys`
|
|
297
|
+
|
|
298
|
+
List all active (non-revoked) workspace API keys. Keys are shown in masked form.
|
|
299
|
+
|
|
300
|
+
```bash
|
|
301
|
+
olympay workspace keys
|
|
302
|
+
```
|
|
303
|
+
|
|
304
|
+
---
|
|
305
|
+
|
|
306
|
+
### `olympay workspace revoke`
|
|
307
|
+
|
|
308
|
+
Revoke a workspace API key by its ID. Revoked keys are immediately rejected by the API.
|
|
309
|
+
|
|
310
|
+
```bash
|
|
311
|
+
olympay workspace revoke KEY_ID
|
|
312
|
+
```
|
|
313
|
+
|
|
314
|
+
Use `olympay workspace keys` to find the key ID first.
|
|
315
|
+
|
|
316
|
+
---
|
|
317
|
+
|
|
318
|
+
## Full Workflow Example
|
|
319
|
+
|
|
320
|
+
```bash
|
|
321
|
+
# 1. Authenticate
|
|
322
|
+
olympay login --key olympay_ws_...
|
|
323
|
+
|
|
324
|
+
# 2. Spawn an agent for handling SaaS payments
|
|
325
|
+
olympay agent create --name "saas-bot" --description "Manages SaaS subscriptions"
|
|
326
|
+
# Output: ID: <AGENT_ID>, API Key: olympay_agt_...
|
|
327
|
+
|
|
328
|
+
# 3. Open a USD account for the agent
|
|
329
|
+
olympay account create --agent <AGENT_ID> --name "saas-budget" --currency USD
|
|
330
|
+
# Output: ID: <ACCOUNT_ID>
|
|
331
|
+
|
|
332
|
+
# 4. Issue a virtual card
|
|
333
|
+
olympay card issue --agent <AGENT_ID> --account <ACCOUNT_ID> --brand VISA
|
|
334
|
+
# Output: ID: <CARD_ID>
|
|
335
|
+
|
|
336
|
+
# 5. Create a spending limit policy (blocks transactions above $100)
|
|
337
|
+
olympay policy create --name "100-cap" --type SPEND_LIMIT --config '{"maxAmountMinor":10000}'
|
|
338
|
+
# Output: ID: <POLICY_ID>
|
|
339
|
+
|
|
340
|
+
# 6. Assign the policy to the agent
|
|
341
|
+
olympay policy assign --policy <POLICY_ID> --target-type AGENT --target <AGENT_ID>
|
|
342
|
+
|
|
343
|
+
# 7. Evaluate a transaction attempt ($50 - should ALLOW)
|
|
344
|
+
olympay tx eval --agent <AGENT_ID> --account <ACCOUNT_ID> --amount 5000
|
|
345
|
+
|
|
346
|
+
# 8. Evaluate a transaction attempt ($200 - should DENY)
|
|
347
|
+
olympay tx eval --agent <AGENT_ID> --account <ACCOUNT_ID> --amount 20000
|
|
348
|
+
|
|
349
|
+
# 9. Emergency: suspend the agent
|
|
350
|
+
olympay agent suspend <AGENT_ID>
|
|
351
|
+
|
|
352
|
+
# 10. Resume after review
|
|
353
|
+
olympay agent activate <AGENT_ID>
|
|
354
|
+
|
|
355
|
+
# 11. List all resources
|
|
356
|
+
olympay agent list
|
|
357
|
+
olympay account list
|
|
358
|
+
olympay card list
|
|
359
|
+
olympay policy list
|
|
360
|
+
```
|
|
361
|
+
|
|
362
|
+
---
|
|
363
|
+
|
|
364
|
+
## Configuration
|
|
365
|
+
|
|
366
|
+
Credentials are stored at `~/.olympay/config.json`:
|
|
367
|
+
|
|
368
|
+
```json
|
|
369
|
+
{
|
|
370
|
+
"apiKey": "olympay_ws_...",
|
|
371
|
+
"apiBase": "https://api.olympay.tech/v1"
|
|
372
|
+
}
|
|
373
|
+
```
|
|
374
|
+
|
|
375
|
+
To use a self-hosted instance, pass `--api` during login:
|
|
376
|
+
|
|
377
|
+
```bash
|
|
378
|
+
olympay login --key olympay_ws_... --api https://your-api-domain.com/v1
|
|
379
|
+
```
|
|
380
|
+
|
|
381
|
+
---
|
|
382
|
+
|
|
383
|
+
## Policy Types
|
|
384
|
+
|
|
385
|
+
Policies are managed in the dashboard. The following types are available:
|
|
386
|
+
|
|
387
|
+
| Type | Description |
|
|
388
|
+
|---|---|
|
|
389
|
+
| `SPEND_LIMIT` | Block transactions above a configured amount |
|
|
390
|
+
| `MERCHANT_ALLOWLIST` | Restrict spending to approved merchant IDs only |
|
|
391
|
+
| `MERCHANT_BLOCKLIST` | Block spending at specific merchant IDs |
|
|
392
|
+
| `APPROVAL_REQUIRED` | Route all transactions to human approval queue |
|
|
393
|
+
| `TIME_WINDOW` | Restrict spending to defined time ranges |
|
|
394
|
+
|
|
395
|
+
---
|
|
396
|
+
|
|
397
|
+
## Links
|
|
398
|
+
|
|
399
|
+
- **Dashboard**: [olympay.tech](https://olympay.tech)
|
|
400
|
+
- **API Base**: `https://api.olympay.tech/v1`
|
|
401
|
+
- **npm**: [npmjs.com/package/olympay](https://www.npmjs.com/package/olympay)
|
|
402
|
+
|
|
403
|
+
---
|
|
404
|
+
|
|
405
|
+
## License
|
|
406
|
+
|
|
407
|
+
MIT
|
package/dist/cli.js
ADDED
|
@@ -0,0 +1,322 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
|
|
3
|
+
// src/cli.ts
|
|
4
|
+
import { Command } from "commander";
|
|
5
|
+
import { readFileSync, writeFileSync, existsSync, mkdirSync } from "fs";
|
|
6
|
+
import { homedir } from "os";
|
|
7
|
+
import { join } from "path";
|
|
8
|
+
var CONFIG_DIR = join(homedir(), ".olympay");
|
|
9
|
+
var CONFIG_FILE = join(CONFIG_DIR, "config.json");
|
|
10
|
+
var DEFAULT_API = "https://api.olympay.tech/v1";
|
|
11
|
+
var RESET = "\x1B[0m";
|
|
12
|
+
var GOLD = "\x1B[38;2;196;146;58m";
|
|
13
|
+
var DIM = "\x1B[2m";
|
|
14
|
+
var BOLD = "\x1B[1m";
|
|
15
|
+
var GREEN = "\x1B[32m";
|
|
16
|
+
var RED = "\x1B[31m";
|
|
17
|
+
var CYAN = "\x1B[36m";
|
|
18
|
+
var ASCII_BANNER = `
|
|
19
|
+
${GOLD} \u2588\u2588\u2588\u2588\u2588\u2588\u2557 \u2588\u2588\u2557 \u2588\u2588\u2557 \u2588\u2588\u2557\u2588\u2588\u2588\u2557 \u2588\u2588\u2588\u2557\u2588\u2588\u2588\u2588\u2588\u2588\u2557 \u2588\u2588\u2588\u2588\u2588\u2557 \u2588\u2588\u2557 \u2588\u2588\u2557${RESET}
|
|
20
|
+
${GOLD}\u2588\u2588\u2554\u2550\u2550\u2550\u2588\u2588\u2557\u2588\u2588\u2551 \u255A\u2588\u2588\u2557 \u2588\u2588\u2554\u255D\u2588\u2588\u2588\u2588\u2557 \u2588\u2588\u2588\u2588\u2551\u2588\u2588\u2554\u2550\u2550\u2588\u2588\u2557\u2588\u2588\u2554\u2550\u2550\u2588\u2588\u2557\u255A\u2588\u2588\u2557 \u2588\u2588\u2554\u255D${RESET}
|
|
21
|
+
${GOLD}\u2588\u2588\u2551 \u2588\u2588\u2551\u2588\u2588\u2551 \u255A\u2588\u2588\u2588\u2588\u2554\u255D \u2588\u2588\u2554\u2588\u2588\u2588\u2588\u2554\u2588\u2588\u2551\u2588\u2588\u2588\u2588\u2588\u2588\u2554\u255D\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2551 \u255A\u2588\u2588\u2588\u2588\u2554\u255D ${RESET}
|
|
22
|
+
${GOLD}\u2588\u2588\u2551 \u2588\u2588\u2551\u2588\u2588\u2551 \u255A\u2588\u2588\u2554\u255D \u2588\u2588\u2551\u255A\u2588\u2588\u2554\u255D\u2588\u2588\u2551\u2588\u2588\u2554\u2550\u2550\u2550\u255D \u2588\u2588\u2554\u2550\u2550\u2588\u2588\u2551 \u255A\u2588\u2588\u2554\u255D ${RESET}
|
|
23
|
+
${GOLD}\u255A\u2588\u2588\u2588\u2588\u2588\u2588\u2554\u255D\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2557 \u2588\u2588\u2551 \u2588\u2588\u2551 \u255A\u2550\u255D \u2588\u2588\u2551\u2588\u2588\u2551 \u2588\u2588\u2551 \u2588\u2588\u2551 \u2588\u2588\u2551 ${RESET}
|
|
24
|
+
${GOLD} \u255A\u2550\u2550\u2550\u2550\u2550\u255D \u255A\u2550\u2550\u2550\u2550\u2550\u2550\u255D \u255A\u2550\u255D \u255A\u2550\u255D \u255A\u2550\u255D\u255A\u2550\u255D \u255A\u2550\u255D \u255A\u2550\u255D \u255A\u2550\u255D ${RESET}
|
|
25
|
+
${DIM} Financial control for autonomous AI agents \u2022 olympay.tech${RESET}
|
|
26
|
+
`;
|
|
27
|
+
function loadConfig() {
|
|
28
|
+
if (!existsSync(CONFIG_FILE)) return null;
|
|
29
|
+
try {
|
|
30
|
+
return JSON.parse(readFileSync(CONFIG_FILE, "utf8"));
|
|
31
|
+
} catch {
|
|
32
|
+
return null;
|
|
33
|
+
}
|
|
34
|
+
}
|
|
35
|
+
function saveConfig(cfg) {
|
|
36
|
+
if (!existsSync(CONFIG_DIR)) mkdirSync(CONFIG_DIR, { recursive: true });
|
|
37
|
+
writeFileSync(CONFIG_FILE, JSON.stringify(cfg, null, 2));
|
|
38
|
+
}
|
|
39
|
+
function requireConfig() {
|
|
40
|
+
const cfg = loadConfig();
|
|
41
|
+
if (!cfg?.apiKey) {
|
|
42
|
+
console.error(`${RED}Not logged in.${RESET} Run: ${GOLD}olympay login --key olympay_ws_...${RESET}`);
|
|
43
|
+
process.exit(1);
|
|
44
|
+
}
|
|
45
|
+
return cfg;
|
|
46
|
+
}
|
|
47
|
+
async function apiCall(method, path, body) {
|
|
48
|
+
const cfg = requireConfig();
|
|
49
|
+
const url = `${cfg.apiBase}${path}`;
|
|
50
|
+
const res = await fetch(url, {
|
|
51
|
+
method,
|
|
52
|
+
headers: {
|
|
53
|
+
"Content-Type": "application/json",
|
|
54
|
+
"Authorization": `Bearer ${cfg.apiKey}`
|
|
55
|
+
},
|
|
56
|
+
body: body ? JSON.stringify(body) : void 0
|
|
57
|
+
});
|
|
58
|
+
const json = await res.json();
|
|
59
|
+
if (!res.ok) {
|
|
60
|
+
const msg = json?.error?.message ?? `HTTP ${res.status}`;
|
|
61
|
+
console.error(`${RED}Error:${RESET} ${msg}`);
|
|
62
|
+
process.exit(1);
|
|
63
|
+
}
|
|
64
|
+
return json?.data ?? json;
|
|
65
|
+
}
|
|
66
|
+
var program = new Command();
|
|
67
|
+
program.name("olympay").description("Olympay CLI - spawn and manage AI agents with financial controls").version("0.1.0").addHelpText("before", ASCII_BANNER);
|
|
68
|
+
program.command("login").description("Authenticate with a workspace API key").requiredOption("--key <key>", "Workspace API key (olympay_ws_...)").option("--api <url>", "API base URL", DEFAULT_API).action((opts) => {
|
|
69
|
+
if (!opts.key.startsWith("olympay_ws_")) {
|
|
70
|
+
console.error(`${RED}Invalid key format.${RESET} Expected: ${GOLD}olympay_ws_...${RESET}`);
|
|
71
|
+
process.exit(1);
|
|
72
|
+
}
|
|
73
|
+
saveConfig({ apiKey: opts.key, apiBase: opts.api });
|
|
74
|
+
console.log(ASCII_BANNER);
|
|
75
|
+
console.log(`${GREEN}Logged in successfully.${RESET}`);
|
|
76
|
+
console.log(`${DIM}API base:${RESET} ${opts.api}`);
|
|
77
|
+
console.log(`${DIM}Config: ${RESET} ${CONFIG_FILE}`);
|
|
78
|
+
console.log(`
|
|
79
|
+
${GOLD}Next steps:${RESET}`);
|
|
80
|
+
console.log(` ${CYAN}olympay agent create --name "my-bot"${RESET} Spawn your first agent`);
|
|
81
|
+
console.log(` ${CYAN}olympay agent list${RESET} List all agents`);
|
|
82
|
+
console.log(` ${CYAN}olympay account create --agent <id> --name "main"${RESET}`);
|
|
83
|
+
});
|
|
84
|
+
program.command("logout").description("Remove stored credentials").action(() => {
|
|
85
|
+
if (existsSync(CONFIG_FILE)) {
|
|
86
|
+
writeFileSync(CONFIG_FILE, "{}");
|
|
87
|
+
}
|
|
88
|
+
console.log(`${GREEN}Logged out.${RESET} Credentials removed from ${CONFIG_FILE}`);
|
|
89
|
+
});
|
|
90
|
+
program.command("whoami").description("Show current credentials").action(() => {
|
|
91
|
+
const cfg = loadConfig();
|
|
92
|
+
if (!cfg?.apiKey) {
|
|
93
|
+
console.log(`${DIM}Not logged in.${RESET}`);
|
|
94
|
+
} else {
|
|
95
|
+
const masked = cfg.apiKey.slice(0, 18) + "..." + cfg.apiKey.slice(-4);
|
|
96
|
+
console.log(`${GOLD}API key:${RESET} ${masked}`);
|
|
97
|
+
console.log(`${GOLD}API base:${RESET} ${cfg.apiBase}`);
|
|
98
|
+
}
|
|
99
|
+
});
|
|
100
|
+
var agentCmd = program.command("agent").description("Manage AI agents");
|
|
101
|
+
agentCmd.command("create").description("Spawn a new AI agent and receive its API key").requiredOption("--name <name>", "Agent name").option("--description <desc>", "Agent description").action(async (opts) => {
|
|
102
|
+
const agent = await apiCall("POST", "/agents", {
|
|
103
|
+
name: opts.name,
|
|
104
|
+
description: opts.description,
|
|
105
|
+
status: "active"
|
|
106
|
+
});
|
|
107
|
+
console.log(`
|
|
108
|
+
${GREEN}Agent spawned successfully!${RESET}`);
|
|
109
|
+
console.log(`${DIM}${"\u2500".repeat(48)}${RESET}`);
|
|
110
|
+
console.log(`${GOLD}ID: ${RESET} ${agent.id}`);
|
|
111
|
+
console.log(`${GOLD}Name: ${RESET} ${agent.name}`);
|
|
112
|
+
console.log(`${GOLD}API Key: ${RESET} ${BOLD}${agent.apiKey}${RESET}`);
|
|
113
|
+
console.log(`${GOLD}Status: ${RESET} ${agent.status}`);
|
|
114
|
+
console.log(`${DIM}${"\u2500".repeat(48)}${RESET}`);
|
|
115
|
+
console.log(`${DIM}Keep this API key safe - use it to authenticate transactions.${RESET}`);
|
|
116
|
+
});
|
|
117
|
+
agentCmd.command("list").description("List all agents in your workspace").action(async () => {
|
|
118
|
+
const agents = await apiCall("GET", "/agents");
|
|
119
|
+
if (!agents.length) {
|
|
120
|
+
console.log(`${DIM}No agents found.${RESET} Create one with: ${GOLD}olympay agent create --name "..."${RESET}`);
|
|
121
|
+
return;
|
|
122
|
+
}
|
|
123
|
+
console.log(`
|
|
124
|
+
${GOLD}${"ID".padEnd(38)} ${"NAME".padEnd(25)} ${"STATUS".padEnd(12)} API KEY${RESET}`);
|
|
125
|
+
console.log(`${DIM}${"\u2500".repeat(100)}${RESET}`);
|
|
126
|
+
for (const a of agents) {
|
|
127
|
+
const key = a.apiKey ? a.apiKey.slice(0, 20) + "..." : DIM + "-" + RESET;
|
|
128
|
+
const status = a.status === "active" ? `${GREEN}${a.status}${RESET}` : `${DIM}${a.status}${RESET}`;
|
|
129
|
+
console.log(`${a.id.padEnd(38)} ${a.name.padEnd(25)} ${a.status.padEnd(12)} ${key}`);
|
|
130
|
+
}
|
|
131
|
+
});
|
|
132
|
+
agentCmd.command("suspend <id>").description("Suspend an agent to block all transactions").action(async (id) => {
|
|
133
|
+
await apiCall("PATCH", `/agents/${id}/status`, { status: "suspended" });
|
|
134
|
+
console.log(`${GREEN}Agent ${id} suspended.${RESET}`);
|
|
135
|
+
});
|
|
136
|
+
agentCmd.command("activate <id>").description("Re-activate a suspended agent").action(async (id) => {
|
|
137
|
+
await apiCall("PATCH", `/agents/${id}/status`, { status: "active" });
|
|
138
|
+
console.log(`${GREEN}Agent ${id} activated.${RESET}`);
|
|
139
|
+
});
|
|
140
|
+
var accountCmd = program.command("account").description("Manage agent ledger accounts");
|
|
141
|
+
accountCmd.command("create").description("Open a ledger account for an agent").requiredOption("--agent <agentId>", "Agent ID").requiredOption("--name <name>", "Account name").option("--currency <currency>", "Currency code", "USD").action(async (opts) => {
|
|
142
|
+
const account = await apiCall("POST", "/accounts", {
|
|
143
|
+
agentId: opts.agent,
|
|
144
|
+
name: opts.name,
|
|
145
|
+
currency: opts.currency
|
|
146
|
+
});
|
|
147
|
+
console.log(`
|
|
148
|
+
${GREEN}Account opened!${RESET}`);
|
|
149
|
+
console.log(`${GOLD}ID: ${RESET} ${account.id}`);
|
|
150
|
+
console.log(`${GOLD}Name: ${RESET} ${account.name}`);
|
|
151
|
+
console.log(`${GOLD}Currency: ${RESET} ${account.currency}`);
|
|
152
|
+
console.log(`${GOLD}Status: ${RESET} ${account.status}`);
|
|
153
|
+
});
|
|
154
|
+
accountCmd.command("list").description("List all accounts in your workspace").action(async () => {
|
|
155
|
+
const accounts = await apiCall("GET", "/accounts");
|
|
156
|
+
if (!accounts.length) {
|
|
157
|
+
console.log(`${DIM}No accounts found.${RESET}`);
|
|
158
|
+
return;
|
|
159
|
+
}
|
|
160
|
+
console.log(`
|
|
161
|
+
${GOLD}${"ID".padEnd(38)} ${"NAME".padEnd(25)} ${"AGENT".padEnd(38)} BALANCE${RESET}`);
|
|
162
|
+
console.log(`${DIM}${"\u2500".repeat(110)}${RESET}`);
|
|
163
|
+
for (const a of accounts) {
|
|
164
|
+
const bal = `$${((a.balanceMinor ?? 0) / 100).toFixed(2)} ${a.currency}`;
|
|
165
|
+
console.log(`${a.id.padEnd(38)} ${a.name.padEnd(25)} ${a.agentId.padEnd(38)} ${bal}`);
|
|
166
|
+
}
|
|
167
|
+
});
|
|
168
|
+
var cardCmd = program.command("card").description("Manage virtual cards");
|
|
169
|
+
cardCmd.command("issue").description("Issue a virtual card linked to an agent account").requiredOption("--agent <agentId>", "Agent ID").requiredOption("--account <accountId>", "Account ID").option("--brand <brand>", "Card brand (e.g. VISA)", "VISA").option("--last4 <last4>", "Last 4 digits (optional)").action(async (opts) => {
|
|
170
|
+
const card = await apiCall("POST", "/cards", {
|
|
171
|
+
agentId: opts.agent,
|
|
172
|
+
accountId: opts.account,
|
|
173
|
+
brand: opts.brand,
|
|
174
|
+
last4: opts.last4
|
|
175
|
+
});
|
|
176
|
+
console.log(`
|
|
177
|
+
${GREEN}Card issued!${RESET}`);
|
|
178
|
+
console.log(`${GOLD}ID: ${RESET} ${card.id}`);
|
|
179
|
+
console.log(`${GOLD}Brand: ${RESET} ${card.brand ?? "VISA"}`);
|
|
180
|
+
console.log(`${GOLD}Last4: ${RESET} ${card.last4 ?? DIM + "-" + RESET}`);
|
|
181
|
+
console.log(`${GOLD}Status: ${RESET} ${card.status}`);
|
|
182
|
+
});
|
|
183
|
+
cardCmd.command("list").description("List all virtual cards in your workspace").action(async () => {
|
|
184
|
+
const cards = await apiCall("GET", "/cards");
|
|
185
|
+
if (!cards.length) {
|
|
186
|
+
console.log(`${DIM}No cards found.${RESET}`);
|
|
187
|
+
return;
|
|
188
|
+
}
|
|
189
|
+
console.log(`
|
|
190
|
+
${GOLD}${"ID".padEnd(38)} ${"BRAND".padEnd(8)} ${"LAST4".padEnd(6)} ${"STATUS".padEnd(12)} AGENT${RESET}`);
|
|
191
|
+
console.log(`${DIM}${"\u2500".repeat(90)}${RESET}`);
|
|
192
|
+
for (const c of cards) {
|
|
193
|
+
console.log(`${c.id.padEnd(38)} ${(c.brand ?? "-").padEnd(8)} ${(c.last4 ?? "-").padEnd(6)} ${c.status.padEnd(12)} ${c.agentId}`);
|
|
194
|
+
}
|
|
195
|
+
});
|
|
196
|
+
var policyCmd = program.command("policy").description("Manage spending policies");
|
|
197
|
+
policyCmd.command("list").description("List all spending policies in your workspace").action(async () => {
|
|
198
|
+
const policies = await apiCall("GET", "/policies");
|
|
199
|
+
if (!policies.length) {
|
|
200
|
+
console.log(`${DIM}No policies found.${RESET} Create one with: ${GOLD}olympay policy create --name "..." --type SPEND_LIMIT${RESET}`);
|
|
201
|
+
return;
|
|
202
|
+
}
|
|
203
|
+
console.log(`
|
|
204
|
+
${GOLD}${"ID".padEnd(38)} ${"NAME".padEnd(30)} ${"TYPE".padEnd(25)} STATUS${RESET}`);
|
|
205
|
+
console.log(`${DIM}${"\u2500".repeat(100)}${RESET}`);
|
|
206
|
+
for (const p of policies) {
|
|
207
|
+
const status = p.status === "active" ? `${GREEN}${p.status}${RESET}` : `${DIM}${p.status}${RESET}`;
|
|
208
|
+
console.log(`${p.id.padEnd(38)} ${p.name.padEnd(30)} ${p.type.padEnd(25)} ${p.status}`);
|
|
209
|
+
}
|
|
210
|
+
});
|
|
211
|
+
policyCmd.command("create").description("Create a spending policy").requiredOption("--name <name>", "Policy name").requiredOption("--type <type>", "Policy type: SPEND_LIMIT | MERCHANT_ALLOWLIST | MERCHANT_BLOCKLIST | APPROVAL_REQUIRED | TIME_WINDOW").option("--config <json>", "Policy config as JSON string", "{}").option("--description <desc>", "Policy description").action(async (opts) => {
|
|
212
|
+
let configJson;
|
|
213
|
+
try {
|
|
214
|
+
configJson = JSON.parse(opts.config);
|
|
215
|
+
} catch {
|
|
216
|
+
console.error(`${RED}Error:${RESET} --config must be valid JSON (e.g. '{"maxAmountMinor":10000}')`);
|
|
217
|
+
process.exit(1);
|
|
218
|
+
}
|
|
219
|
+
const validTypes = ["SPEND_LIMIT", "MERCHANT_ALLOWLIST", "MERCHANT_BLOCKLIST", "APPROVAL_REQUIRED", "TIME_WINDOW"];
|
|
220
|
+
if (!validTypes.includes(opts.type)) {
|
|
221
|
+
console.error(`${RED}Error:${RESET} Invalid type. Must be one of: ${validTypes.join(", ")}`);
|
|
222
|
+
process.exit(1);
|
|
223
|
+
}
|
|
224
|
+
const policy = await apiCall("POST", "/policies", {
|
|
225
|
+
name: opts.name,
|
|
226
|
+
type: opts.type,
|
|
227
|
+
configJson,
|
|
228
|
+
description: opts.description
|
|
229
|
+
});
|
|
230
|
+
console.log(`
|
|
231
|
+
${GREEN}Policy created!${RESET}`);
|
|
232
|
+
console.log(`${DIM}${"\u2500".repeat(56)}${RESET}`);
|
|
233
|
+
console.log(`${GOLD}ID: ${RESET} ${policy.id}`);
|
|
234
|
+
console.log(`${GOLD}Name: ${RESET} ${policy.name}`);
|
|
235
|
+
console.log(`${GOLD}Type: ${RESET} ${policy.type}`);
|
|
236
|
+
console.log(`${GOLD}Status: ${RESET} ${policy.status}`);
|
|
237
|
+
console.log(`${GOLD}Config: ${RESET} ${JSON.stringify(policy.configJson)}`);
|
|
238
|
+
console.log(`${DIM}${"\u2500".repeat(56)}${RESET}`);
|
|
239
|
+
});
|
|
240
|
+
policyCmd.command("assign").description("Assign a policy to an agent, account, or card").requiredOption("--policy <policyId>", "Policy ID").requiredOption("--target-type <type>", "Target type: AGENT | ACCOUNT | CARD").requiredOption("--target <targetId>", "Target entity ID").option("--priority <n>", "Priority (lower = higher priority)", "100").action(async (opts) => {
|
|
241
|
+
const validTargets = ["AGENT", "ACCOUNT", "CARD"];
|
|
242
|
+
if (!validTargets.includes(opts.targetType)) {
|
|
243
|
+
console.error(`${RED}Error:${RESET} --target-type must be one of: ${validTargets.join(", ")}`);
|
|
244
|
+
process.exit(1);
|
|
245
|
+
}
|
|
246
|
+
const assignment = await apiCall("POST", "/policy-assignments", {
|
|
247
|
+
policyId: opts.policy,
|
|
248
|
+
targetType: opts.targetType,
|
|
249
|
+
targetId: opts.target,
|
|
250
|
+
priority: parseInt(opts.priority, 10)
|
|
251
|
+
});
|
|
252
|
+
console.log(`
|
|
253
|
+
${GREEN}Policy assigned!${RESET}`);
|
|
254
|
+
console.log(`${GOLD}Assignment ID: ${RESET} ${assignment.id}`);
|
|
255
|
+
console.log(`${GOLD}Policy: ${RESET} ${assignment.policyId}`);
|
|
256
|
+
console.log(`${GOLD}Target: ${RESET} ${assignment.targetType} ${assignment.targetId}`);
|
|
257
|
+
console.log(`${GOLD}Priority: ${RESET} ${assignment.priority}`);
|
|
258
|
+
});
|
|
259
|
+
var txCmd = program.command("tx").description("Evaluate and inspect transactions");
|
|
260
|
+
txCmd.command("eval").description("Submit a transaction attempt for policy evaluation").requiredOption("--agent <agentId>", "Agent ID").requiredOption("--account <accountId>", "Account ID").requiredOption("--amount <minor>", "Amount in minor currency units (e.g. 1000 = $10.00)").option("--card <cardId>", "Card ID (optional)").option("--merchant <merchantId>", "Merchant identifier (optional)").option("--currency <currency>", "Currency code", "USD").option("--direction <direction>", "DEBIT or CREDIT", "DEBIT").action(async (opts) => {
|
|
261
|
+
const amountMinor = parseInt(opts.amount, 10);
|
|
262
|
+
if (isNaN(amountMinor) || amountMinor <= 0) {
|
|
263
|
+
console.error(`${RED}Error:${RESET} --amount must be a positive integer (minor units)`);
|
|
264
|
+
process.exit(1);
|
|
265
|
+
}
|
|
266
|
+
const result = await apiCall("POST", "/transactions/attempt", {
|
|
267
|
+
agentId: opts.agent,
|
|
268
|
+
accountId: opts.account,
|
|
269
|
+
cardId: opts.card,
|
|
270
|
+
merchantId: opts.merchant,
|
|
271
|
+
amountMinor,
|
|
272
|
+
currency: opts.currency,
|
|
273
|
+
direction: opts.direction
|
|
274
|
+
});
|
|
275
|
+
const { transaction, decision, approvalRequest } = result;
|
|
276
|
+
const decColor = decision.result === "ALLOW" ? GREEN : decision.result === "DENY" ? RED : GOLD;
|
|
277
|
+
console.log(`
|
|
278
|
+
${decColor}Transaction ${decision.result}${RESET}`);
|
|
279
|
+
console.log(`${DIM}${"\u2500".repeat(56)}${RESET}`);
|
|
280
|
+
console.log(`${GOLD}Transaction ID: ${RESET} ${transaction.id}`);
|
|
281
|
+
console.log(`${GOLD}Amount: ${RESET} ${(amountMinor / 100).toFixed(2)} ${opts.currency}`);
|
|
282
|
+
console.log(`${GOLD}Decision: ${RESET} ${decColor}${decision.result}${RESET}`);
|
|
283
|
+
console.log(`${GOLD}Reason: ${RESET} ${decision.reason}`);
|
|
284
|
+
if (decision.matchedPolicies?.length) {
|
|
285
|
+
console.log(`${GOLD}Policies: ${RESET} ${decision.matchedPolicies.map((p) => `${p.policyType}=${p.outcome}`).join(", ")}`);
|
|
286
|
+
}
|
|
287
|
+
if (approvalRequest) {
|
|
288
|
+
console.log(`${GOLD}Approval ID: ${RESET} ${approvalRequest.id}`);
|
|
289
|
+
console.log(`${DIM}Human approval required - review in dashboard${RESET}`);
|
|
290
|
+
}
|
|
291
|
+
console.log(`${DIM}${"\u2500".repeat(56)}${RESET}`);
|
|
292
|
+
});
|
|
293
|
+
var wsCmd = program.command("workspace").description("Manage workspace settings and API keys");
|
|
294
|
+
wsCmd.command("keys").description("List active workspace API keys").action(async () => {
|
|
295
|
+
const keys = await apiCall("GET", "/workspace/keys");
|
|
296
|
+
if (!keys.length) {
|
|
297
|
+
console.log(`${DIM}No keys found.${RESET} Generate one with: ${GOLD}olympay workspace generate-key${RESET}`);
|
|
298
|
+
return;
|
|
299
|
+
}
|
|
300
|
+
console.log(`
|
|
301
|
+
${GOLD}${"ID".padEnd(38)} ${"NAME".padEnd(20)} KEY${RESET}`);
|
|
302
|
+
console.log(`${DIM}${"\u2500".repeat(100)}${RESET}`);
|
|
303
|
+
for (const k of keys) {
|
|
304
|
+
console.log(`${k.id.padEnd(38)} ${k.name.padEnd(20)} ${k.key}`);
|
|
305
|
+
}
|
|
306
|
+
});
|
|
307
|
+
wsCmd.command("generate-key").description("Generate a new workspace API key").option("--name <name>", "Key label", "CLI Key").action(async (opts) => {
|
|
308
|
+
const key = await apiCall("POST", "/workspace/keys", { name: opts.name });
|
|
309
|
+
console.log(`
|
|
310
|
+
${GREEN}New workspace API key generated!${RESET}`);
|
|
311
|
+
console.log(`${DIM}${"\u2500".repeat(56)}${RESET}`);
|
|
312
|
+
console.log(`${GOLD}ID: ${RESET} ${key.id}`);
|
|
313
|
+
console.log(`${GOLD}Name: ${RESET} ${key.name}`);
|
|
314
|
+
console.log(`${GOLD}Key: ${RESET} ${BOLD}${key.key}${RESET}`);
|
|
315
|
+
console.log(`${DIM}${"\u2500".repeat(56)}${RESET}`);
|
|
316
|
+
console.log(`${DIM}Use with:${RESET} ${GOLD}olympay login --key ${key.key}${RESET}`);
|
|
317
|
+
});
|
|
318
|
+
wsCmd.command("revoke <id>").description("Revoke a workspace API key by ID").action(async (id) => {
|
|
319
|
+
await apiCall("DELETE", `/workspace/keys/${id}`);
|
|
320
|
+
console.log(`${GREEN}Key ${id} revoked successfully.${RESET}`);
|
|
321
|
+
});
|
|
322
|
+
program.parse();
|
package/package.json
ADDED
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "olympay",
|
|
3
|
+
"version": "0.1.0",
|
|
4
|
+
"description": "CLI for Olympay - financial control for autonomous AI agents",
|
|
5
|
+
"keywords": [
|
|
6
|
+
"olympay",
|
|
7
|
+
"ai",
|
|
8
|
+
"agents",
|
|
9
|
+
"fintech",
|
|
10
|
+
"cli"
|
|
11
|
+
],
|
|
12
|
+
"license": "MIT",
|
|
13
|
+
"type": "module",
|
|
14
|
+
"bin": {
|
|
15
|
+
"olympay": "dist/cli.js"
|
|
16
|
+
},
|
|
17
|
+
"files": [
|
|
18
|
+
"dist"
|
|
19
|
+
],
|
|
20
|
+
"scripts": {
|
|
21
|
+
"build": "node build.mjs",
|
|
22
|
+
"olympay": "tsx ./src/cli.ts",
|
|
23
|
+
"typecheck": "tsc -p tsconfig.json --noEmit"
|
|
24
|
+
},
|
|
25
|
+
"dependencies": {
|
|
26
|
+
"commander": "^14.0.3"
|
|
27
|
+
},
|
|
28
|
+
"devDependencies": {
|
|
29
|
+
"@types/node": "catalog:",
|
|
30
|
+
"esbuild": "^0.25.5",
|
|
31
|
+
"tsx": "catalog:"
|
|
32
|
+
}
|
|
33
|
+
}
|