@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 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 read and categorise transactions through the
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.2.0 -- sloth-agent --help
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 save it in your operating system's
25
- secure credential store:
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
- For a non-interactive import, pass the token through stdin or import it from
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
- Never put a token in a command argument. For CI and headless systems, keep
43
- using an environment secret:
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
- To migrate from an existing environment-only setup:
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
- Check the active credential with a live API request:
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
- Read categories and available budget line items:
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
- Read uncategorised transactions:
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
- Search a date range:
128
+ 3. Copy the exact `transactionRef` for the transaction and a `categoryId` from
129
+ the earlier outputs into `assignments.json`:
90
130
 
91
- ```bash
92
- sloth-agent transactions \
93
- --q "tesco" \
94
- --start-date 2026-05-01 \
95
- --end-date 2026-05-31
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
- Preview an assignment file without writing:
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
- Apply the same file:
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
- Create a partner clarification link:
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 ask-partner --transaction-ref sloth_txn_...
221
+ sloth-agent transactions --assignment-scope joint --uncategorized
114
222
  ```
115
223
 
116
- Assignment files use the Agent API request shape:
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
- "assignments": [
121
- {
122
- "transactionRef": "sloth_txn_...",
123
- "categoryId": "groceries",
124
- "lineItemId": "weekly"
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
- return { command: 'help' };
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) {