@slothmoney/agent-cli 0.9.1 → 0.11.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/CHANGELOG.md +15 -1
- package/README.md +73 -20
- package/dist/args.js +95 -40
- package/dist/cli.js +176 -36
- package/dist/contracts.js +83 -19
- package/dist/goal-metadata.js +4 -0
- package/package.json +1 -1
package/CHANGELOG.md
CHANGED
|
@@ -1,6 +1,20 @@
|
|
|
1
1
|
# Changelog
|
|
2
2
|
|
|
3
|
-
##
|
|
3
|
+
## 0.11.0 - 2026-08-16
|
|
4
|
+
|
|
5
|
+
- Add preview-by-default `budget move` for atomically moving current assigned
|
|
6
|
+
money between categories or To Assign without changing planned budgets.
|
|
7
|
+
- Accept human-readable currency amounts at the CLI boundary, send integer
|
|
8
|
+
pence to the Agent API, and validate the returned affected balances.
|
|
9
|
+
|
|
10
|
+
## 0.10.0 - 2026-08-15
|
|
11
|
+
|
|
12
|
+
- Require every goal create to specify a positive target amount and Keep or
|
|
13
|
+
Spend type, matching the breaking Agent Goals API v1 contract.
|
|
14
|
+
- Expose `goalType` and nullable `spentAt`, allow active goal type changes, and
|
|
15
|
+
remove obsolete `isAchieved` and target-amount clearing assumptions.
|
|
16
|
+
- Add preview-by-default `goals mark-spent` and `goals restore` actions that
|
|
17
|
+
send the canonical `isSpent` lifecycle update only with `--apply`.
|
|
4
18
|
|
|
5
19
|
## 0.9.1 - 2026-08-15
|
|
6
20
|
|
package/README.md
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
# Sloth Agent CLI
|
|
2
2
|
|
|
3
|
-
Use your own agent to inspect accounts, investments, and budgets, manage goals, update planned amounts, and categorise transactions through the
|
|
3
|
+
Use your own agent to inspect accounts, investments, and budgets, manage goals, move assigned budget money, update planned amounts, and categorise transactions through the
|
|
4
4
|
[Sloth Money Agent API](https://slothmoney.app/developers/).
|
|
5
5
|
|
|
6
6
|
## Install
|
|
@@ -15,7 +15,7 @@ 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.11.0 -- sloth-agent --help
|
|
19
19
|
```
|
|
20
20
|
|
|
21
21
|
## Authenticate
|
|
@@ -27,7 +27,7 @@ where the CLI runs.
|
|
|
27
27
|
New tokens are view-only. That is enough for `auth status`, `accounts`, `investments`,
|
|
28
28
|
`budget`, `categories`, `transactions`, and `goals` list. Enable **Allow changes** when
|
|
29
29
|
creating the token only if the CLI must apply assignments, manage categories
|
|
30
|
-
or line items, update planned budgets, manage accounts, ask a partner for an explanation, or manage goals. Token
|
|
30
|
+
or line items, move assigned budget money, update planned budgets, manage accounts, ask a partner for an explanation, or manage goals. Token
|
|
31
31
|
permissions cannot be changed later - revoke and reissue the token instead.
|
|
32
32
|
|
|
33
33
|
### Local computer
|
|
@@ -105,6 +105,7 @@ sloth-agent auth login --help
|
|
|
105
105
|
sloth-agent accounts --help
|
|
106
106
|
sloth-agent budget --help
|
|
107
107
|
sloth-agent budget update --help
|
|
108
|
+
sloth-agent budget move --help
|
|
108
109
|
sloth-agent categories --help
|
|
109
110
|
sloth-agent categories create --help
|
|
110
111
|
sloth-agent line-items create --help
|
|
@@ -112,6 +113,8 @@ sloth-agent transactions --help
|
|
|
112
113
|
sloth-agent assign --help
|
|
113
114
|
sloth-agent goals create --help
|
|
114
115
|
sloth-agent goals update --help
|
|
116
|
+
sloth-agent goals mark-spent --help
|
|
117
|
+
sloth-agent goals restore --help
|
|
115
118
|
sloth-agent ask-partner --help
|
|
116
119
|
```
|
|
117
120
|
|
|
@@ -283,6 +286,37 @@ period and everything after it. Earlier and historical periods remain unchanged.
|
|
|
283
286
|
Without `--apply`, the CLI validates the file locally and does not load a token
|
|
284
287
|
or contact Sloth Money. Applying requires a write-enabled token.
|
|
285
288
|
|
|
289
|
+
Move current assigned money between two categories, or use the reserved
|
|
290
|
+
`to-assign` ID to move money to or from To Assign:
|
|
291
|
+
|
|
292
|
+
```bash
|
|
293
|
+
sloth-agent budget move \
|
|
294
|
+
--scope personal \
|
|
295
|
+
--from-category-id activities \
|
|
296
|
+
--to-category-id groceries \
|
|
297
|
+
--amount 52.95
|
|
298
|
+
|
|
299
|
+
sloth-agent budget move \
|
|
300
|
+
--scope personal \
|
|
301
|
+
--from-category-id activities \
|
|
302
|
+
--to-category-id groceries \
|
|
303
|
+
--amount 52.95 \
|
|
304
|
+
--apply
|
|
305
|
+
```
|
|
306
|
+
|
|
307
|
+
Copy category IDs from `sloth-agent budget` output. `--amount` is expressed in
|
|
308
|
+
the budget currency and accepts up to two decimal places; the CLI converts the
|
|
309
|
+
decimal digits exactly and sends a positive safe-integer number of pence to the
|
|
310
|
+
API. Without `--apply`, the command validates and prints the exact request
|
|
311
|
+
without loading credentials or contacting Sloth Money.
|
|
312
|
+
|
|
313
|
+
Applying subtracts and adds the amount atomically, records the movement in the
|
|
314
|
+
budget history, and returns the affected assigned balances. It does not change
|
|
315
|
+
planned line-item amounts or future budget plans. Like the UI, it permits a
|
|
316
|
+
source category or To Assign to become negative; an automated workflow should
|
|
317
|
+
choose donors from its own available-balance policy. Historical periods cannot
|
|
318
|
+
be changed, and applying requires a write-enabled token.
|
|
319
|
+
|
|
286
320
|
Create or rename a custom category. Writes are previews until `--apply` is
|
|
287
321
|
present:
|
|
288
322
|
|
|
@@ -413,45 +447,64 @@ Goal writes are previews unless `--apply` is present:
|
|
|
413
447
|
sloth-agent goals create \
|
|
414
448
|
--name "Emergency fund" \
|
|
415
449
|
--target-amount 12000 \
|
|
416
|
-
--
|
|
450
|
+
--type keep
|
|
417
451
|
|
|
418
452
|
sloth-agent goals create \
|
|
419
|
-
--name "
|
|
420
|
-
--target-amount
|
|
453
|
+
--name "Wedding" \
|
|
454
|
+
--target-amount 22000 \
|
|
421
455
|
--target-month 2027-06 \
|
|
456
|
+
--type spend \
|
|
422
457
|
--apply
|
|
423
458
|
```
|
|
424
459
|
|
|
425
|
-
|
|
460
|
+
Every goal is either Keep or Spend. A Keep goal continues reserving its funded
|
|
461
|
+
money. A Spend goal reserves money until you explicitly mark it spent. Goal
|
|
462
|
+
list, create, and update output includes lowercase `goalType` and nullable
|
|
463
|
+
`spentAt`; `spentAt` is an ISO timestamp only after a Spend goal is marked
|
|
464
|
+
spent.
|
|
465
|
+
|
|
466
|
+
Use the `id` from list or create output to update a goal, change its type, or
|
|
467
|
+
move it in the priority order:
|
|
426
468
|
|
|
427
469
|
```bash
|
|
428
470
|
sloth-agent goals update \
|
|
429
471
|
--goal-id goal-id \
|
|
430
|
-
--clear-target-amount \
|
|
431
472
|
--target-month 2027-12 \
|
|
432
|
-
--
|
|
473
|
+
--type spend \
|
|
433
474
|
--apply
|
|
434
475
|
|
|
435
476
|
sloth-agent goals update \
|
|
436
477
|
--goal-id house-goal-id \
|
|
437
478
|
--priority 2 \
|
|
438
479
|
--apply
|
|
480
|
+
```
|
|
481
|
+
|
|
482
|
+
Marking spent and restoring are also previews by default:
|
|
483
|
+
|
|
484
|
+
```bash
|
|
485
|
+
sloth-agent goals mark-spent --goal-id goal-id
|
|
486
|
+
sloth-agent goals mark-spent --goal-id goal-id --apply
|
|
487
|
+
|
|
488
|
+
sloth-agent goals restore --goal-id goal-id
|
|
489
|
+
sloth-agent goals restore --goal-id goal-id --apply
|
|
439
490
|
|
|
440
491
|
sloth-agent goals delete --goal-id goal-id --apply
|
|
441
492
|
```
|
|
442
493
|
|
|
443
|
-
Updates are partial. Use `--clear-target-
|
|
444
|
-
|
|
445
|
-
|
|
446
|
-
|
|
447
|
-
|
|
448
|
-
|
|
494
|
+
Updates are partial. Use `--clear-target-month` to remove the optional month.
|
|
495
|
+
A Keep goal cannot be marked spent. A spent goal must be restored before its
|
|
496
|
+
type can change; the API returns these lifecycle conflicts without hiding the
|
|
497
|
+
required recovery action. Restoring clears `spentAt` and returns the goal to
|
|
498
|
+
allocation at its saved priority. Deleting a goal also removes its forecast
|
|
499
|
+
assignments and drift history.
|
|
500
|
+
|
|
501
|
+
Goal sharing remains app-managed. Change an active shared goal's pot-tracked
|
|
502
|
+
target amount in the Sloth Budget app, where account balances can be
|
|
503
|
+
reallocated across goals in priority order. Goal list output includes a
|
|
449
504
|
one-based `priority`; `1` is highest. Moving one goal automatically shifts the
|
|
450
|
-
goals between its old and new positions. The
|
|
451
|
-
|
|
452
|
-
|
|
453
|
-
progress are browser-owned derived state and refresh when the owner next opens
|
|
454
|
-
the Forecast screen.
|
|
505
|
+
goals between its old and new positions. The priority option must be used on
|
|
506
|
+
its own. Forecast assignments and shared pot progress are browser-owned
|
|
507
|
+
derived state and refresh when the owner next opens the Forecast screen.
|
|
455
508
|
|
|
456
509
|
Read uncategorised contributions to the joint budget:
|
|
457
510
|
|
package/dist/args.js
CHANGED
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import { UsageError } from './errors.js';
|
|
2
2
|
import { CATEGORY_TYPES, ICON_KEYS, } from './category-metadata.js';
|
|
3
|
+
import { isGoalType, } from './goal-metadata.js';
|
|
3
4
|
const PRODUCTION_BASE_URL = 'https://budget.slothmoney.app';
|
|
4
5
|
const LOCAL_HOSTS = new Set(['localhost', '127.0.0.1', '[::1]']);
|
|
5
6
|
function readOptionValue(args, index, name) {
|
|
@@ -48,16 +49,33 @@ function requireNonEmpty(value, name) {
|
|
|
48
49
|
throw new UsageError(`${name} requires a value`);
|
|
49
50
|
return value;
|
|
50
51
|
}
|
|
51
|
-
function
|
|
52
|
+
function validatePositiveDecimalAmount(value, name) {
|
|
52
53
|
if (!/^\d+(?:\.\d{1,2})?$/.test(value)) {
|
|
53
54
|
throw new UsageError(`${name} must be a positive amount with at most two decimal places`);
|
|
54
55
|
}
|
|
56
|
+
const digits = value.replace('.', '');
|
|
57
|
+
if (!/[1-9]/.test(digits)) {
|
|
58
|
+
throw new UsageError(`${name} must be a positive amount with at most two decimal places`);
|
|
59
|
+
}
|
|
60
|
+
}
|
|
61
|
+
function parsePositiveDecimalAmount(value, name) {
|
|
62
|
+
validatePositiveDecimalAmount(value, name);
|
|
55
63
|
const amount = Number(value);
|
|
56
64
|
if (!Number.isFinite(amount) || amount <= 0) {
|
|
57
65
|
throw new UsageError(`${name} must be a positive amount with at most two decimal places`);
|
|
58
66
|
}
|
|
59
67
|
return amount;
|
|
60
68
|
}
|
|
69
|
+
function parsePositiveAmountPence(value, name) {
|
|
70
|
+
validatePositiveDecimalAmount(value, name);
|
|
71
|
+
const [wholePounds, fractionalPounds = ''] = value.split('.');
|
|
72
|
+
const amountPence = BigInt(wholePounds) * 100n
|
|
73
|
+
+ BigInt(fractionalPounds.padEnd(2, '0'));
|
|
74
|
+
if (amountPence > BigInt(Number.MAX_SAFE_INTEGER)) {
|
|
75
|
+
throw new UsageError(`${name} must be a positive amount with at most two decimal places`);
|
|
76
|
+
}
|
|
77
|
+
return Number(amountPence);
|
|
78
|
+
}
|
|
61
79
|
function parseGoalMonthKey(value, name) {
|
|
62
80
|
if (!/^\d{4}-(0[1-9]|1[0-2])$/.test(value)) {
|
|
63
81
|
throw new UsageError(`${name} must be a valid YYYY-MM month`);
|
|
@@ -74,11 +92,11 @@ function parseGoalPriority(value) {
|
|
|
74
92
|
}
|
|
75
93
|
return priority;
|
|
76
94
|
}
|
|
77
|
-
function
|
|
78
|
-
if (value
|
|
79
|
-
throw new UsageError(
|
|
95
|
+
function parseGoalType(value) {
|
|
96
|
+
if (!isGoalType(value)) {
|
|
97
|
+
throw new UsageError('--type must be keep or spend');
|
|
80
98
|
}
|
|
81
|
-
return value
|
|
99
|
+
return value;
|
|
82
100
|
}
|
|
83
101
|
function parseGoalName(value) {
|
|
84
102
|
const name = value.trim();
|
|
@@ -108,7 +126,7 @@ function parseResourceId(value, option) {
|
|
|
108
126
|
const codePoint = character.codePointAt(0);
|
|
109
127
|
return codePoint !== undefined && (codePoint < 32 || codePoint === 127);
|
|
110
128
|
})) {
|
|
111
|
-
const resource = option === '--
|
|
129
|
+
const resource = option === '--line-item-id' ? 'line-item' : 'category';
|
|
112
130
|
throw new UsageError(`${option} must be a valid ${resource} document ID`);
|
|
113
131
|
}
|
|
114
132
|
return id;
|
|
@@ -354,13 +372,18 @@ function parseInvestments(args, baseUrl) {
|
|
|
354
372
|
return withBaseUrl({ command: 'investments', ...(accountRef ? { accountRef } : {}) }, baseUrl);
|
|
355
373
|
}
|
|
356
374
|
function parseBudget(args, baseUrl) {
|
|
357
|
-
const
|
|
358
|
-
|
|
359
|
-
|
|
360
|
-
const
|
|
361
|
-
|
|
375
|
+
const subcommand = args[0] === 'update' || args[0] === 'move' ? args.shift() : undefined;
|
|
376
|
+
const update = subcommand === 'update';
|
|
377
|
+
const move = subcommand === 'move';
|
|
378
|
+
const commandLabel = update ? 'budget update' : move ? 'budget move' : 'budget';
|
|
379
|
+
const { values, apply } = parseNamedOptions(args, commandLabel, new Set(update
|
|
380
|
+
? ['--scope', '--period', '--input']
|
|
381
|
+
: move
|
|
382
|
+
? ['--scope', '--period', '--from-category-id', '--to-category-id', '--amount']
|
|
383
|
+
: ['--scope', '--period']));
|
|
384
|
+
if (!update && !move && apply)
|
|
362
385
|
throw new UsageError('Unknown budget option: --apply');
|
|
363
|
-
const scope = requiredOption(values, '--scope',
|
|
386
|
+
const scope = requiredOption(values, '--scope', commandLabel);
|
|
364
387
|
if (scope !== 'personal' && scope !== 'joint') {
|
|
365
388
|
throw new UsageError('--scope must be personal or joint');
|
|
366
389
|
}
|
|
@@ -369,8 +392,23 @@ function parseBudget(args, baseUrl) {
|
|
|
369
392
|
scope: scope,
|
|
370
393
|
...(period === undefined ? {} : { periodKey: parseGoalMonthKey(period, '--period') }),
|
|
371
394
|
};
|
|
372
|
-
if (!update)
|
|
395
|
+
if (!update && !move)
|
|
373
396
|
return withBaseUrl({ command: 'budget', ...common }, baseUrl);
|
|
397
|
+
if (move) {
|
|
398
|
+
const fromCategoryId = parseResourceId(requiredOption(values, '--from-category-id', commandLabel), '--from-category-id');
|
|
399
|
+
const toCategoryId = parseResourceId(requiredOption(values, '--to-category-id', commandLabel), '--to-category-id');
|
|
400
|
+
if (fromCategoryId === toCategoryId) {
|
|
401
|
+
throw new UsageError('--from-category-id and --to-category-id must differ');
|
|
402
|
+
}
|
|
403
|
+
return withBaseUrl({
|
|
404
|
+
command: 'budget-move',
|
|
405
|
+
...common,
|
|
406
|
+
fromCategoryId,
|
|
407
|
+
toCategoryId,
|
|
408
|
+
amountPence: parsePositiveAmountPence(requiredOption(values, '--amount', commandLabel), '--amount'),
|
|
409
|
+
apply,
|
|
410
|
+
}, baseUrl);
|
|
411
|
+
}
|
|
374
412
|
return withBaseUrl({
|
|
375
413
|
command: 'budget-update',
|
|
376
414
|
...common,
|
|
@@ -500,6 +538,7 @@ function parseGoals(args, baseUrl) {
|
|
|
500
538
|
let name;
|
|
501
539
|
let targetAmount;
|
|
502
540
|
let targetMonthKey;
|
|
541
|
+
let goalType;
|
|
503
542
|
let apply = false;
|
|
504
543
|
for (let index = 0; index < args.length; index += 1) {
|
|
505
544
|
const argument = args[index];
|
|
@@ -514,7 +553,8 @@ function parseGoals(args, baseUrl) {
|
|
|
514
553
|
: [argument, undefined];
|
|
515
554
|
if (option !== '--name'
|
|
516
555
|
&& option !== '--target-amount'
|
|
517
|
-
&& option !== '--target-month'
|
|
556
|
+
&& option !== '--target-month'
|
|
557
|
+
&& option !== '--type') {
|
|
518
558
|
throw new UsageError(`Unknown goals create option: ${argument}`);
|
|
519
559
|
}
|
|
520
560
|
const value = requireNonEmpty(inlineValue ?? readOptionValue(args, index, option), option);
|
|
@@ -524,19 +564,29 @@ function parseGoals(args, baseUrl) {
|
|
|
524
564
|
name = setOnce(name, parseGoalName(value), option);
|
|
525
565
|
}
|
|
526
566
|
else if (option === '--target-amount') {
|
|
527
|
-
targetAmount = setOnce(targetAmount,
|
|
567
|
+
targetAmount = setOnce(targetAmount, parsePositiveDecimalAmount(value, option), option);
|
|
528
568
|
}
|
|
529
|
-
else {
|
|
569
|
+
else if (option === '--target-month') {
|
|
530
570
|
targetMonthKey = setOnce(targetMonthKey, parseGoalMonthKey(value, option), option);
|
|
531
571
|
}
|
|
572
|
+
else {
|
|
573
|
+
goalType = setOnce(goalType, parseGoalType(value), option);
|
|
574
|
+
}
|
|
532
575
|
}
|
|
533
576
|
if (!name)
|
|
534
577
|
throw new UsageError('goals create requires --name <name>');
|
|
578
|
+
if (targetAmount === undefined) {
|
|
579
|
+
throw new UsageError('goals create requires --target-amount <amount>');
|
|
580
|
+
}
|
|
581
|
+
if (goalType === undefined) {
|
|
582
|
+
throw new UsageError('goals create requires --type <keep|spend>');
|
|
583
|
+
}
|
|
535
584
|
return withBaseUrl({
|
|
536
585
|
command: 'goals-create',
|
|
537
586
|
name,
|
|
538
|
-
|
|
587
|
+
targetAmount,
|
|
539
588
|
...(targetMonthKey === undefined ? {} : { targetMonthKey }),
|
|
589
|
+
goalType,
|
|
540
590
|
apply,
|
|
541
591
|
}, baseUrl);
|
|
542
592
|
}
|
|
@@ -545,7 +595,7 @@ function parseGoals(args, baseUrl) {
|
|
|
545
595
|
let name;
|
|
546
596
|
let targetAmount;
|
|
547
597
|
let targetMonthKey;
|
|
548
|
-
let
|
|
598
|
+
let goalType;
|
|
549
599
|
let priority;
|
|
550
600
|
let apply = false;
|
|
551
601
|
for (let index = 0; index < args.length; index += 1) {
|
|
@@ -556,13 +606,6 @@ function parseGoals(args, baseUrl) {
|
|
|
556
606
|
apply = true;
|
|
557
607
|
continue;
|
|
558
608
|
}
|
|
559
|
-
if (argument === '--clear-target-amount') {
|
|
560
|
-
if (targetAmount !== undefined) {
|
|
561
|
-
throw new UsageError('--target-amount and --clear-target-amount are mutually exclusive');
|
|
562
|
-
}
|
|
563
|
-
targetAmount = null;
|
|
564
|
-
continue;
|
|
565
|
-
}
|
|
566
609
|
if (argument === '--clear-target-month') {
|
|
567
610
|
if (targetMonthKey !== undefined) {
|
|
568
611
|
throw new UsageError('--target-month and --clear-target-month are mutually exclusive');
|
|
@@ -577,7 +620,7 @@ function parseGoals(args, baseUrl) {
|
|
|
577
620
|
&& option !== '--name'
|
|
578
621
|
&& option !== '--target-amount'
|
|
579
622
|
&& option !== '--target-month'
|
|
580
|
-
&& option !== '--
|
|
623
|
+
&& option !== '--type'
|
|
581
624
|
&& option !== '--priority') {
|
|
582
625
|
throw new UsageError(`Unknown goals update option: ${argument}`);
|
|
583
626
|
}
|
|
@@ -591,10 +634,7 @@ function parseGoals(args, baseUrl) {
|
|
|
591
634
|
name = setOnce(name, parseGoalName(value), option);
|
|
592
635
|
}
|
|
593
636
|
else if (option === '--target-amount') {
|
|
594
|
-
|
|
595
|
-
throw new UsageError('--target-amount and --clear-target-amount are mutually exclusive');
|
|
596
|
-
}
|
|
597
|
-
targetAmount = parseGoalAmount(value, option);
|
|
637
|
+
targetAmount = setOnce(targetAmount, parsePositiveDecimalAmount(value, option), option);
|
|
598
638
|
}
|
|
599
639
|
else if (option === '--target-month') {
|
|
600
640
|
if (targetMonthKey !== undefined) {
|
|
@@ -606,7 +646,7 @@ function parseGoals(args, baseUrl) {
|
|
|
606
646
|
priority = setOnce(priority, parseGoalPriority(value), option);
|
|
607
647
|
}
|
|
608
648
|
else {
|
|
609
|
-
|
|
649
|
+
goalType = setOnce(goalType, parseGoalType(value), option);
|
|
610
650
|
}
|
|
611
651
|
}
|
|
612
652
|
if (!goalId)
|
|
@@ -614,7 +654,7 @@ function parseGoals(args, baseUrl) {
|
|
|
614
654
|
if (name === undefined
|
|
615
655
|
&& targetAmount === undefined
|
|
616
656
|
&& targetMonthKey === undefined
|
|
617
|
-
&&
|
|
657
|
+
&& goalType === undefined
|
|
618
658
|
&& priority === undefined) {
|
|
619
659
|
throw new UsageError('goals update requires at least one field to update');
|
|
620
660
|
}
|
|
@@ -622,7 +662,7 @@ function parseGoals(args, baseUrl) {
|
|
|
622
662
|
&& (name !== undefined
|
|
623
663
|
|| targetAmount !== undefined
|
|
624
664
|
|| targetMonthKey !== undefined
|
|
625
|
-
||
|
|
665
|
+
|| goalType !== undefined)) {
|
|
626
666
|
throw new UsageError('--priority must be used on its own');
|
|
627
667
|
}
|
|
628
668
|
return withBaseUrl({
|
|
@@ -631,12 +671,14 @@ function parseGoals(args, baseUrl) {
|
|
|
631
671
|
...(name === undefined ? {} : { name }),
|
|
632
672
|
...(targetAmount === undefined ? {} : { targetAmount }),
|
|
633
673
|
...(targetMonthKey === undefined ? {} : { targetMonthKey }),
|
|
634
|
-
...(
|
|
674
|
+
...(goalType === undefined ? {} : { goalType }),
|
|
635
675
|
...(priority === undefined ? {} : { priority }),
|
|
636
676
|
apply,
|
|
637
677
|
}, baseUrl);
|
|
638
678
|
}
|
|
639
|
-
if (subcommand === '
|
|
679
|
+
if (subcommand === 'mark-spent'
|
|
680
|
+
|| subcommand === 'restore'
|
|
681
|
+
|| subcommand === 'delete') {
|
|
640
682
|
let goalId;
|
|
641
683
|
let apply = false;
|
|
642
684
|
for (let index = 0; index < args.length; index += 1) {
|
|
@@ -651,17 +693,22 @@ function parseGoals(args, baseUrl) {
|
|
|
651
693
|
? argument.split(/=(.*)/s, 2)
|
|
652
694
|
: [argument, undefined];
|
|
653
695
|
if (option !== '--goal-id') {
|
|
654
|
-
throw new UsageError(`Unknown goals
|
|
696
|
+
throw new UsageError(`Unknown goals ${subcommand} option: ${argument}`);
|
|
655
697
|
}
|
|
656
698
|
const value = requireNonEmpty(inlineValue ?? readOptionValue(args, index, option), option);
|
|
657
699
|
if (inlineValue === undefined)
|
|
658
700
|
index += 1;
|
|
659
701
|
goalId = setOnce(goalId, parseGoalId(value), option);
|
|
660
702
|
}
|
|
661
|
-
if (!goalId)
|
|
662
|
-
throw new UsageError(
|
|
703
|
+
if (!goalId) {
|
|
704
|
+
throw new UsageError(`goals ${subcommand} requires --goal-id <id>`);
|
|
705
|
+
}
|
|
663
706
|
return withBaseUrl({
|
|
664
|
-
command: '
|
|
707
|
+
command: subcommand === 'mark-spent'
|
|
708
|
+
? 'goals-mark-spent'
|
|
709
|
+
: subcommand === 'restore'
|
|
710
|
+
? 'goals-restore'
|
|
711
|
+
: 'goals-delete',
|
|
665
712
|
goalId,
|
|
666
713
|
apply,
|
|
667
714
|
}, baseUrl);
|
|
@@ -699,6 +746,10 @@ function helpTopic(argv) {
|
|
|
699
746
|
return 'goals-create';
|
|
700
747
|
if (subcommand === 'update')
|
|
701
748
|
return 'goals-update';
|
|
749
|
+
if (subcommand === 'mark-spent')
|
|
750
|
+
return 'goals-mark-spent';
|
|
751
|
+
if (subcommand === 'restore')
|
|
752
|
+
return 'goals-restore';
|
|
702
753
|
if (subcommand === 'delete')
|
|
703
754
|
return 'goals-delete';
|
|
704
755
|
return 'goals';
|
|
@@ -718,7 +769,11 @@ function helpTopic(argv) {
|
|
|
718
769
|
return undefined;
|
|
719
770
|
}
|
|
720
771
|
if (command === 'budget') {
|
|
721
|
-
|
|
772
|
+
if (subcommand === 'update')
|
|
773
|
+
return 'budget-update';
|
|
774
|
+
if (subcommand === 'move')
|
|
775
|
+
return 'budget-move';
|
|
776
|
+
return 'budget';
|
|
722
777
|
}
|
|
723
778
|
if (command === 'accounts'
|
|
724
779
|
|| command === 'transactions'
|
package/dist/cli.js
CHANGED
|
@@ -1,10 +1,10 @@
|
|
|
1
1
|
import fs from 'node:fs';
|
|
2
2
|
import { parseArgs, resolveBaseUrl, } from './args.js';
|
|
3
3
|
import { ICON_KEYS } from './category-metadata.js';
|
|
4
|
-
import { parseApiResponse, validateAssignmentPayload, validateBudgetUpdatePayload, } from './contracts.js';
|
|
4
|
+
import { parseApiResponse, validateAssignmentPayload, validateBudgetMovementResponse, validateBudgetUpdatePayload, } from './contracts.js';
|
|
5
5
|
import { createSystemCredentialStore, secureStorageUnavailableError, } from './credential-store.js';
|
|
6
6
|
import { ApiError, CliError, ConfigError, UsageError, } from './errors.js';
|
|
7
|
-
export const CLI_VERSION = '0.
|
|
7
|
+
export const CLI_VERSION = '0.11.0';
|
|
8
8
|
const REQUEST_TIMEOUT_MS = 60_000;
|
|
9
9
|
const API_ORIGIN_HELP_LINES = [
|
|
10
10
|
'',
|
|
@@ -32,6 +32,8 @@ export function usageText() {
|
|
|
32
32
|
' sloth-agent budget --scope personal|joint [--period YYYY-MM] [--base-url URL]',
|
|
33
33
|
' sloth-agent budget update --scope personal|joint [--period YYYY-MM]',
|
|
34
34
|
' --input budget.json [--apply] [--base-url URL]',
|
|
35
|
+
' sloth-agent budget move --scope personal|joint [--period YYYY-MM]',
|
|
36
|
+
' --from-category-id ID --to-category-id ID --amount AMOUNT [--apply]',
|
|
35
37
|
' sloth-agent categories [list] [--base-url URL]',
|
|
36
38
|
' sloth-agent categories create --name NAME --icon-key KEY --type TYPE [--apply]',
|
|
37
39
|
' sloth-agent categories rename --category-id ID --name NAME [--apply]',
|
|
@@ -43,9 +45,11 @@ export function usageText() {
|
|
|
43
45
|
' [--cursor CURSOR] [--base-url URL]',
|
|
44
46
|
' sloth-agent assign --input assignments.json [--apply] [--base-url URL]',
|
|
45
47
|
' sloth-agent goals [list] [--base-url URL]',
|
|
46
|
-
' sloth-agent goals create --name NAME
|
|
47
|
-
' [--target-month YYYY-MM] [--apply] [--base-url URL]',
|
|
48
|
+
' sloth-agent goals create --name NAME --target-amount AMOUNT',
|
|
49
|
+
' --type keep|spend [--target-month YYYY-MM] [--apply] [--base-url URL]',
|
|
48
50
|
' sloth-agent goals update --goal-id ID [fields] [--apply] [--base-url URL]',
|
|
51
|
+
' sloth-agent goals mark-spent --goal-id ID [--apply] [--base-url URL]',
|
|
52
|
+
' sloth-agent goals restore --goal-id ID [--apply] [--base-url URL]',
|
|
49
53
|
' sloth-agent goals delete --goal-id ID [--apply] [--base-url URL]',
|
|
50
54
|
' sloth-agent ask-partner --transaction-ref REF [--base-url URL]',
|
|
51
55
|
'',
|
|
@@ -455,6 +459,47 @@ export function budgetUpdateHelpText() {
|
|
|
455
459
|
' Apply mode returns the complete persisted budget response.',
|
|
456
460
|
].join('\n');
|
|
457
461
|
}
|
|
462
|
+
export function budgetMoveHelpText() {
|
|
463
|
+
return [
|
|
464
|
+
'Sloth Agent CLI — budget move',
|
|
465
|
+
'',
|
|
466
|
+
'Preview or move assigned money between categories or To Assign.',
|
|
467
|
+
'',
|
|
468
|
+
'Usage:',
|
|
469
|
+
' sloth-agent budget move --scope personal|joint [--period YYYY-MM] --from-category-id ID --to-category-id ID --amount AMOUNT [--apply] [--base-url URL]',
|
|
470
|
+
'',
|
|
471
|
+
'Required inputs:',
|
|
472
|
+
' --scope personal|joint Budget ownership scope.',
|
|
473
|
+
' --from-category-id ID Source category ID, or to-assign.',
|
|
474
|
+
' --to-category-id ID Destination category ID, or to-assign.',
|
|
475
|
+
' --amount AMOUNT Positive amount in the budget currency, with up to two decimals.',
|
|
476
|
+
' The integer-pence value must be at most 9,007,199,254,740,991.',
|
|
477
|
+
'',
|
|
478
|
+
'Optional inputs:',
|
|
479
|
+
' --period YYYY-MM Defaults to the current Sloth budget period.',
|
|
480
|
+
' --apply Send the movement. Without it, only validate and preview.',
|
|
481
|
+
' --base-url URL Override the API origin.',
|
|
482
|
+
' -h, --help Show this help.',
|
|
483
|
+
...API_ORIGIN_HELP_LINES,
|
|
484
|
+
'',
|
|
485
|
+
'Write behavior:',
|
|
486
|
+
' Without --apply, returns JSON locally without loading credentials or contacting Sloth Money.',
|
|
487
|
+
' With --apply, atomically subtracts from the source and adds to the destination.',
|
|
488
|
+
' Use the reserved ID to-assign to move money to or from To Assign.',
|
|
489
|
+
' The move changes current assigned balances and records budget movement history.',
|
|
490
|
+
' The source category or To Assign may become negative, so choose the source deliberately.',
|
|
491
|
+
' It does not change planned amounts or future budget plans.',
|
|
492
|
+
' Historical periods cannot be changed. Applying requires agent:write.',
|
|
493
|
+
'',
|
|
494
|
+
'Output:',
|
|
495
|
+
' Preview mode returns dryRun, endpoint, method, and the amountPence payload.',
|
|
496
|
+
' Apply mode returns the period, currency, movement, To Assign balance, and affected category balances.',
|
|
497
|
+
'',
|
|
498
|
+
'Examples:',
|
|
499
|
+
' sloth-agent budget move --scope personal --from-category-id activities --to-category-id groceries --amount 52.95',
|
|
500
|
+
' sloth-agent budget move --scope personal --from-category-id activities --to-category-id groceries --amount 52.95 --apply',
|
|
501
|
+
].join('\n');
|
|
502
|
+
}
|
|
458
503
|
export function transactionsHelpText() {
|
|
459
504
|
return [
|
|
460
505
|
'Sloth Agent CLI — transactions',
|
|
@@ -578,12 +623,14 @@ export function goalsHelpText() {
|
|
|
578
623
|
return [
|
|
579
624
|
'Sloth Agent CLI — goals',
|
|
580
625
|
'',
|
|
581
|
-
'List, create, update, or delete your savings goals.',
|
|
626
|
+
'List, create, update, mark spent, restore, or delete your savings goals.',
|
|
582
627
|
'',
|
|
583
628
|
'Commands:',
|
|
584
629
|
' sloth-agent goals list List goals; "sloth-agent goals" is equivalent.',
|
|
585
630
|
' sloth-agent goals create Preview or create a goal.',
|
|
586
631
|
' sloth-agent goals update Preview or update selected goal fields.',
|
|
632
|
+
' sloth-agent goals mark-spent Preview or mark a Spend goal spent.',
|
|
633
|
+
' sloth-agent goals restore Preview or restore a spent goal.',
|
|
587
634
|
' sloth-agent goals delete Preview or permanently delete a goal.',
|
|
588
635
|
'',
|
|
589
636
|
'Help:',
|
|
@@ -611,7 +658,8 @@ export function goalsListHelpText() {
|
|
|
611
658
|
'',
|
|
612
659
|
'Output:',
|
|
613
660
|
' JSON containing currency and goals. Each goal contains id, name, priority,',
|
|
614
|
-
' targetAmount, targetMonthKey,
|
|
661
|
+
' targetAmount, targetMonthKey, goalType, nullable spentAt, and',
|
|
662
|
+
' sharedWithPartner.',
|
|
615
663
|
].join('\n');
|
|
616
664
|
}
|
|
617
665
|
export function goalsCreateHelpText() {
|
|
@@ -621,11 +669,12 @@ export function goalsCreateHelpText() {
|
|
|
621
669
|
'Preview or create a goal.',
|
|
622
670
|
'',
|
|
623
671
|
'Usage:',
|
|
624
|
-
' sloth-agent goals create --name NAME [options]',
|
|
672
|
+
' sloth-agent goals create --name NAME --target-amount AMOUNT --type keep|spend [options]',
|
|
625
673
|
'',
|
|
626
674
|
'Options:',
|
|
627
675
|
' --name NAME Required. Goal name, 1 to 200 characters.',
|
|
628
|
-
' --target-amount AMOUNT
|
|
676
|
+
' --target-amount AMOUNT Required. Positive major-unit amount with up to 2 decimals.',
|
|
677
|
+
' --type keep|spend Required. Keep reserves funded money; Spend is spent later.',
|
|
629
678
|
' --target-month YYYY-MM Optional. Target calendar month.',
|
|
630
679
|
' --apply Optional. Create the goal in Sloth Money.',
|
|
631
680
|
' --base-url URL Optional. Override the API origin.',
|
|
@@ -638,8 +687,8 @@ export function goalsCreateHelpText() {
|
|
|
638
687
|
' New goals are private to the owner and appended to the existing goal order.',
|
|
639
688
|
'',
|
|
640
689
|
'Example:',
|
|
641
|
-
' sloth-agent goals create --name "Emergency fund" --target-amount 12000',
|
|
642
|
-
' sloth-agent goals create --name "
|
|
690
|
+
' sloth-agent goals create --name "Emergency fund" --target-amount 12000 --type keep',
|
|
691
|
+
' sloth-agent goals create --name "Wedding" --target-amount 22000 --type spend --target-month 2027-06 --apply',
|
|
643
692
|
'',
|
|
644
693
|
'Output:',
|
|
645
694
|
' Preview mode returns dryRun, method, endpoint, and payload.',
|
|
@@ -659,10 +708,9 @@ export function goalsUpdateHelpText() {
|
|
|
659
708
|
' --goal-id ID Required. Goal ID from goals list or create output.',
|
|
660
709
|
' --name NAME Optional. Replacement name, 1 to 200 characters.',
|
|
661
710
|
' --target-amount AMOUNT Optional. Positive amount with up to 2 decimals.',
|
|
662
|
-
' --clear-target-amount Optional. Remove the target amount.',
|
|
663
711
|
' --target-month YYYY-MM Optional. Replace the target month.',
|
|
664
712
|
' --clear-target-month Optional. Remove the target month.',
|
|
665
|
-
' --
|
|
713
|
+
' --type keep|spend Optional. Change how funded money is treated.',
|
|
666
714
|
' --priority POSITION Optional. Positive whole-number position; 1 is highest.',
|
|
667
715
|
' --apply Optional. Write the partial update.',
|
|
668
716
|
' --base-url URL Optional. Override the API origin.',
|
|
@@ -676,9 +724,8 @@ export function goalsUpdateHelpText() {
|
|
|
676
724
|
' Moving a goal shifts the intervening goals automatically.',
|
|
677
725
|
' Forecast assignments and shared progress refresh when the owner next opens',
|
|
678
726
|
' the Forecast screen.',
|
|
679
|
-
' Set and clear options
|
|
680
|
-
'
|
|
681
|
-
' Marking it active again does not restore the previous assignment.',
|
|
727
|
+
' Set and clear target-month options are mutually exclusive.',
|
|
728
|
+
' Restore a spent goal before changing its type.',
|
|
682
729
|
' Change active shared pot target amounts in the Sloth Budget app, where',
|
|
683
730
|
' account balances can be reconciled across goals in priority order.',
|
|
684
731
|
' Sharing remains app-managed. Updates to an already shared goal remain visible',
|
|
@@ -689,6 +736,7 @@ export function goalsUpdateHelpText() {
|
|
|
689
736
|
' Applying requires a write-enabled token created with Allow changes.',
|
|
690
737
|
'',
|
|
691
738
|
'Example:',
|
|
739
|
+
' sloth-agent goals update --goal-id wedding --type spend --apply',
|
|
692
740
|
' sloth-agent goals update --goal-id goal-3 --priority 2 --apply',
|
|
693
741
|
'',
|
|
694
742
|
'Output:',
|
|
@@ -696,6 +744,63 @@ export function goalsUpdateHelpText() {
|
|
|
696
744
|
' Apply mode returns the complete persisted goal and currency.',
|
|
697
745
|
].join('\n');
|
|
698
746
|
}
|
|
747
|
+
export function goalsMarkSpentHelpText() {
|
|
748
|
+
return [
|
|
749
|
+
'Sloth Agent CLI — goals mark-spent',
|
|
750
|
+
'',
|
|
751
|
+
'Preview or mark a Spend goal spent.',
|
|
752
|
+
'',
|
|
753
|
+
'Usage:',
|
|
754
|
+
' sloth-agent goals mark-spent --goal-id ID [--apply] [--base-url URL]',
|
|
755
|
+
'',
|
|
756
|
+
'Options:',
|
|
757
|
+
' --goal-id ID Required. Spend goal ID from goals list or create output.',
|
|
758
|
+
' --apply Optional. Mark the goal spent in Sloth Money.',
|
|
759
|
+
' --base-url URL Optional. Override the API origin.',
|
|
760
|
+
' -h, --help Show this help.',
|
|
761
|
+
...API_ORIGIN_HELP_LINES,
|
|
762
|
+
'',
|
|
763
|
+
'Safety and lifecycle:',
|
|
764
|
+
' Without --apply, the command previews PATCH {"isSpent":true} and does not write.',
|
|
765
|
+
' Keep goals cannot be marked spent. Change an active goal to Spend first.',
|
|
766
|
+
' A spent goal is excluded from future goal allocation until restored.',
|
|
767
|
+
'',
|
|
768
|
+
'Example:',
|
|
769
|
+
' sloth-agent goals mark-spent --goal-id wedding --apply',
|
|
770
|
+
'',
|
|
771
|
+
'Output:',
|
|
772
|
+
' Preview mode returns dryRun, method, endpoint, and payload.',
|
|
773
|
+
' Apply mode returns the complete persisted goal and currency.',
|
|
774
|
+
].join('\n');
|
|
775
|
+
}
|
|
776
|
+
export function goalsRestoreHelpText() {
|
|
777
|
+
return [
|
|
778
|
+
'Sloth Agent CLI — goals restore',
|
|
779
|
+
'',
|
|
780
|
+
'Preview or restore a spent Spend goal.',
|
|
781
|
+
'',
|
|
782
|
+
'Usage:',
|
|
783
|
+
' sloth-agent goals restore --goal-id ID [--apply] [--base-url URL]',
|
|
784
|
+
'',
|
|
785
|
+
'Options:',
|
|
786
|
+
' --goal-id ID Required. Spent goal ID from goals list output.',
|
|
787
|
+
' --apply Optional. Restore the goal in Sloth Money.',
|
|
788
|
+
' --base-url URL Optional. Override the API origin.',
|
|
789
|
+
' -h, --help Show this help.',
|
|
790
|
+
...API_ORIGIN_HELP_LINES,
|
|
791
|
+
'',
|
|
792
|
+
'Safety and lifecycle:',
|
|
793
|
+
' Without --apply, the command previews PATCH {"isSpent":false} and does not write.',
|
|
794
|
+
' Restoring clears spentAt and returns the goal to allocation at its saved priority.',
|
|
795
|
+
'',
|
|
796
|
+
'Example:',
|
|
797
|
+
' sloth-agent goals restore --goal-id wedding --apply',
|
|
798
|
+
'',
|
|
799
|
+
'Output:',
|
|
800
|
+
' Preview mode returns dryRun, method, endpoint, and payload.',
|
|
801
|
+
' Apply mode returns the complete persisted goal and currency.',
|
|
802
|
+
].join('\n');
|
|
803
|
+
}
|
|
699
804
|
export function goalsDeleteHelpText() {
|
|
700
805
|
return [
|
|
701
806
|
'Sloth Agent CLI — goals delete',
|
|
@@ -762,6 +867,7 @@ export function commandHelpText(topic) {
|
|
|
762
867
|
'accounts-remove': accountsRemoveHelpText,
|
|
763
868
|
investments: investmentsHelpText,
|
|
764
869
|
budget: budgetHelpText,
|
|
870
|
+
'budget-move': budgetMoveHelpText,
|
|
765
871
|
'budget-update': budgetUpdateHelpText,
|
|
766
872
|
categories: categoriesHelpText,
|
|
767
873
|
'categories-create': categoriesCreateHelpText,
|
|
@@ -774,6 +880,8 @@ export function commandHelpText(topic) {
|
|
|
774
880
|
'goals-list': goalsListHelpText,
|
|
775
881
|
'goals-create': goalsCreateHelpText,
|
|
776
882
|
'goals-update': goalsUpdateHelpText,
|
|
883
|
+
'goals-mark-spent': goalsMarkSpentHelpText,
|
|
884
|
+
'goals-restore': goalsRestoreHelpText,
|
|
777
885
|
'goals-delete': goalsDeleteHelpText,
|
|
778
886
|
'ask-partner': askPartnerHelpText,
|
|
779
887
|
};
|
|
@@ -1076,6 +1184,24 @@ export async function runCli(argv = process.argv.slice(2), options = {}) {
|
|
|
1076
1184
|
});
|
|
1077
1185
|
return 0;
|
|
1078
1186
|
}
|
|
1187
|
+
const budgetMovementPayload = parsed.command === 'budget-move'
|
|
1188
|
+
? {
|
|
1189
|
+
scope: parsed.scope,
|
|
1190
|
+
...(parsed.periodKey === undefined ? {} : { periodKey: parsed.periodKey }),
|
|
1191
|
+
fromCategoryId: parsed.fromCategoryId,
|
|
1192
|
+
toCategoryId: parsed.toCategoryId,
|
|
1193
|
+
amountPence: parsed.amountPence,
|
|
1194
|
+
}
|
|
1195
|
+
: undefined;
|
|
1196
|
+
if (parsed.command === 'budget-move' && !parsed.apply) {
|
|
1197
|
+
writeJson(writeStdout, {
|
|
1198
|
+
dryRun: true,
|
|
1199
|
+
endpoint: `${baseUrl}/api/agent/v1/budget-movements`,
|
|
1200
|
+
method: 'POST',
|
|
1201
|
+
payload: budgetMovementPayload,
|
|
1202
|
+
});
|
|
1203
|
+
return 0;
|
|
1204
|
+
}
|
|
1079
1205
|
const credential = await resolveCredential(environment, baseUrl, getCredentialStore);
|
|
1080
1206
|
token = credential.token;
|
|
1081
1207
|
const headers = requestHeaders(token);
|
|
@@ -1118,6 +1244,17 @@ export async function runCli(argv = process.argv.slice(2), options = {}) {
|
|
|
1118
1244
|
writeJson(writeStdout, data);
|
|
1119
1245
|
return 0;
|
|
1120
1246
|
}
|
|
1247
|
+
if (parsed.command === 'budget-move') {
|
|
1248
|
+
const response = await fetchImplementation(`${baseUrl}/api/agent/v1/budget-movements`, {
|
|
1249
|
+
method: 'POST',
|
|
1250
|
+
headers: { ...headers, 'Content-Type': 'application/json' },
|
|
1251
|
+
body: JSON.stringify(budgetMovementPayload),
|
|
1252
|
+
signal: AbortSignal.timeout(REQUEST_TIMEOUT_MS),
|
|
1253
|
+
});
|
|
1254
|
+
const data = validateBudgetMovementResponse(await parseHttpResponse(response, token), budgetMovementPayload);
|
|
1255
|
+
writeJson(writeStdout, data);
|
|
1256
|
+
return 0;
|
|
1257
|
+
}
|
|
1121
1258
|
if (parsed.command === 'categories-create'
|
|
1122
1259
|
|| parsed.command === 'categories-rename'
|
|
1123
1260
|
|| parsed.command === 'line-items-create'
|
|
@@ -1156,12 +1293,11 @@ export async function runCli(argv = process.argv.slice(2), options = {}) {
|
|
|
1156
1293
|
const endpoint = `${baseUrl}/api/agent/v1/goals`;
|
|
1157
1294
|
const payload = {
|
|
1158
1295
|
name: parsed.name,
|
|
1159
|
-
|
|
1160
|
-
? {}
|
|
1161
|
-
: { targetAmount: parsed.targetAmount }),
|
|
1296
|
+
targetAmount: parsed.targetAmount,
|
|
1162
1297
|
...(parsed.targetMonthKey === undefined
|
|
1163
1298
|
? {}
|
|
1164
1299
|
: { targetMonthKey: parsed.targetMonthKey }),
|
|
1300
|
+
goalType: parsed.goalType,
|
|
1165
1301
|
};
|
|
1166
1302
|
if (!parsed.apply) {
|
|
1167
1303
|
writeJson(writeStdout, {
|
|
@@ -1185,23 +1321,27 @@ export async function runCli(argv = process.argv.slice(2), options = {}) {
|
|
|
1185
1321
|
writeJson(writeStdout, data);
|
|
1186
1322
|
return 0;
|
|
1187
1323
|
}
|
|
1188
|
-
if (parsed.command === 'goals-update'
|
|
1324
|
+
if (parsed.command === 'goals-update'
|
|
1325
|
+
|| parsed.command === 'goals-mark-spent'
|
|
1326
|
+
|| parsed.command === 'goals-restore') {
|
|
1189
1327
|
const endpoint = `${baseUrl}/api/agent/v1/goals/${encodeURIComponent(parsed.goalId)}`;
|
|
1190
|
-
const payload =
|
|
1191
|
-
|
|
1192
|
-
|
|
1193
|
-
|
|
1194
|
-
|
|
1195
|
-
|
|
1196
|
-
|
|
1197
|
-
|
|
1198
|
-
|
|
1199
|
-
|
|
1200
|
-
|
|
1201
|
-
|
|
1202
|
-
|
|
1203
|
-
|
|
1204
|
-
|
|
1328
|
+
const payload = parsed.command === 'goals-update'
|
|
1329
|
+
? {
|
|
1330
|
+
...(parsed.name === undefined ? {} : { name: parsed.name }),
|
|
1331
|
+
...(parsed.targetAmount === undefined
|
|
1332
|
+
? {}
|
|
1333
|
+
: { targetAmount: parsed.targetAmount }),
|
|
1334
|
+
...(parsed.targetMonthKey === undefined
|
|
1335
|
+
? {}
|
|
1336
|
+
: { targetMonthKey: parsed.targetMonthKey }),
|
|
1337
|
+
...(parsed.goalType === undefined
|
|
1338
|
+
? {}
|
|
1339
|
+
: { goalType: parsed.goalType }),
|
|
1340
|
+
...(parsed.priority === undefined
|
|
1341
|
+
? {}
|
|
1342
|
+
: { priority: parsed.priority }),
|
|
1343
|
+
}
|
|
1344
|
+
: { isSpent: parsed.command === 'goals-mark-spent' };
|
|
1205
1345
|
if (!parsed.apply) {
|
|
1206
1346
|
writeJson(writeStdout, {
|
|
1207
1347
|
dryRun: true,
|
|
@@ -1220,8 +1360,8 @@ export async function runCli(argv = process.argv.slice(2), options = {}) {
|
|
|
1220
1360
|
body: JSON.stringify(payload),
|
|
1221
1361
|
signal: AbortSignal.timeout(REQUEST_TIMEOUT_MS),
|
|
1222
1362
|
});
|
|
1223
|
-
const data = parseApiResponse(
|
|
1224
|
-
writeJson(writeStdout, parsed.priority === undefined
|
|
1363
|
+
const data = parseApiResponse(parsed.command, await parseHttpResponse(response, token));
|
|
1364
|
+
writeJson(writeStdout, parsed.command !== 'goals-update' || parsed.priority === undefined
|
|
1225
1365
|
? data
|
|
1226
1366
|
: withUpdatedGoalPriority(data, parsed.priority));
|
|
1227
1367
|
return 0;
|
package/dist/contracts.js
CHANGED
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import { ApiError, UsageError, } from './errors.js';
|
|
2
2
|
import { CATEGORY_TYPES, ICON_KEYS } from './category-metadata.js';
|
|
3
|
+
import { isGoalType } from './goal-metadata.js';
|
|
3
4
|
function isObject(value) {
|
|
4
5
|
return Boolean(value) && typeof value === 'object' && !Array.isArray(value);
|
|
5
6
|
}
|
|
@@ -297,7 +298,8 @@ function isGoal(value) {
|
|
|
297
298
|
'name',
|
|
298
299
|
'targetAmount',
|
|
299
300
|
'targetMonthKey',
|
|
300
|
-
'
|
|
301
|
+
'goalType',
|
|
302
|
+
'spentAt',
|
|
301
303
|
'sharedWithPartner',
|
|
302
304
|
])
|
|
303
305
|
&& typeof value.id === 'string'
|
|
@@ -309,7 +311,10 @@ function isGoal(value) {
|
|
|
309
311
|
&& (value.targetMonthKey === null
|
|
310
312
|
|| (typeof value.targetMonthKey === 'string'
|
|
311
313
|
&& /^\d{4}-(0[1-9]|1[0-2])$/.test(value.targetMonthKey)))
|
|
312
|
-
&&
|
|
314
|
+
&& isGoalType(value.goalType)
|
|
315
|
+
&& (value.goalType === 'spend'
|
|
316
|
+
? value.spentAt === null || isIsoDateTime(value.spentAt)
|
|
317
|
+
: value.spentAt === null)
|
|
313
318
|
&& typeof value.sharedWithPartner === 'boolean');
|
|
314
319
|
}
|
|
315
320
|
function isCurrency(value) {
|
|
@@ -370,6 +375,63 @@ function isBudgetResponse(value) {
|
|
|
370
375
|
&& Array.isArray(value.categories)
|
|
371
376
|
&& value.categories.every(isBudgetCategory));
|
|
372
377
|
}
|
|
378
|
+
function isBudgetMovementResponse(value) {
|
|
379
|
+
return (isObject(value)
|
|
380
|
+
&& hasOnlyFields(value, [
|
|
381
|
+
'moved',
|
|
382
|
+
'scope',
|
|
383
|
+
'periodKey',
|
|
384
|
+
'currency',
|
|
385
|
+
'fromCategoryId',
|
|
386
|
+
'toCategoryId',
|
|
387
|
+
'amountPence',
|
|
388
|
+
'toAssignPence',
|
|
389
|
+
'categoryBalances',
|
|
390
|
+
])
|
|
391
|
+
&& value.moved === true
|
|
392
|
+
&& (value.scope === 'personal' || value.scope === 'joint')
|
|
393
|
+
&& typeof value.periodKey === 'string'
|
|
394
|
+
&& /^\d{4}-(0[1-9]|1[0-2])$/.test(value.periodKey)
|
|
395
|
+
&& isCurrency(value.currency)
|
|
396
|
+
&& typeof value.fromCategoryId === 'string'
|
|
397
|
+
&& value.fromCategoryId.trim().length > 0
|
|
398
|
+
&& typeof value.toCategoryId === 'string'
|
|
399
|
+
&& value.toCategoryId.trim().length > 0
|
|
400
|
+
&& value.fromCategoryId !== value.toCategoryId
|
|
401
|
+
&& isNonnegativeSafeInteger(value.amountPence)
|
|
402
|
+
&& value.amountPence > 0
|
|
403
|
+
&& isSafeInteger(value.toAssignPence)
|
|
404
|
+
&& Array.isArray(value.categoryBalances)
|
|
405
|
+
&& value.categoryBalances.length >= 1
|
|
406
|
+
&& value.categoryBalances.length <= 2
|
|
407
|
+
&& value.categoryBalances.every((balance) => (isObject(balance)
|
|
408
|
+
&& hasOnlyFields(balance, ['categoryId', 'assignedPence'])
|
|
409
|
+
&& typeof balance.categoryId === 'string'
|
|
410
|
+
&& balance.categoryId.trim().length > 0
|
|
411
|
+
&& isSafeInteger(balance.assignedPence))));
|
|
412
|
+
}
|
|
413
|
+
export function validateBudgetMovementResponse(value, expected) {
|
|
414
|
+
if (!isBudgetMovementResponse(value)) {
|
|
415
|
+
throw new ApiError('Invalid budget-move response from the Agent API');
|
|
416
|
+
}
|
|
417
|
+
const expectedCategoryIds = [expected.fromCategoryId, expected.toCategoryId]
|
|
418
|
+
.filter(categoryId => categoryId !== 'to-assign')
|
|
419
|
+
.sort();
|
|
420
|
+
const actualCategoryIds = value.categoryBalances
|
|
421
|
+
.map(balance => balance.categoryId)
|
|
422
|
+
.sort();
|
|
423
|
+
const matchesRequest = value.scope === expected.scope
|
|
424
|
+
&& (expected.periodKey === undefined || value.periodKey === expected.periodKey)
|
|
425
|
+
&& value.fromCategoryId === expected.fromCategoryId
|
|
426
|
+
&& value.toCategoryId === expected.toCategoryId
|
|
427
|
+
&& value.amountPence === expected.amountPence;
|
|
428
|
+
const matchesAffectedCategories = actualCategoryIds.length === expectedCategoryIds.length
|
|
429
|
+
&& actualCategoryIds.every((categoryId, index) => categoryId === expectedCategoryIds[index]);
|
|
430
|
+
if (!matchesRequest || !matchesAffectedCategories) {
|
|
431
|
+
throw new ApiError('Invalid budget-move response from the Agent API');
|
|
432
|
+
}
|
|
433
|
+
return value;
|
|
434
|
+
}
|
|
373
435
|
function isNullableNonEmptyString(value) {
|
|
374
436
|
return value === null || (typeof value === 'string'
|
|
375
437
|
&& value.length > 0
|
|
@@ -506,23 +568,25 @@ export function parseApiResponse(command, value) {
|
|
|
506
568
|
? isInvestmentsResponse(value)
|
|
507
569
|
: command === 'budget' || command === 'budget-update'
|
|
508
570
|
? isBudgetResponse(value)
|
|
509
|
-
: command === '
|
|
510
|
-
?
|
|
511
|
-
: command === 'categories
|
|
512
|
-
?
|
|
513
|
-
: command === '
|
|
514
|
-
?
|
|
515
|
-
: command === '
|
|
516
|
-
?
|
|
517
|
-
: command === '
|
|
518
|
-
?
|
|
519
|
-
: command === '
|
|
520
|
-
?
|
|
521
|
-
: command === '
|
|
522
|
-
?
|
|
523
|
-
: command === 'goals-
|
|
524
|
-
?
|
|
525
|
-
:
|
|
571
|
+
: command === 'budget-move'
|
|
572
|
+
? isBudgetMovementResponse(value)
|
|
573
|
+
: command === 'categories'
|
|
574
|
+
? isCategoryResponse(value)
|
|
575
|
+
: command === 'categories-create' || command === 'categories-rename'
|
|
576
|
+
? isCategoryMutationResponse(value)
|
|
577
|
+
: command === 'line-items-create' || command === 'line-items-rename'
|
|
578
|
+
? isLineItemMutationResponse(value)
|
|
579
|
+
: command === 'transactions'
|
|
580
|
+
? isTransactionsResponse(value)
|
|
581
|
+
: command === 'assign'
|
|
582
|
+
? isAssignmentResponse(value)
|
|
583
|
+
: command === 'ask-partner'
|
|
584
|
+
? isPartnerResponse(value)
|
|
585
|
+
: command === 'goals-list'
|
|
586
|
+
? isGoalsResponse(value)
|
|
587
|
+
: command === 'goals-delete'
|
|
588
|
+
? isGoalDeleteResponse(value)
|
|
589
|
+
: isGoalMutationResponse(value);
|
|
526
590
|
if (!valid) {
|
|
527
591
|
const label = command === 'assign' ? 'assignment' : command;
|
|
528
592
|
throw new ApiError(`Invalid ${label} response from the Agent API`);
|