@slothmoney/agent-cli 0.2.0 → 0.3.1
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/CHANGELOG.md +29 -0
- package/README.md +175 -38
- package/dist/args.js +302 -2
- package/dist/cli.js +615 -7
- package/dist/contracts.js +120 -3
- package/package.json +1 -1
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,34 @@
|
|
|
1
1
|
# Changelog
|
|
2
2
|
|
|
3
|
+
## Unreleased
|
|
4
|
+
|
|
5
|
+
## 0.3.1 - 2026-07-31
|
|
6
|
+
|
|
7
|
+
- Coordinate transaction reads with the Sloth Budget daily refresh process.
|
|
8
|
+
- Wait up to 45 seconds for fresh persisted data, then return readable cached
|
|
9
|
+
transactions with structured refresh status when work continues or fails.
|
|
10
|
+
- Validate the additive transaction refresh response contract.
|
|
11
|
+
|
|
12
|
+
## 0.3.0 - 2026-07-30
|
|
13
|
+
|
|
14
|
+
- Add command-specific help for every command and auth subcommand, including
|
|
15
|
+
required inputs, option constraints, output, examples, and write safety.
|
|
16
|
+
- Explain that categories are parents and line items are scoped children, and
|
|
17
|
+
that line-item labels may repeat across categories.
|
|
18
|
+
- Clarify local native credential storage and environment-only authentication
|
|
19
|
+
for containers, CI, and other headless systems.
|
|
20
|
+
- Read and preview or apply the linked joint-budget shared-transaction setting.
|
|
21
|
+
- Filter transactions by personal or joint assignment scope.
|
|
22
|
+
- Categorise shared personal-account contributions against the joint catalogue
|
|
23
|
+
with `assignmentScope: "joint"`.
|
|
24
|
+
- Validate the nested `jointBudgetContribution` response contract.
|
|
25
|
+
- Add goal listing, creation, partial updates, and deletion through the Agent
|
|
26
|
+
API, with goal document ID validation and typed response validation.
|
|
27
|
+
- Keep goal writes in preview mode unless `--apply` is provided, including
|
|
28
|
+
explicit amount and month clearing.
|
|
29
|
+
- Explain that active shared goal target amounts remain app-managed so balance
|
|
30
|
+
allocations can be recalculated safely.
|
|
31
|
+
|
|
3
32
|
## 0.2.0 - 2026-07-23
|
|
4
33
|
|
|
5
34
|
- Add `auth login`, `auth status`, and `auth logout`.
|
package/README.md
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
# Sloth Agent CLI
|
|
2
2
|
|
|
3
|
-
Use your own agent to
|
|
3
|
+
Use your own agent to manage goals and categorise transactions through the
|
|
4
4
|
[Sloth Money Agent API](https://slothmoney.app/developers/).
|
|
5
5
|
|
|
6
6
|
## Install
|
|
@@ -15,14 +15,19 @@ sloth-agent --version
|
|
|
15
15
|
For a one-off pinned run:
|
|
16
16
|
|
|
17
17
|
```bash
|
|
18
|
-
npm exec --yes --package=@slothmoney/agent-cli@0.
|
|
18
|
+
npm exec --yes --package=@slothmoney/agent-cli@0.3.1 -- sloth-agent --help
|
|
19
19
|
```
|
|
20
20
|
|
|
21
21
|
## Authenticate
|
|
22
22
|
|
|
23
23
|
Create a personal access token in Sloth Money under
|
|
24
|
-
**Settings > Developer access**, then
|
|
25
|
-
|
|
24
|
+
**Settings > Developer access**, then choose the authentication method for
|
|
25
|
+
where the CLI runs.
|
|
26
|
+
|
|
27
|
+
### Local computer
|
|
28
|
+
|
|
29
|
+
On an interactive desktop, save the token in your operating system's secure
|
|
30
|
+
credential store:
|
|
26
31
|
|
|
27
32
|
```bash
|
|
28
33
|
sloth-agent auth login
|
|
@@ -31,36 +36,45 @@ sloth-agent auth login
|
|
|
31
36
|
The prompt hides the token. The CLI validates it before replacing any
|
|
32
37
|
credential already stored for the selected API origin.
|
|
33
38
|
|
|
34
|
-
|
|
35
|
-
the environment:
|
|
36
|
-
|
|
37
|
-
```bash
|
|
38
|
-
printf '%s' "$SLOTH_AGENT_TOKEN" | sloth-agent auth login --token-stdin
|
|
39
|
-
sloth-agent auth login --from-env
|
|
40
|
-
```
|
|
39
|
+
### Containers, CI, and headless systems
|
|
41
40
|
|
|
42
|
-
|
|
43
|
-
|
|
41
|
+
Native credential storage may be unavailable in a container or other headless
|
|
42
|
+
environment. Inject `SLOTH_AGENT_TOKEN` at runtime through your platform's
|
|
43
|
+
secret manager:
|
|
44
44
|
|
|
45
45
|
```bash
|
|
46
46
|
export SLOTH_AGENT_TOKEN="sloth_pat_v1_..."
|
|
47
|
+
sloth-agent auth status
|
|
47
48
|
```
|
|
48
49
|
|
|
50
|
+
Keep using the environment variable for later commands. You do not need to run
|
|
51
|
+
`sloth-agent auth login` in this setup. Do not put the token in a command
|
|
52
|
+
argument, source file, or container image.
|
|
53
|
+
|
|
49
54
|
`SLOTH_AGENT_TOKEN` always overrides a stored credential.
|
|
50
55
|
|
|
51
|
-
|
|
56
|
+
### Import an existing environment token
|
|
57
|
+
|
|
58
|
+
To save an environment token in native credential storage on a local computer:
|
|
52
59
|
|
|
53
60
|
```bash
|
|
54
61
|
sloth-agent auth login --from-env
|
|
55
62
|
unset SLOTH_AGENT_TOKEN
|
|
56
|
-
sloth-agent auth status
|
|
57
|
-
sloth-agent categories
|
|
58
63
|
```
|
|
59
64
|
|
|
60
|
-
|
|
65
|
+
You can also pass a token to the login command through stdin:
|
|
66
|
+
|
|
67
|
+
```bash
|
|
68
|
+
printf '%s' "$SLOTH_AGENT_TOKEN" | sloth-agent auth login --token-stdin
|
|
69
|
+
```
|
|
70
|
+
|
|
71
|
+
Both commands validate the token before replacing the stored credential.
|
|
72
|
+
|
|
73
|
+
Check the active credential and read your categories:
|
|
61
74
|
|
|
62
75
|
```bash
|
|
63
76
|
sloth-agent auth status
|
|
77
|
+
sloth-agent categories
|
|
64
78
|
```
|
|
65
79
|
|
|
66
80
|
This updates the PAT's `lastUsedAt` value. To remove the local credential:
|
|
@@ -74,59 +88,182 @@ remotely in **Sloth Money Settings > Developer access**.
|
|
|
74
88
|
|
|
75
89
|
## Commands
|
|
76
90
|
|
|
77
|
-
|
|
91
|
+
An assignment categorises an existing transaction e.g. assigning category
|
|
92
|
+
Groceries to a transaction.
|
|
93
|
+
|
|
94
|
+
Every command has built-in reference documentation covering its inputs,
|
|
95
|
+
options, output, and examples:
|
|
96
|
+
|
|
97
|
+
```bash
|
|
98
|
+
sloth-agent auth login --help
|
|
99
|
+
sloth-agent categories --help
|
|
100
|
+
sloth-agent transactions --help
|
|
101
|
+
sloth-agent assign --help
|
|
102
|
+
sloth-agent joint-budget-settings --help
|
|
103
|
+
sloth-agent goals create --help
|
|
104
|
+
sloth-agent goals update --help
|
|
105
|
+
sloth-agent ask-partner --help
|
|
106
|
+
```
|
|
107
|
+
|
|
108
|
+
### Categorise a transaction end to end
|
|
109
|
+
|
|
110
|
+
1. Read categories and available budget line items:
|
|
78
111
|
|
|
79
112
|
```bash
|
|
80
113
|
sloth-agent categories
|
|
81
114
|
```
|
|
82
115
|
|
|
83
|
-
|
|
116
|
+
A category is the broader parent. A line item is a child within one category.
|
|
117
|
+
Line-item names such as `Other` may repeat, so preserve the full choice as
|
|
118
|
+
`(scope, categoryId, lineItemId)`. Use the personal or joint line-item map that
|
|
119
|
+
matches the transaction scope. For example, `Bills → Other` and `Subscriptions
|
|
120
|
+
→ Other` are different choices.
|
|
121
|
+
|
|
122
|
+
2. Read uncategorised transactions:
|
|
84
123
|
|
|
85
124
|
```bash
|
|
86
125
|
sloth-agent transactions --uncategorized --limit 50
|
|
87
126
|
```
|
|
88
127
|
|
|
89
|
-
|
|
128
|
+
3. Copy the exact `transactionRef` for the transaction and a `categoryId` from
|
|
129
|
+
the earlier outputs into `assignments.json`:
|
|
90
130
|
|
|
91
|
-
```
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
131
|
+
```json
|
|
132
|
+
{
|
|
133
|
+
"assignments": [
|
|
134
|
+
{
|
|
135
|
+
"transactionRef": "PASTE_THE_EXACT_TRANSACTION_REF_HERE",
|
|
136
|
+
"categoryId": "PASTE_A_CATEGORY_ID_HERE"
|
|
137
|
+
}
|
|
138
|
+
]
|
|
139
|
+
}
|
|
96
140
|
```
|
|
97
141
|
|
|
98
|
-
|
|
142
|
+
These are placeholders. Do not submit the example values.
|
|
143
|
+
|
|
144
|
+
4. Preview the assignment without writing:
|
|
99
145
|
|
|
100
146
|
```bash
|
|
101
147
|
sloth-agent assign --input assignments.json
|
|
102
148
|
```
|
|
103
149
|
|
|
104
|
-
|
|
150
|
+
Without `--apply`, the CLI checks that the file is valid and returns the
|
|
151
|
+
payload it would send. It does not contact Sloth Money, verify the
|
|
152
|
+
`transactionRef` or category values, or write anything. A successful preview
|
|
153
|
+
does not guarantee that applying it will succeed.
|
|
154
|
+
|
|
155
|
+
5. Apply the same file:
|
|
105
156
|
|
|
106
157
|
```bash
|
|
107
158
|
sloth-agent assign --input assignments.json --apply
|
|
108
159
|
```
|
|
109
160
|
|
|
110
|
-
|
|
161
|
+
Inspect every item in the returned `succeeded` and `failed` arrays.
|
|
162
|
+
|
|
163
|
+
6. Check the result. Successful assignments update the category and optional
|
|
164
|
+
budget line item on the original transaction. See the result in **Sloth
|
|
165
|
+
Money → Transactions**, or re-run the original transaction query without
|
|
166
|
+
`--uncategorized` and inspect its category fields:
|
|
167
|
+
|
|
168
|
+
```bash
|
|
169
|
+
sloth-agent transactions --limit 50
|
|
170
|
+
```
|
|
171
|
+
|
|
172
|
+
The transaction should also disappear from the matching `--uncategorized`
|
|
173
|
+
query. Assignments do not create a separate list.
|
|
174
|
+
|
|
175
|
+
### Other workflows
|
|
176
|
+
|
|
177
|
+
List your goals:
|
|
178
|
+
|
|
179
|
+
```bash
|
|
180
|
+
sloth-agent goals
|
|
181
|
+
```
|
|
182
|
+
|
|
183
|
+
Goal writes are previews unless `--apply` is present:
|
|
184
|
+
|
|
185
|
+
```bash
|
|
186
|
+
sloth-agent goals create \
|
|
187
|
+
--name "Emergency fund" \
|
|
188
|
+
--target-amount 12000 \
|
|
189
|
+
--target-month 2027-06
|
|
190
|
+
|
|
191
|
+
sloth-agent goals create \
|
|
192
|
+
--name "Emergency fund" \
|
|
193
|
+
--target-amount 12000 \
|
|
194
|
+
--target-month 2027-06 \
|
|
195
|
+
--apply
|
|
196
|
+
```
|
|
197
|
+
|
|
198
|
+
Use the `id` from list or create output to update or delete a goal:
|
|
199
|
+
|
|
200
|
+
```bash
|
|
201
|
+
sloth-agent goals update \
|
|
202
|
+
--goal-id goal-id \
|
|
203
|
+
--clear-target-amount \
|
|
204
|
+
--target-month 2027-12 \
|
|
205
|
+
--achieved=false \
|
|
206
|
+
--apply
|
|
207
|
+
|
|
208
|
+
sloth-agent goals delete --goal-id goal-id --apply
|
|
209
|
+
```
|
|
210
|
+
|
|
211
|
+
Updates are partial. Use `--clear-target-amount` or `--clear-target-month` to
|
|
212
|
+
remove an optional value. Marking a goal achieved removes its forecast
|
|
213
|
+
assignment. Deleting a goal also removes its forecast assignments and drift
|
|
214
|
+
history. Goal sharing remains app-managed. Change an active shared goal's
|
|
215
|
+
pot-tracked target amount in the Sloth Budget app, where account balances can
|
|
216
|
+
be reallocated across goals in priority order.
|
|
217
|
+
|
|
218
|
+
Read uncategorised contributions to the joint budget:
|
|
111
219
|
|
|
112
220
|
```bash
|
|
113
|
-
sloth-agent
|
|
221
|
+
sloth-agent transactions --assignment-scope joint --uncategorized
|
|
114
222
|
```
|
|
115
223
|
|
|
116
|
-
|
|
224
|
+
The first transaction read after the UTC day changes may refresh linked bank
|
|
225
|
+
data. The CLI waits up to 45 seconds for that refresh to persist, then returns
|
|
226
|
+
the requested booked transactions. If the refresh is still running, partially
|
|
227
|
+
fails, or fails globally, readable cached transactions are still returned with
|
|
228
|
+
a structured `refresh` object:
|
|
117
229
|
|
|
118
230
|
```json
|
|
119
231
|
{
|
|
120
|
-
"
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
}
|
|
126
|
-
]
|
|
232
|
+
"refresh": {
|
|
233
|
+
"status": "in_progress",
|
|
234
|
+
"reason": "wait_timeout",
|
|
235
|
+
"utcDate": "2026-07-31"
|
|
236
|
+
}
|
|
127
237
|
}
|
|
128
238
|
```
|
|
129
239
|
|
|
240
|
+
Re-run the transaction query later to observe the completed refresh. A partial
|
|
241
|
+
account failure remains eligible for an automatic retry.
|
|
242
|
+
|
|
243
|
+
Set `"assignmentScope": "joint"` on an assignment to categorise the eligible
|
|
244
|
+
shared portion for the joint budget.
|
|
245
|
+
|
|
246
|
+
Set whether the shared portions of personal-account transactions count in the
|
|
247
|
+
linked joint budget. The first command previews; the second applies:
|
|
248
|
+
|
|
249
|
+
```bash
|
|
250
|
+
sloth-agent joint-budget-settings \
|
|
251
|
+
--include-shared-personal-transactions=true
|
|
252
|
+
sloth-agent joint-budget-settings \
|
|
253
|
+
--include-shared-personal-transactions=true \
|
|
254
|
+
--apply
|
|
255
|
+
```
|
|
256
|
+
|
|
257
|
+
Create a partner clarification link:
|
|
258
|
+
|
|
259
|
+
```bash
|
|
260
|
+
sloth-agent ask-partner \
|
|
261
|
+
--transaction-ref PASTE_THE_EXACT_TRANSACTION_REF_HERE
|
|
262
|
+
```
|
|
263
|
+
|
|
264
|
+
The value shown is a placeholder. Copy the exact `transactionRef` from
|
|
265
|
+
`sloth-agent transactions` output.
|
|
266
|
+
|
|
130
267
|
## Configuration and output
|
|
131
268
|
|
|
132
269
|
The CLI defaults to `https://budget.slothmoney.app`. For local development,
|
|
@@ -144,7 +281,7 @@ Command results are JSON on stdout. Diagnostics are written to stderr.
|
|
|
144
281
|
| --- | --- |
|
|
145
282
|
| `0` | Success |
|
|
146
283
|
| `1` | API, network, credential-store, response-validation, or partial assignment failure |
|
|
147
|
-
| `2` | Invalid command, option, URL, date, auth input, or assignment input |
|
|
284
|
+
| `2` | Invalid command, option, URL, date, auth input, goal input, or assignment input |
|
|
148
285
|
| `3` | No credential or native secure storage is unavailable |
|
|
149
286
|
|
|
150
287
|
Assignment writes are best-effort. A response containing any failed assignment
|
package/dist/args.js
CHANGED
|
@@ -47,6 +47,47 @@ function requireNonEmpty(value, name) {
|
|
|
47
47
|
throw new UsageError(`${name} requires a value`);
|
|
48
48
|
return value;
|
|
49
49
|
}
|
|
50
|
+
function parseGoalAmount(value, name) {
|
|
51
|
+
if (!/^\d+(?:\.\d{1,2})?$/.test(value)) {
|
|
52
|
+
throw new UsageError(`${name} must be a positive amount with at most two decimal places`);
|
|
53
|
+
}
|
|
54
|
+
const amount = Number(value);
|
|
55
|
+
if (!Number.isFinite(amount) || amount <= 0) {
|
|
56
|
+
throw new UsageError(`${name} must be a positive amount with at most two decimal places`);
|
|
57
|
+
}
|
|
58
|
+
return amount;
|
|
59
|
+
}
|
|
60
|
+
function parseGoalMonthKey(value, name) {
|
|
61
|
+
if (!/^\d{4}-(0[1-9]|1[0-2])$/.test(value)) {
|
|
62
|
+
throw new UsageError(`${name} must be a valid YYYY-MM month`);
|
|
63
|
+
}
|
|
64
|
+
return value;
|
|
65
|
+
}
|
|
66
|
+
function parseExplicitBoolean(value, name) {
|
|
67
|
+
if (value !== 'true' && value !== 'false') {
|
|
68
|
+
throw new UsageError(`${name} must be true or false`);
|
|
69
|
+
}
|
|
70
|
+
return value === 'true';
|
|
71
|
+
}
|
|
72
|
+
function parseGoalName(value) {
|
|
73
|
+
const name = value.trim();
|
|
74
|
+
if (name.length > 200) {
|
|
75
|
+
throw new UsageError('--name must be at most 200 characters');
|
|
76
|
+
}
|
|
77
|
+
return name;
|
|
78
|
+
}
|
|
79
|
+
function parseGoalId(value) {
|
|
80
|
+
const goalId = value.trim();
|
|
81
|
+
if (goalId.length > 500
|
|
82
|
+
|| goalId.includes('/')
|
|
83
|
+
|| Array.from(goalId).some((character) => {
|
|
84
|
+
const codePoint = character.codePointAt(0);
|
|
85
|
+
return codePoint !== undefined && (codePoint < 32 || codePoint === 127);
|
|
86
|
+
})) {
|
|
87
|
+
throw new UsageError('--goal-id must be a valid goal document ID');
|
|
88
|
+
}
|
|
89
|
+
return goalId;
|
|
90
|
+
}
|
|
50
91
|
function parseTransactions(args) {
|
|
51
92
|
const filters = {};
|
|
52
93
|
for (let index = 0; index < args.length; index += 1) {
|
|
@@ -73,6 +114,7 @@ function parseTransactions(args) {
|
|
|
73
114
|
'--q',
|
|
74
115
|
'--account-id',
|
|
75
116
|
'--category-id',
|
|
117
|
+
'--assignment-scope',
|
|
76
118
|
'--cursor',
|
|
77
119
|
]);
|
|
78
120
|
if (!name || !supported.has(name)) {
|
|
@@ -108,6 +150,12 @@ function parseTransactions(args) {
|
|
|
108
150
|
else if (name === '--category-id') {
|
|
109
151
|
filters.categoryId = setOnce(filters.categoryId, value, name);
|
|
110
152
|
}
|
|
153
|
+
else if (name === '--assignment-scope') {
|
|
154
|
+
if (value !== 'personal' && value !== 'joint') {
|
|
155
|
+
throw new UsageError('--assignment-scope must be personal or joint');
|
|
156
|
+
}
|
|
157
|
+
filters.assignmentScope = setOnce(filters.assignmentScope, value, name);
|
|
158
|
+
}
|
|
111
159
|
else if (name === '--cursor') {
|
|
112
160
|
filters.cursor = setOnce(filters.cursor, value, name);
|
|
113
161
|
}
|
|
@@ -162,9 +210,221 @@ function parseAuth(args, baseUrl) {
|
|
|
162
210
|
}
|
|
163
211
|
throw new UsageError(`Unknown auth command: ${authCommand}`);
|
|
164
212
|
}
|
|
213
|
+
function parseGoals(args, baseUrl) {
|
|
214
|
+
const subcommand = args.shift();
|
|
215
|
+
if (subcommand === undefined || subcommand === 'list') {
|
|
216
|
+
if (args.length > 0) {
|
|
217
|
+
throw new UsageError(`Unknown goals list option: ${args[0]}`);
|
|
218
|
+
}
|
|
219
|
+
return withBaseUrl({ command: 'goals-list' }, baseUrl);
|
|
220
|
+
}
|
|
221
|
+
if (subcommand === 'create') {
|
|
222
|
+
let name;
|
|
223
|
+
let targetAmount;
|
|
224
|
+
let targetMonthKey;
|
|
225
|
+
let apply = false;
|
|
226
|
+
for (let index = 0; index < args.length; index += 1) {
|
|
227
|
+
const argument = args[index];
|
|
228
|
+
if (argument === '--apply') {
|
|
229
|
+
if (apply)
|
|
230
|
+
throw new UsageError('--apply may only be provided once');
|
|
231
|
+
apply = true;
|
|
232
|
+
continue;
|
|
233
|
+
}
|
|
234
|
+
const [option, inlineValue] = argument.includes('=')
|
|
235
|
+
? argument.split(/=(.*)/s, 2)
|
|
236
|
+
: [argument, undefined];
|
|
237
|
+
if (option !== '--name'
|
|
238
|
+
&& option !== '--target-amount'
|
|
239
|
+
&& option !== '--target-month') {
|
|
240
|
+
throw new UsageError(`Unknown goals create option: ${argument}`);
|
|
241
|
+
}
|
|
242
|
+
const value = requireNonEmpty(inlineValue ?? readOptionValue(args, index, option), option);
|
|
243
|
+
if (inlineValue === undefined)
|
|
244
|
+
index += 1;
|
|
245
|
+
if (option === '--name') {
|
|
246
|
+
name = setOnce(name, parseGoalName(value), option);
|
|
247
|
+
}
|
|
248
|
+
else if (option === '--target-amount') {
|
|
249
|
+
targetAmount = setOnce(targetAmount, parseGoalAmount(value, option), option);
|
|
250
|
+
}
|
|
251
|
+
else {
|
|
252
|
+
targetMonthKey = setOnce(targetMonthKey, parseGoalMonthKey(value, option), option);
|
|
253
|
+
}
|
|
254
|
+
}
|
|
255
|
+
if (!name)
|
|
256
|
+
throw new UsageError('goals create requires --name <name>');
|
|
257
|
+
return withBaseUrl({
|
|
258
|
+
command: 'goals-create',
|
|
259
|
+
name,
|
|
260
|
+
...(targetAmount === undefined ? {} : { targetAmount }),
|
|
261
|
+
...(targetMonthKey === undefined ? {} : { targetMonthKey }),
|
|
262
|
+
apply,
|
|
263
|
+
}, baseUrl);
|
|
264
|
+
}
|
|
265
|
+
if (subcommand === 'update') {
|
|
266
|
+
let goalId;
|
|
267
|
+
let name;
|
|
268
|
+
let targetAmount;
|
|
269
|
+
let targetMonthKey;
|
|
270
|
+
let isAchieved;
|
|
271
|
+
let apply = false;
|
|
272
|
+
for (let index = 0; index < args.length; index += 1) {
|
|
273
|
+
const argument = args[index];
|
|
274
|
+
if (argument === '--apply') {
|
|
275
|
+
if (apply)
|
|
276
|
+
throw new UsageError('--apply may only be provided once');
|
|
277
|
+
apply = true;
|
|
278
|
+
continue;
|
|
279
|
+
}
|
|
280
|
+
if (argument === '--clear-target-amount') {
|
|
281
|
+
if (targetAmount !== undefined) {
|
|
282
|
+
throw new UsageError('--target-amount and --clear-target-amount are mutually exclusive');
|
|
283
|
+
}
|
|
284
|
+
targetAmount = null;
|
|
285
|
+
continue;
|
|
286
|
+
}
|
|
287
|
+
if (argument === '--clear-target-month') {
|
|
288
|
+
if (targetMonthKey !== undefined) {
|
|
289
|
+
throw new UsageError('--target-month and --clear-target-month are mutually exclusive');
|
|
290
|
+
}
|
|
291
|
+
targetMonthKey = null;
|
|
292
|
+
continue;
|
|
293
|
+
}
|
|
294
|
+
const [option, inlineValue] = argument.includes('=')
|
|
295
|
+
? argument.split(/=(.*)/s, 2)
|
|
296
|
+
: [argument, undefined];
|
|
297
|
+
if (option !== '--goal-id'
|
|
298
|
+
&& option !== '--name'
|
|
299
|
+
&& option !== '--target-amount'
|
|
300
|
+
&& option !== '--target-month'
|
|
301
|
+
&& option !== '--achieved') {
|
|
302
|
+
throw new UsageError(`Unknown goals update option: ${argument}`);
|
|
303
|
+
}
|
|
304
|
+
const value = requireNonEmpty(inlineValue ?? readOptionValue(args, index, option), option);
|
|
305
|
+
if (inlineValue === undefined)
|
|
306
|
+
index += 1;
|
|
307
|
+
if (option === '--goal-id') {
|
|
308
|
+
goalId = setOnce(goalId, parseGoalId(value), option);
|
|
309
|
+
}
|
|
310
|
+
else if (option === '--name') {
|
|
311
|
+
name = setOnce(name, parseGoalName(value), option);
|
|
312
|
+
}
|
|
313
|
+
else if (option === '--target-amount') {
|
|
314
|
+
if (targetAmount !== undefined) {
|
|
315
|
+
throw new UsageError('--target-amount and --clear-target-amount are mutually exclusive');
|
|
316
|
+
}
|
|
317
|
+
targetAmount = parseGoalAmount(value, option);
|
|
318
|
+
}
|
|
319
|
+
else if (option === '--target-month') {
|
|
320
|
+
if (targetMonthKey !== undefined) {
|
|
321
|
+
throw new UsageError('--target-month and --clear-target-month are mutually exclusive');
|
|
322
|
+
}
|
|
323
|
+
targetMonthKey = parseGoalMonthKey(value, option);
|
|
324
|
+
}
|
|
325
|
+
else {
|
|
326
|
+
isAchieved = setOnce(isAchieved, parseExplicitBoolean(value, option), option);
|
|
327
|
+
}
|
|
328
|
+
}
|
|
329
|
+
if (!goalId)
|
|
330
|
+
throw new UsageError('goals update requires --goal-id <id>');
|
|
331
|
+
if (name === undefined
|
|
332
|
+
&& targetAmount === undefined
|
|
333
|
+
&& targetMonthKey === undefined
|
|
334
|
+
&& isAchieved === undefined) {
|
|
335
|
+
throw new UsageError('goals update requires at least one field to update');
|
|
336
|
+
}
|
|
337
|
+
return withBaseUrl({
|
|
338
|
+
command: 'goals-update',
|
|
339
|
+
goalId,
|
|
340
|
+
...(name === undefined ? {} : { name }),
|
|
341
|
+
...(targetAmount === undefined ? {} : { targetAmount }),
|
|
342
|
+
...(targetMonthKey === undefined ? {} : { targetMonthKey }),
|
|
343
|
+
...(isAchieved === undefined ? {} : { isAchieved }),
|
|
344
|
+
apply,
|
|
345
|
+
}, baseUrl);
|
|
346
|
+
}
|
|
347
|
+
if (subcommand === 'delete') {
|
|
348
|
+
let goalId;
|
|
349
|
+
let apply = false;
|
|
350
|
+
for (let index = 0; index < args.length; index += 1) {
|
|
351
|
+
const argument = args[index];
|
|
352
|
+
if (argument === '--apply') {
|
|
353
|
+
if (apply)
|
|
354
|
+
throw new UsageError('--apply may only be provided once');
|
|
355
|
+
apply = true;
|
|
356
|
+
continue;
|
|
357
|
+
}
|
|
358
|
+
const [option, inlineValue] = argument.includes('=')
|
|
359
|
+
? argument.split(/=(.*)/s, 2)
|
|
360
|
+
: [argument, undefined];
|
|
361
|
+
if (option !== '--goal-id') {
|
|
362
|
+
throw new UsageError(`Unknown goals delete option: ${argument}`);
|
|
363
|
+
}
|
|
364
|
+
const value = requireNonEmpty(inlineValue ?? readOptionValue(args, index, option), option);
|
|
365
|
+
if (inlineValue === undefined)
|
|
366
|
+
index += 1;
|
|
367
|
+
goalId = setOnce(goalId, parseGoalId(value), option);
|
|
368
|
+
}
|
|
369
|
+
if (!goalId)
|
|
370
|
+
throw new UsageError('goals delete requires --goal-id <id>');
|
|
371
|
+
return withBaseUrl({
|
|
372
|
+
command: 'goals-delete',
|
|
373
|
+
goalId,
|
|
374
|
+
apply,
|
|
375
|
+
}, baseUrl);
|
|
376
|
+
}
|
|
377
|
+
throw new UsageError(`Unknown goals command: ${subcommand}`);
|
|
378
|
+
}
|
|
379
|
+
function helpTopic(argv) {
|
|
380
|
+
const positionals = [];
|
|
381
|
+
for (let index = 0; index < argv.length; index += 1) {
|
|
382
|
+
const argument = argv[index];
|
|
383
|
+
if (argument === undefined)
|
|
384
|
+
continue;
|
|
385
|
+
if (argument === '--base-url') {
|
|
386
|
+
index += 1;
|
|
387
|
+
continue;
|
|
388
|
+
}
|
|
389
|
+
if (argument.startsWith('--base-url=') || argument.startsWith('-')) {
|
|
390
|
+
continue;
|
|
391
|
+
}
|
|
392
|
+
positionals.push(argument);
|
|
393
|
+
}
|
|
394
|
+
const [command, subcommand] = positionals;
|
|
395
|
+
if (command === 'auth') {
|
|
396
|
+
if (subcommand === 'login'
|
|
397
|
+
|| subcommand === 'status'
|
|
398
|
+
|| subcommand === 'logout') {
|
|
399
|
+
return `auth-${subcommand}`;
|
|
400
|
+
}
|
|
401
|
+
return 'auth';
|
|
402
|
+
}
|
|
403
|
+
if (command === 'goals') {
|
|
404
|
+
if (subcommand === 'list')
|
|
405
|
+
return 'goals-list';
|
|
406
|
+
if (subcommand === 'create')
|
|
407
|
+
return 'goals-create';
|
|
408
|
+
if (subcommand === 'update')
|
|
409
|
+
return 'goals-update';
|
|
410
|
+
if (subcommand === 'delete')
|
|
411
|
+
return 'goals-delete';
|
|
412
|
+
return 'goals';
|
|
413
|
+
}
|
|
414
|
+
if (command === 'categories'
|
|
415
|
+
|| command === 'transactions'
|
|
416
|
+
|| command === 'assign'
|
|
417
|
+
|| command === 'joint-budget-settings'
|
|
418
|
+
|| command === 'ask-partner') {
|
|
419
|
+
return command;
|
|
420
|
+
}
|
|
421
|
+
return undefined;
|
|
422
|
+
}
|
|
165
423
|
export function parseArgs(argv) {
|
|
166
|
-
if (argv.includes('--help') || argv.includes('-h'))
|
|
167
|
-
|
|
424
|
+
if (argv.includes('--help') || argv.includes('-h')) {
|
|
425
|
+
const topic = helpTopic(argv);
|
|
426
|
+
return topic ? { command: 'help', topic } : { command: 'help' };
|
|
427
|
+
}
|
|
168
428
|
if (argv.includes('--version') || argv.includes('-V'))
|
|
169
429
|
return { command: 'version' };
|
|
170
430
|
const { args, baseUrl } = parseGlobalOptions(argv);
|
|
@@ -174,6 +434,9 @@ export function parseArgs(argv) {
|
|
|
174
434
|
if (command === 'auth') {
|
|
175
435
|
return parseAuth(args, baseUrl);
|
|
176
436
|
}
|
|
437
|
+
if (command === 'goals') {
|
|
438
|
+
return parseGoals(args, baseUrl);
|
|
439
|
+
}
|
|
177
440
|
if (command === 'categories') {
|
|
178
441
|
if (args.length > 0) {
|
|
179
442
|
throw new UsageError(`Unknown categories option: ${args[0]}`);
|
|
@@ -208,6 +471,43 @@ export function parseArgs(argv) {
|
|
|
208
471
|
throw new UsageError('assign requires --input <file>');
|
|
209
472
|
return withBaseUrl({ command, input, apply }, baseUrl);
|
|
210
473
|
}
|
|
474
|
+
if (command === 'joint-budget-settings') {
|
|
475
|
+
let includeSharedPersonalTransactions;
|
|
476
|
+
let apply = false;
|
|
477
|
+
for (let index = 0; index < args.length; index += 1) {
|
|
478
|
+
const argument = args[index];
|
|
479
|
+
if (argument === '--apply') {
|
|
480
|
+
if (apply)
|
|
481
|
+
throw new UsageError('--apply may only be provided once');
|
|
482
|
+
apply = true;
|
|
483
|
+
continue;
|
|
484
|
+
}
|
|
485
|
+
const optionName = '--include-shared-personal-transactions';
|
|
486
|
+
if (argument === optionName || argument.startsWith(`${optionName}=`)) {
|
|
487
|
+
const value = argument === optionName
|
|
488
|
+
? readOptionValue(args, index, optionName)
|
|
489
|
+
: argument.slice(`${optionName}=`.length);
|
|
490
|
+
if (argument === optionName)
|
|
491
|
+
index += 1;
|
|
492
|
+
if (value !== 'true' && value !== 'false') {
|
|
493
|
+
throw new UsageError(`${optionName} must be true or false`);
|
|
494
|
+
}
|
|
495
|
+
includeSharedPersonalTransactions = setOnce(includeSharedPersonalTransactions, value === 'true', optionName);
|
|
496
|
+
continue;
|
|
497
|
+
}
|
|
498
|
+
throw new UsageError(`Unknown joint-budget-settings option: ${argument}`);
|
|
499
|
+
}
|
|
500
|
+
if (apply && includeSharedPersonalTransactions === undefined) {
|
|
501
|
+
throw new UsageError('--apply requires --include-shared-personal-transactions=true|false');
|
|
502
|
+
}
|
|
503
|
+
return withBaseUrl({
|
|
504
|
+
command,
|
|
505
|
+
...(includeSharedPersonalTransactions === undefined
|
|
506
|
+
? {}
|
|
507
|
+
: { includeSharedPersonalTransactions }),
|
|
508
|
+
apply,
|
|
509
|
+
}, baseUrl);
|
|
510
|
+
}
|
|
211
511
|
if (command === 'ask-partner') {
|
|
212
512
|
let transactionRef;
|
|
213
513
|
for (let index = 0; index < args.length; index += 1) {
|