@slothmoney/agent-cli 0.19.0 → 0.21.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 +18 -0
- package/README.md +77 -9
- package/dist/args.js +272 -2
- package/dist/cli.js +341 -13
- package/dist/contracts.js +196 -42
- package/package.json +1 -1
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,23 @@
|
|
|
1
1
|
# Changelog
|
|
2
2
|
|
|
3
|
+
## 0.21.0 - 2026-08-28
|
|
4
|
+
|
|
5
|
+
- Add `scenarios` commands to list, create, update, activate, and delete the
|
|
6
|
+
month-anchored choices used by Goal forecasts.
|
|
7
|
+
- Preview every scenario change against Sloth's canonical planner with zero
|
|
8
|
+
writes; `--apply` remains the explicit write boundary.
|
|
9
|
+
- Create the common No/Yes choice directly, support recurring and one-off
|
|
10
|
+
account contributions, and return the recalculated Goal roadmap.
|
|
11
|
+
|
|
12
|
+
## 0.20.0 - 2026-08-27
|
|
13
|
+
|
|
14
|
+
- Add `portfolio` views for personal, partner-shared, and combined household
|
|
15
|
+
savings and investments.
|
|
16
|
+
- Add account-level partner visibility updates for private, balance-only, and
|
|
17
|
+
balance-and-holdings sharing.
|
|
18
|
+
- Keep portfolio results strict, machine-readable, and explicit about refresh
|
|
19
|
+
status and excluded currencies.
|
|
20
|
+
|
|
3
21
|
## 0.19.0 - 2026-08-26
|
|
4
22
|
|
|
5
23
|
- Make every Goal use one explicit personal Goal-funding account and expose
|
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, move assigned budget money, update planned amounts, categorise transactions, and configure payment notifications through the
|
|
3
|
+
Use your own agent to inspect personal and household accounts, investments, and budgets, manage goals and forecast scenarios, move assigned budget money, update planned amounts, categorise transactions, and configure payment notifications 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.21.0 -- sloth-agent --help
|
|
19
19
|
```
|
|
20
20
|
|
|
21
21
|
## Authenticate
|
|
@@ -24,10 +24,10 @@ Create a personal access token in Sloth Money under
|
|
|
24
24
|
**Settings > Developer access**, then choose the authentication method for
|
|
25
25
|
where the CLI runs.
|
|
26
26
|
|
|
27
|
-
New tokens are view-only. That is enough for `auth status`, `accounts`, `investments`,
|
|
28
|
-
`budget`, `categories`, `transactions`, and `
|
|
27
|
+
New tokens are view-only. That is enough for `auth status`, `accounts`, `investments`, `portfolio`,
|
|
28
|
+
`budget`, `categories`, `transactions`, `goals`, and `scenarios` list. Enable **Allow changes** when
|
|
29
29
|
creating the token only if the CLI must apply assignments, manage categories
|
|
30
|
-
or line items, move assigned budget money, 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 and scenarios. Token
|
|
31
31
|
permissions cannot be changed later - revoke and reissue the token instead.
|
|
32
32
|
|
|
33
33
|
### Local computer
|
|
@@ -103,6 +103,7 @@ options, output, and examples. For example:
|
|
|
103
103
|
```bash
|
|
104
104
|
sloth-agent auth login --help
|
|
105
105
|
sloth-agent accounts --help
|
|
106
|
+
sloth-agent portfolio --help
|
|
106
107
|
sloth-agent budget --help
|
|
107
108
|
sloth-agent budget status --help
|
|
108
109
|
sloth-agent budget update --help
|
|
@@ -121,6 +122,9 @@ sloth-agent goals create --help
|
|
|
121
122
|
sloth-agent goals update --help
|
|
122
123
|
sloth-agent goals mark-spent --help
|
|
123
124
|
sloth-agent goals restore --help
|
|
125
|
+
sloth-agent scenarios create --help
|
|
126
|
+
sloth-agent scenarios update --help
|
|
127
|
+
sloth-agent scenarios activate --help
|
|
124
128
|
sloth-agent ask-partner --help
|
|
125
129
|
```
|
|
126
130
|
|
|
@@ -584,7 +588,8 @@ sloth-agent accounts
|
|
|
584
588
|
The command is read-only and cache-only: it does not refresh linked banks or
|
|
585
589
|
change account data. Each result contains an opaque `accountRef`, personal or
|
|
586
590
|
joint ownership, connected or manual source, native balance/currency when
|
|
587
|
-
known, `lastBalanceUpdatedAt`, `connectionState`,
|
|
591
|
+
known, `lastBalanceUpdatedAt`, `connectionState`, `isGoalFundingAccount`, and
|
|
592
|
+
`partnerVisibility`.
|
|
588
593
|
Missing values are JSON
|
|
589
594
|
`null`; currencies are never converted or combined. Partner personal accounts
|
|
590
595
|
are excluded, while enabled shared joint accounts follow Sloth's existing
|
|
@@ -604,7 +609,7 @@ Copy the value from `sloth-agent accounts`. Account references are the public
|
|
|
604
609
|
account identifier for transaction filtering.
|
|
605
610
|
|
|
606
611
|
Account changes are previews unless `--apply` is present. Connected accounts
|
|
607
|
-
support
|
|
612
|
+
support Goal-funding eligibility and partner visibility. Manual current accounts support their
|
|
608
613
|
institution, name, currency, and ownership. Manual balance accounts also
|
|
609
614
|
support balance, Savings/Investments type, and Goal-funding eligibility.
|
|
610
615
|
Partner-owned shared accounts return an explanatory error.
|
|
@@ -618,14 +623,31 @@ sloth-agent accounts update \
|
|
|
618
623
|
--ownership individual \
|
|
619
624
|
--balance-amount 12500.75 \
|
|
620
625
|
--account-type investments \
|
|
621
|
-
--goal-funding-account false
|
|
626
|
+
--goal-funding-account false \
|
|
627
|
+
--partner-visibility holdings
|
|
622
628
|
|
|
623
629
|
sloth-agent accounts update \
|
|
624
630
|
--account-ref sloth_account_v1_... \
|
|
625
|
-
--
|
|
631
|
+
--partner-visibility balance \
|
|
626
632
|
--apply
|
|
627
633
|
```
|
|
628
634
|
|
|
635
|
+
Read the same current position from your, your partner's, or the combined
|
|
636
|
+
household perspective:
|
|
637
|
+
|
|
638
|
+
```bash
|
|
639
|
+
sloth-agent portfolio
|
|
640
|
+
sloth-agent portfolio --view partner
|
|
641
|
+
sloth-agent portfolio --view household
|
|
642
|
+
```
|
|
643
|
+
|
|
644
|
+
The command waits up to 45 seconds for eligible linked balances to refresh,
|
|
645
|
+
then returns cached data if work continues. Partner accounts appear only when
|
|
646
|
+
their owner has shared the balance or linked holdings. Sharing is for household
|
|
647
|
+
planning only. It does not change account ownership, transaction access, Goal
|
|
648
|
+
funding, or who can move money. Totals use the viewer's budget currency and
|
|
649
|
+
exclude other native currencies without converting them.
|
|
650
|
+
|
|
629
651
|
Archive an owned manual account. The account disappears from active Sloth
|
|
630
652
|
surfaces, but its underlying records are retained. Repeating an applied removal
|
|
631
653
|
is safe and returns `changed: false`.
|
|
@@ -729,6 +751,52 @@ one-based `priority`; `1` is highest. Moving one goal automatically shifts the
|
|
|
729
751
|
goals between its old and new positions. Sloth recalculates the active-scenario
|
|
730
752
|
roadmap before every applied Goal mutation and returns the updated forecast.
|
|
731
753
|
|
|
754
|
+
List the scenarios that supply assumptions to that roadmap:
|
|
755
|
+
|
|
756
|
+
```bash
|
|
757
|
+
sloth-agent scenarios
|
|
758
|
+
```
|
|
759
|
+
|
|
760
|
+
Create a monthly contribution choice. Preview is the default and performs zero
|
|
761
|
+
writes; add `--apply` after reviewing the returned scenario and recalculated
|
|
762
|
+
Goals:
|
|
763
|
+
|
|
764
|
+
```bash
|
|
765
|
+
sloth-agent scenarios create \
|
|
766
|
+
--month 2026-09 \
|
|
767
|
+
--name "Deposit £100 into the shopping pot each month?" \
|
|
768
|
+
--account-ref sloth_account_v1_... \
|
|
769
|
+
--recurring-amount 100
|
|
770
|
+
```
|
|
771
|
+
|
|
772
|
+
Creation adds No and Yes options and activates Yes. The recurring contribution
|
|
773
|
+
continues until a later active scenario changes it. A one-off amount applies
|
|
774
|
+
only in the scenario month. Scenarios alter the forecast; they do not move
|
|
775
|
+
money.
|
|
776
|
+
|
|
777
|
+
Use stable IDs from `scenarios` output to edit or select an option:
|
|
778
|
+
|
|
779
|
+
```bash
|
|
780
|
+
sloth-agent scenarios update \
|
|
781
|
+
--month 2026-09 \
|
|
782
|
+
--option-id yes \
|
|
783
|
+
--account-ref sloth_account_v1_... \
|
|
784
|
+
--recurring-amount 125 \
|
|
785
|
+
--apply
|
|
786
|
+
|
|
787
|
+
sloth-agent scenarios activate \
|
|
788
|
+
--month 2026-09 \
|
|
789
|
+
--option-id no \
|
|
790
|
+
--apply
|
|
791
|
+
|
|
792
|
+
sloth-agent scenarios delete --month 2026-09 --apply
|
|
793
|
+
```
|
|
794
|
+
|
|
795
|
+
For recurring contributions, `--recurring-amount 0` explicitly stops the
|
|
796
|
+
earlier amount. `--clear-recurring` removes this month's override, so the
|
|
797
|
+
earlier recurring amount continues. Contribution updates use the active option
|
|
798
|
+
when `--option-id` is omitted.
|
|
799
|
+
|
|
732
800
|
Read uncategorised contributions to the joint budget:
|
|
733
801
|
|
|
734
802
|
```bash
|
package/dist/args.js
CHANGED
|
@@ -67,6 +67,16 @@ function parsePositiveDecimalAmount(value, name) {
|
|
|
67
67
|
}
|
|
68
68
|
return amount;
|
|
69
69
|
}
|
|
70
|
+
function parseNonNegativeDecimalAmount(value, name) {
|
|
71
|
+
if (!/^\d+(?:\.\d{1,2})?$/.test(value)) {
|
|
72
|
+
throw new UsageError(`${name} must be zero or a positive amount with at most two decimal places`);
|
|
73
|
+
}
|
|
74
|
+
const amount = Number(value);
|
|
75
|
+
if (!Number.isFinite(amount) || amount < 0) {
|
|
76
|
+
throw new UsageError(`${name} must be zero or a positive amount with at most two decimal places`);
|
|
77
|
+
}
|
|
78
|
+
return amount;
|
|
79
|
+
}
|
|
70
80
|
function parsePositiveAmountPence(value, name) {
|
|
71
81
|
validatePositiveDecimalAmount(value, name);
|
|
72
82
|
const [wholePounds, fractionalPounds = ''] = value.split('.');
|
|
@@ -390,6 +400,7 @@ function parseAccounts(args, baseUrl) {
|
|
|
390
400
|
'--balance-amount',
|
|
391
401
|
'--account-type',
|
|
392
402
|
'--goal-funding-account',
|
|
403
|
+
'--partner-visibility',
|
|
393
404
|
]));
|
|
394
405
|
const institutionName = values.get('--institution-name');
|
|
395
406
|
const accountName = values.get('--account-name');
|
|
@@ -398,6 +409,7 @@ function parseAccounts(args, baseUrl) {
|
|
|
398
409
|
const balanceValue = values.get('--balance-amount');
|
|
399
410
|
const accountTypeValue = values.get('--account-type');
|
|
400
411
|
const sourceValue = values.get('--goal-funding-account');
|
|
412
|
+
const partnerVisibility = values.get('--partner-visibility');
|
|
401
413
|
if (currencyValue !== undefined && !/^[A-Za-z]{3}$/.test(currencyValue)) {
|
|
402
414
|
throw new UsageError('--currency must be a three-letter currency code');
|
|
403
415
|
}
|
|
@@ -414,6 +426,12 @@ function parseAccounts(args, baseUrl) {
|
|
|
414
426
|
if (sourceValue !== undefined && sourceValue !== 'true' && sourceValue !== 'false') {
|
|
415
427
|
throw new UsageError('--goal-funding-account must be true or false');
|
|
416
428
|
}
|
|
429
|
+
if (partnerVisibility !== undefined
|
|
430
|
+
&& partnerVisibility !== 'private'
|
|
431
|
+
&& partnerVisibility !== 'balance'
|
|
432
|
+
&& partnerVisibility !== 'holdings') {
|
|
433
|
+
throw new UsageError('--partner-visibility must be private, balance, or holdings');
|
|
434
|
+
}
|
|
417
435
|
const update = {
|
|
418
436
|
...(institutionName === undefined
|
|
419
437
|
? {}
|
|
@@ -430,6 +448,9 @@ function parseAccounts(args, baseUrl) {
|
|
|
430
448
|
? {}
|
|
431
449
|
: { accountType: accountTypeValue }),
|
|
432
450
|
...(sourceValue === undefined ? {} : { isGoalFundingAccount: sourceValue === 'true' }),
|
|
451
|
+
...(partnerVisibility === undefined ? {} : {
|
|
452
|
+
partnerVisibility: partnerVisibility,
|
|
453
|
+
}),
|
|
433
454
|
};
|
|
434
455
|
if (Object.keys(update).length === 0) {
|
|
435
456
|
throw new UsageError('accounts update requires at least one field to update');
|
|
@@ -471,6 +492,16 @@ function parseInvestments(args, baseUrl) {
|
|
|
471
492
|
}
|
|
472
493
|
return withBaseUrl({ command: 'investments', ...(accountRef ? { accountRef } : {}) }, baseUrl);
|
|
473
494
|
}
|
|
495
|
+
function parsePortfolio(args, baseUrl) {
|
|
496
|
+
const { values, apply } = parseNamedOptions(args, 'portfolio', new Set(['--view']));
|
|
497
|
+
if (apply)
|
|
498
|
+
throw new UsageError('portfolio does not accept --apply');
|
|
499
|
+
const view = values.get('--view') ?? 'mine';
|
|
500
|
+
if (view !== 'mine' && view !== 'partner' && view !== 'household') {
|
|
501
|
+
throw new UsageError('--view must be mine, partner, or household');
|
|
502
|
+
}
|
|
503
|
+
return withBaseUrl({ command: 'portfolio', view }, baseUrl);
|
|
504
|
+
}
|
|
474
505
|
function parseBudget(args, baseUrl) {
|
|
475
506
|
const subcommand = args[0] === 'status' || args[0] === 'update' || args[0] === 'move'
|
|
476
507
|
? args.shift()
|
|
@@ -843,6 +874,226 @@ function parseGoals(args, baseUrl) {
|
|
|
843
874
|
}
|
|
844
875
|
throw new UsageError(`Unknown goals command: ${subcommand}`);
|
|
845
876
|
}
|
|
877
|
+
function parseScenarioText(value, option, maxLength = 60) {
|
|
878
|
+
const text = value.trim();
|
|
879
|
+
if (text.length > maxLength) {
|
|
880
|
+
throw new UsageError(`${option} must be at most ${maxLength} characters`);
|
|
881
|
+
}
|
|
882
|
+
return text;
|
|
883
|
+
}
|
|
884
|
+
function parseScenarios(args, baseUrl) {
|
|
885
|
+
const subcommand = args.shift();
|
|
886
|
+
if (subcommand === undefined || subcommand === 'list') {
|
|
887
|
+
if (args.length > 0) {
|
|
888
|
+
throw new UsageError(`Unknown scenarios list option: ${args[0]}`);
|
|
889
|
+
}
|
|
890
|
+
return withBaseUrl({ command: 'scenarios-list' }, baseUrl);
|
|
891
|
+
}
|
|
892
|
+
if (subcommand === 'create') {
|
|
893
|
+
let monthKey;
|
|
894
|
+
let name;
|
|
895
|
+
let accountRef;
|
|
896
|
+
let recurringAmount;
|
|
897
|
+
let oneOffAmount;
|
|
898
|
+
let apply = false;
|
|
899
|
+
for (let index = 0; index < args.length; index += 1) {
|
|
900
|
+
const argument = args[index];
|
|
901
|
+
if (argument === '--apply') {
|
|
902
|
+
if (apply)
|
|
903
|
+
throw new UsageError('--apply may only be provided once');
|
|
904
|
+
apply = true;
|
|
905
|
+
continue;
|
|
906
|
+
}
|
|
907
|
+
const [option, inlineValue] = argument.includes('=')
|
|
908
|
+
? argument.split(/=(.*)/s, 2)
|
|
909
|
+
: [argument, undefined];
|
|
910
|
+
if (option !== '--month'
|
|
911
|
+
&& option !== '--name'
|
|
912
|
+
&& option !== '--account-ref'
|
|
913
|
+
&& option !== '--recurring-amount'
|
|
914
|
+
&& option !== '--one-off-amount') {
|
|
915
|
+
throw new UsageError(`Unknown scenarios create option: ${argument}`);
|
|
916
|
+
}
|
|
917
|
+
const value = requireNonEmpty(inlineValue ?? readOptionValue(args, index, option), option);
|
|
918
|
+
if (inlineValue === undefined)
|
|
919
|
+
index += 1;
|
|
920
|
+
if (option === '--month') {
|
|
921
|
+
monthKey = setOnce(monthKey, parseGoalMonthKey(value, option), option);
|
|
922
|
+
}
|
|
923
|
+
else if (option === '--name') {
|
|
924
|
+
name = setOnce(name, parseScenarioText(value, option), option);
|
|
925
|
+
}
|
|
926
|
+
else if (option === '--account-ref') {
|
|
927
|
+
accountRef = setOnce(accountRef, parseAccountRef(value), option);
|
|
928
|
+
}
|
|
929
|
+
else if (option === '--recurring-amount') {
|
|
930
|
+
recurringAmount = setOnce(recurringAmount, parseNonNegativeDecimalAmount(value, option), option);
|
|
931
|
+
}
|
|
932
|
+
else {
|
|
933
|
+
oneOffAmount = setOnce(oneOffAmount, parseNonNegativeDecimalAmount(value, option), option);
|
|
934
|
+
}
|
|
935
|
+
}
|
|
936
|
+
if (!monthKey)
|
|
937
|
+
throw new UsageError('scenarios create requires --month <YYYY-MM>');
|
|
938
|
+
if (!name)
|
|
939
|
+
throw new UsageError('scenarios create requires --name <name>');
|
|
940
|
+
if (!accountRef)
|
|
941
|
+
throw new UsageError('scenarios create requires --account-ref <accountRef>');
|
|
942
|
+
if (recurringAmount === undefined && oneOffAmount === undefined) {
|
|
943
|
+
throw new UsageError('scenarios create requires --recurring-amount or --one-off-amount');
|
|
944
|
+
}
|
|
945
|
+
if ((recurringAmount ?? 0) === 0 && (oneOffAmount ?? 0) === 0) {
|
|
946
|
+
throw new UsageError('scenarios create requires at least one positive contribution');
|
|
947
|
+
}
|
|
948
|
+
return withBaseUrl({
|
|
949
|
+
command: 'scenarios-create',
|
|
950
|
+
monthKey,
|
|
951
|
+
name,
|
|
952
|
+
accountRef,
|
|
953
|
+
...(recurringAmount === undefined ? {} : { recurringAmount }),
|
|
954
|
+
...(oneOffAmount === undefined
|
|
955
|
+
? recurringAmount === undefined ? {} : { oneOffAmount: 0 }
|
|
956
|
+
: { oneOffAmount }),
|
|
957
|
+
apply,
|
|
958
|
+
}, baseUrl);
|
|
959
|
+
}
|
|
960
|
+
if (subcommand === 'update') {
|
|
961
|
+
let monthKey;
|
|
962
|
+
let name;
|
|
963
|
+
let optionId;
|
|
964
|
+
let optionLabel;
|
|
965
|
+
let accountRef;
|
|
966
|
+
let recurringAmount;
|
|
967
|
+
let oneOffAmount;
|
|
968
|
+
let apply = false;
|
|
969
|
+
for (let index = 0; index < args.length; index += 1) {
|
|
970
|
+
const argument = args[index];
|
|
971
|
+
if (argument === '--apply') {
|
|
972
|
+
if (apply)
|
|
973
|
+
throw new UsageError('--apply may only be provided once');
|
|
974
|
+
apply = true;
|
|
975
|
+
continue;
|
|
976
|
+
}
|
|
977
|
+
if (argument === '--clear-recurring') {
|
|
978
|
+
if (recurringAmount !== undefined) {
|
|
979
|
+
throw new UsageError('--recurring-amount and --clear-recurring are mutually exclusive');
|
|
980
|
+
}
|
|
981
|
+
recurringAmount = null;
|
|
982
|
+
continue;
|
|
983
|
+
}
|
|
984
|
+
const [option, inlineValue] = argument.includes('=')
|
|
985
|
+
? argument.split(/=(.*)/s, 2)
|
|
986
|
+
: [argument, undefined];
|
|
987
|
+
if (option !== '--month'
|
|
988
|
+
&& option !== '--name'
|
|
989
|
+
&& option !== '--option-id'
|
|
990
|
+
&& option !== '--option-label'
|
|
991
|
+
&& option !== '--account-ref'
|
|
992
|
+
&& option !== '--recurring-amount'
|
|
993
|
+
&& option !== '--one-off-amount') {
|
|
994
|
+
throw new UsageError(`Unknown scenarios update option: ${argument}`);
|
|
995
|
+
}
|
|
996
|
+
const value = requireNonEmpty(inlineValue ?? readOptionValue(args, index, option), option);
|
|
997
|
+
if (inlineValue === undefined)
|
|
998
|
+
index += 1;
|
|
999
|
+
if (option === '--month') {
|
|
1000
|
+
monthKey = setOnce(monthKey, parseGoalMonthKey(value, option), option);
|
|
1001
|
+
}
|
|
1002
|
+
else if (option === '--name') {
|
|
1003
|
+
name = setOnce(name, parseScenarioText(value, option), option);
|
|
1004
|
+
}
|
|
1005
|
+
else if (option === '--option-id') {
|
|
1006
|
+
optionId = setOnce(optionId, parseScenarioText(value, option, 200), option);
|
|
1007
|
+
}
|
|
1008
|
+
else if (option === '--option-label') {
|
|
1009
|
+
optionLabel = setOnce(optionLabel, parseScenarioText(value, option), option);
|
|
1010
|
+
}
|
|
1011
|
+
else if (option === '--account-ref') {
|
|
1012
|
+
accountRef = setOnce(accountRef, parseAccountRef(value), option);
|
|
1013
|
+
}
|
|
1014
|
+
else if (option === '--recurring-amount') {
|
|
1015
|
+
if (recurringAmount !== undefined) {
|
|
1016
|
+
throw new UsageError('--recurring-amount and --clear-recurring are mutually exclusive');
|
|
1017
|
+
}
|
|
1018
|
+
recurringAmount = parseNonNegativeDecimalAmount(value, option);
|
|
1019
|
+
}
|
|
1020
|
+
else {
|
|
1021
|
+
oneOffAmount = setOnce(oneOffAmount, parseNonNegativeDecimalAmount(value, option), option);
|
|
1022
|
+
}
|
|
1023
|
+
}
|
|
1024
|
+
if (!monthKey)
|
|
1025
|
+
throw new UsageError('scenarios update requires --month <YYYY-MM>');
|
|
1026
|
+
if (optionLabel !== undefined && optionId === undefined) {
|
|
1027
|
+
throw new UsageError('--option-label requires --option-id');
|
|
1028
|
+
}
|
|
1029
|
+
if ((recurringAmount !== undefined || oneOffAmount !== undefined) && !accountRef) {
|
|
1030
|
+
throw new UsageError('scenario contribution changes require --account-ref');
|
|
1031
|
+
}
|
|
1032
|
+
if (accountRef !== undefined
|
|
1033
|
+
&& recurringAmount === undefined
|
|
1034
|
+
&& oneOffAmount === undefined) {
|
|
1035
|
+
throw new UsageError('--account-ref requires a contribution change');
|
|
1036
|
+
}
|
|
1037
|
+
if (name === undefined
|
|
1038
|
+
&& optionLabel === undefined
|
|
1039
|
+
&& accountRef === undefined
|
|
1040
|
+
&& recurringAmount === undefined
|
|
1041
|
+
&& oneOffAmount === undefined) {
|
|
1042
|
+
throw new UsageError('scenarios update requires at least one field to update');
|
|
1043
|
+
}
|
|
1044
|
+
return withBaseUrl({
|
|
1045
|
+
command: 'scenarios-update',
|
|
1046
|
+
monthKey,
|
|
1047
|
+
...(name === undefined ? {} : { name }),
|
|
1048
|
+
...(optionId === undefined ? {} : { optionId }),
|
|
1049
|
+
...(optionLabel === undefined ? {} : { optionLabel }),
|
|
1050
|
+
...(accountRef === undefined ? {} : { accountRef }),
|
|
1051
|
+
...(recurringAmount === undefined ? {} : { recurringAmount }),
|
|
1052
|
+
...(oneOffAmount === undefined ? {} : { oneOffAmount }),
|
|
1053
|
+
apply,
|
|
1054
|
+
}, baseUrl);
|
|
1055
|
+
}
|
|
1056
|
+
if (subcommand === 'activate' || subcommand === 'delete') {
|
|
1057
|
+
let monthKey;
|
|
1058
|
+
let optionId;
|
|
1059
|
+
let apply = false;
|
|
1060
|
+
for (let index = 0; index < args.length; index += 1) {
|
|
1061
|
+
const argument = args[index];
|
|
1062
|
+
if (argument === '--apply') {
|
|
1063
|
+
if (apply)
|
|
1064
|
+
throw new UsageError('--apply may only be provided once');
|
|
1065
|
+
apply = true;
|
|
1066
|
+
continue;
|
|
1067
|
+
}
|
|
1068
|
+
const [option, inlineValue] = argument.includes('=')
|
|
1069
|
+
? argument.split(/=(.*)/s, 2)
|
|
1070
|
+
: [argument, undefined];
|
|
1071
|
+
if (option !== '--month' && !(subcommand === 'activate' && option === '--option-id')) {
|
|
1072
|
+
throw new UsageError(`Unknown scenarios ${subcommand} option: ${argument}`);
|
|
1073
|
+
}
|
|
1074
|
+
const value = requireNonEmpty(inlineValue ?? readOptionValue(args, index, option), option);
|
|
1075
|
+
if (inlineValue === undefined)
|
|
1076
|
+
index += 1;
|
|
1077
|
+
if (option === '--month') {
|
|
1078
|
+
monthKey = setOnce(monthKey, parseGoalMonthKey(value, option), option);
|
|
1079
|
+
}
|
|
1080
|
+
else {
|
|
1081
|
+
optionId = setOnce(optionId, parseScenarioText(value, option, 200), option);
|
|
1082
|
+
}
|
|
1083
|
+
}
|
|
1084
|
+
if (!monthKey)
|
|
1085
|
+
throw new UsageError(`scenarios ${subcommand} requires --month <YYYY-MM>`);
|
|
1086
|
+
if (subcommand === 'activate') {
|
|
1087
|
+
if (!optionId)
|
|
1088
|
+
throw new UsageError('scenarios activate requires --option-id <id>');
|
|
1089
|
+
return withBaseUrl({
|
|
1090
|
+
command: 'scenarios-activate', monthKey, optionId, apply,
|
|
1091
|
+
}, baseUrl);
|
|
1092
|
+
}
|
|
1093
|
+
return withBaseUrl({ command: 'scenarios-delete', monthKey, apply }, baseUrl);
|
|
1094
|
+
}
|
|
1095
|
+
throw new UsageError(`Unknown scenarios command: ${subcommand}`);
|
|
1096
|
+
}
|
|
846
1097
|
function parseRules(args, baseUrl) {
|
|
847
1098
|
const subcommand = args.shift();
|
|
848
1099
|
if (subcommand === undefined || subcommand === 'list') {
|
|
@@ -924,6 +1175,19 @@ function helpTopic(argv) {
|
|
|
924
1175
|
return 'goals-delete';
|
|
925
1176
|
return 'goals';
|
|
926
1177
|
}
|
|
1178
|
+
if (command === 'scenarios') {
|
|
1179
|
+
if (subcommand === 'list')
|
|
1180
|
+
return 'scenarios-list';
|
|
1181
|
+
if (subcommand === 'create')
|
|
1182
|
+
return 'scenarios-create';
|
|
1183
|
+
if (subcommand === 'update')
|
|
1184
|
+
return 'scenarios-update';
|
|
1185
|
+
if (subcommand === 'activate')
|
|
1186
|
+
return 'scenarios-activate';
|
|
1187
|
+
if (subcommand === 'delete')
|
|
1188
|
+
return 'scenarios-delete';
|
|
1189
|
+
return 'scenarios';
|
|
1190
|
+
}
|
|
927
1191
|
if (command === 'rules') {
|
|
928
1192
|
if (subcommand === 'get')
|
|
929
1193
|
return 'rules-get';
|
|
@@ -979,8 +1243,8 @@ function helpTopic(argv) {
|
|
|
979
1243
|
return 'accounts-remove';
|
|
980
1244
|
return command;
|
|
981
1245
|
}
|
|
982
|
-
if (command === 'investments')
|
|
983
|
-
return
|
|
1246
|
+
if (command === 'investments' || command === 'portfolio')
|
|
1247
|
+
return command;
|
|
984
1248
|
return undefined;
|
|
985
1249
|
}
|
|
986
1250
|
export function parseArgs(argv) {
|
|
@@ -1000,6 +1264,9 @@ export function parseArgs(argv) {
|
|
|
1000
1264
|
if (command === 'goals') {
|
|
1001
1265
|
return parseGoals(args, baseUrl);
|
|
1002
1266
|
}
|
|
1267
|
+
if (command === 'scenarios') {
|
|
1268
|
+
return parseScenarios(args, baseUrl);
|
|
1269
|
+
}
|
|
1003
1270
|
if (command === 'rules') {
|
|
1004
1271
|
return parseRules(args, baseUrl);
|
|
1005
1272
|
}
|
|
@@ -1015,6 +1282,9 @@ export function parseArgs(argv) {
|
|
|
1015
1282
|
if (command === 'investments') {
|
|
1016
1283
|
return parseInvestments(args, baseUrl);
|
|
1017
1284
|
}
|
|
1285
|
+
if (command === 'portfolio') {
|
|
1286
|
+
return parsePortfolio(args, baseUrl);
|
|
1287
|
+
}
|
|
1018
1288
|
if (command === 'budget') {
|
|
1019
1289
|
return parseBudget(args, baseUrl);
|
|
1020
1290
|
}
|
package/dist/cli.js
CHANGED
|
@@ -6,7 +6,7 @@ import { ICON_KEYS } from './category-metadata.js';
|
|
|
6
6
|
import { parseApiResponse, parseAssignmentOperationResponse, toLegacyAssignmentResponse, validateAssignmentPayload, validateBudgetMovementResponse, validateBudgetUpdatePayload, validateNotificationRulePayload, validateReceiptConfirmation, } from './contracts.js';
|
|
7
7
|
import { createSystemCredentialStore, secureStorageUnavailableError, } from './credential-store.js';
|
|
8
8
|
import { ApiError, CliError, ConfigError, UsageError, } from './errors.js';
|
|
9
|
-
export const CLI_VERSION = '0.
|
|
9
|
+
export const CLI_VERSION = '0.21.0';
|
|
10
10
|
const REQUEST_TIMEOUT_MS = 60_000;
|
|
11
11
|
const MAX_CONTRACT_PDF_BYTES = 6_000_000;
|
|
12
12
|
const API_ORIGIN_HELP_LINES = [
|
|
@@ -34,6 +34,7 @@ export function usageText() {
|
|
|
34
34
|
' sloth-agent accounts update --account-ref REF [fields] [--apply]',
|
|
35
35
|
' sloth-agent accounts remove --account-ref REF [--apply]',
|
|
36
36
|
' sloth-agent investments [--account-ref REF] [--base-url URL]',
|
|
37
|
+
' sloth-agent portfolio [--view mine|partner|household] [--base-url URL]',
|
|
37
38
|
' sloth-agent budget --scope personal|joint [--period YYYY-MM] [--base-url URL]',
|
|
38
39
|
' sloth-agent budget status --scope personal|joint [--period YYYY-MM] [--base-url URL]',
|
|
39
40
|
' sloth-agent budget update --scope personal|joint [--period YYYY-MM]',
|
|
@@ -67,6 +68,12 @@ export function usageText() {
|
|
|
67
68
|
' sloth-agent goals mark-spent --goal-id ID [--apply] [--base-url URL]',
|
|
68
69
|
' sloth-agent goals restore --goal-id ID [--apply] [--base-url URL]',
|
|
69
70
|
' sloth-agent goals delete --goal-id ID [--apply] [--base-url URL]',
|
|
71
|
+
' sloth-agent scenarios [list] [--base-url URL]',
|
|
72
|
+
' sloth-agent scenarios create --month YYYY-MM --name NAME --account-ref REF',
|
|
73
|
+
' [--recurring-amount AMOUNT] [--one-off-amount AMOUNT] [--apply]',
|
|
74
|
+
' sloth-agent scenarios update --month YYYY-MM [fields] [--apply]',
|
|
75
|
+
' sloth-agent scenarios activate --month YYYY-MM --option-id ID [--apply]',
|
|
76
|
+
' sloth-agent scenarios delete --month YYYY-MM [--apply]',
|
|
70
77
|
' sloth-agent ask-partner --transaction-ref REF [--base-url URL]',
|
|
71
78
|
'',
|
|
72
79
|
'Help:',
|
|
@@ -378,11 +385,14 @@ export function accountsUpdateHelpText() {
|
|
|
378
385
|
' --balance-amount AMOUNT Balance-only account balance.',
|
|
379
386
|
' --account-type savings|investments Balance-only account type.',
|
|
380
387
|
' --goal-funding-account true|false Whether Goals may use this account.',
|
|
388
|
+
' --partner-visibility private|balance|holdings',
|
|
389
|
+
' What this account shares with your partner.',
|
|
381
390
|
'',
|
|
382
391
|
'Write behavior:',
|
|
383
392
|
' Without --apply, returns a JSON preview without credentials or a network request.',
|
|
384
393
|
' With --apply, requires agent:write on a write-enabled token and updates saved Sloth metadata.',
|
|
385
|
-
' Connected accounts support
|
|
394
|
+
' Connected accounts support --goal-funding-account and --partner-visibility.',
|
|
395
|
+
' Sharing exposes planning data only. It does not change ownership or assign the account to Goals.',
|
|
386
396
|
' Manual current accounts cannot change type, balance, or Goal-funding membership.',
|
|
387
397
|
' Partner-owned shared accounts cannot be changed.',
|
|
388
398
|
' Unknown, disconnected, or inaccessible references return Account not found.',
|
|
@@ -442,6 +452,33 @@ export function investmentsHelpText() {
|
|
|
442
452
|
' provider-native and are not converted or guaranteed to reconcile to totals.',
|
|
443
453
|
].join('\n');
|
|
444
454
|
}
|
|
455
|
+
export function portfolioHelpText() {
|
|
456
|
+
return [
|
|
457
|
+
'Sloth Agent CLI — portfolio',
|
|
458
|
+
'',
|
|
459
|
+
'Read your savings and investments from one household planning perspective.',
|
|
460
|
+
'',
|
|
461
|
+
'Usage:',
|
|
462
|
+
' sloth-agent portfolio [--view mine|partner|household] [--base-url URL]',
|
|
463
|
+
'',
|
|
464
|
+
'Options:',
|
|
465
|
+
' --view mine|partner|household Optional. Defaults to mine.',
|
|
466
|
+
' --base-url URL Optional. Override the API origin.',
|
|
467
|
+
' -h, --help Show this help.',
|
|
468
|
+
...API_ORIGIN_HELP_LINES,
|
|
469
|
+
'',
|
|
470
|
+
'Access:',
|
|
471
|
+
' This read-only command waits up to 45 seconds for eligible linked balances to refresh.',
|
|
472
|
+
' Partner shows only balances or holdings your partner explicitly shared.',
|
|
473
|
+
' Household combines your accounts with those shared balances and deduplicates joint accounts.',
|
|
474
|
+
' Shared data supports planning only. It does not assign partner accounts to Goals or change ownership.',
|
|
475
|
+
'',
|
|
476
|
+
'Output:',
|
|
477
|
+
' totals gives savings, investments, and tracked amounts in the viewer currency.',
|
|
478
|
+
' accounts includes ownerRole, freshness, sharing level, and permitted holdings.',
|
|
479
|
+
' refresh reports whether eligible linked balances refreshed or cached data was returned.',
|
|
480
|
+
].join('\n');
|
|
481
|
+
}
|
|
445
482
|
export function budgetHelpText() {
|
|
446
483
|
return [
|
|
447
484
|
'Sloth Agent CLI — budget',
|
|
@@ -927,6 +964,192 @@ export function goalsDeleteHelpText() {
|
|
|
927
964
|
' Apply mode returns deleted and deletedGoalId.',
|
|
928
965
|
].join('\n');
|
|
929
966
|
}
|
|
967
|
+
export function scenariosHelpText() {
|
|
968
|
+
return [
|
|
969
|
+
'Sloth Agent CLI - scenarios',
|
|
970
|
+
'',
|
|
971
|
+
'Manage the month-anchored choices used by the Goal forecast.',
|
|
972
|
+
'Each scenario contains options. Its active option controls the forecast calculation.',
|
|
973
|
+
'',
|
|
974
|
+
'Commands:',
|
|
975
|
+
' sloth-agent scenarios list List scenarios and their options.',
|
|
976
|
+
' sloth-agent scenarios create Create a No/Yes scenario.',
|
|
977
|
+
' sloth-agent scenarios update Change a scenario or option.',
|
|
978
|
+
' sloth-agent scenarios activate Make an option active.',
|
|
979
|
+
' sloth-agent scenarios delete Remove a scenario.',
|
|
980
|
+
'',
|
|
981
|
+
'Help:',
|
|
982
|
+
' Run sloth-agent scenarios <command> --help for command-specific details.',
|
|
983
|
+
...API_ORIGIN_HELP_LINES,
|
|
984
|
+
].join('\n');
|
|
985
|
+
}
|
|
986
|
+
export function scenariosListHelpText() {
|
|
987
|
+
return [
|
|
988
|
+
'Sloth Agent CLI - scenarios list',
|
|
989
|
+
'',
|
|
990
|
+
'List each scenario, its active option, and account contributions.',
|
|
991
|
+
'',
|
|
992
|
+
'Usage:',
|
|
993
|
+
' sloth-agent scenarios [list] [--base-url URL] [-h]',
|
|
994
|
+
'',
|
|
995
|
+
'Options:',
|
|
996
|
+
' --base-url URL Optional. Override the API origin.',
|
|
997
|
+
' -h, --help Show this help.',
|
|
998
|
+
'',
|
|
999
|
+
'Behavior:',
|
|
1000
|
+
'This command is read-only and requires agent:read.',
|
|
1001
|
+
...API_ORIGIN_HELP_LINES,
|
|
1002
|
+
'',
|
|
1003
|
+
'Output:',
|
|
1004
|
+
' JSON with currency, forecastBasis, and scenarios. Each scenario contains',
|
|
1005
|
+
' activeOptionId and options with isActive and account contributions.',
|
|
1006
|
+
].join('\n');
|
|
1007
|
+
}
|
|
1008
|
+
export function scenariosCreateHelpText() {
|
|
1009
|
+
return [
|
|
1010
|
+
'Sloth Agent CLI - scenarios create',
|
|
1011
|
+
'',
|
|
1012
|
+
'Create a month-anchored choice and recalculate the Goal roadmap.',
|
|
1013
|
+
'',
|
|
1014
|
+
'Usage:',
|
|
1015
|
+
' sloth-agent scenarios create --month YYYY-MM --name NAME --account-ref REF',
|
|
1016
|
+
' [--recurring-amount AMOUNT] [--one-off-amount AMOUNT] [--apply]',
|
|
1017
|
+
' [--base-url URL] [-h]',
|
|
1018
|
+
'',
|
|
1019
|
+
'Required:',
|
|
1020
|
+
' --month YYYY-MM Month when this scenario begins.',
|
|
1021
|
+
' --name NAME Question shown for the scenario, up to 60 characters.',
|
|
1022
|
+
' --account-ref REF Exact accountRef from sloth-agent accounts.',
|
|
1023
|
+
'',
|
|
1024
|
+
'Contribution: provide at least one:',
|
|
1025
|
+
' --recurring-amount AMOUNT Optional monthly contribution in the budget currency.',
|
|
1026
|
+
' --one-off-amount AMOUNT Optional contribution for this month.',
|
|
1027
|
+
' AMOUNT accepts zero or a positive decimal with at most two decimal places.',
|
|
1028
|
+
' At least one supplied amount must be positive.',
|
|
1029
|
+
'',
|
|
1030
|
+
'Options:',
|
|
1031
|
+
' --apply Optional. Save the scenario; otherwise preview it.',
|
|
1032
|
+
' --base-url URL Optional. Override the API origin.',
|
|
1033
|
+
' -h, --help Show this help.',
|
|
1034
|
+
'',
|
|
1035
|
+
'Provide at least one positive contribution. A recurring contribution continues',
|
|
1036
|
+
'until a later active scenario changes it. Creation adds No and Yes options and',
|
|
1037
|
+
'activates Yes. It records a forecast assumption and does not move money.',
|
|
1038
|
+
'',
|
|
1039
|
+
'Write behavior:',
|
|
1040
|
+
' Without --apply, Sloth authenticates, calculates the result, and performs zero writes.',
|
|
1041
|
+
' With --apply, Sloth saves the scenario using a write-enabled token with Allow changes.',
|
|
1042
|
+
...API_ORIGIN_HELP_LINES,
|
|
1043
|
+
'',
|
|
1044
|
+
'Output:',
|
|
1045
|
+
' JSON with changed, forecastBasis, the proposed or saved scenario, and',
|
|
1046
|
+
' recalculated Goals. Preview and apply use the same output contract.',
|
|
1047
|
+
'',
|
|
1048
|
+
'Example:',
|
|
1049
|
+
' sloth-agent scenarios create --month 2026-09 \\',
|
|
1050
|
+
' --name "Deposit £100 into the shopping pot each month?" \\',
|
|
1051
|
+
' --account-ref PASTE_THE_EXACT_ACCOUNT_REF_HERE --recurring-amount 100',
|
|
1052
|
+
].join('\n');
|
|
1053
|
+
}
|
|
1054
|
+
export function scenariosUpdateHelpText() {
|
|
1055
|
+
return [
|
|
1056
|
+
'Sloth Agent CLI - scenarios update',
|
|
1057
|
+
'',
|
|
1058
|
+
'Change a scenario, one option, or an account contribution.',
|
|
1059
|
+
'',
|
|
1060
|
+
'Usage:',
|
|
1061
|
+
' sloth-agent scenarios update --month YYYY-MM [fields] [--apply]',
|
|
1062
|
+
' [--base-url URL] [-h]',
|
|
1063
|
+
'',
|
|
1064
|
+
'Required:',
|
|
1065
|
+
' --month YYYY-MM Scenario month.',
|
|
1066
|
+
'',
|
|
1067
|
+
'Fields:',
|
|
1068
|
+
' --name NAME Rename the scenario, up to 60 characters.',
|
|
1069
|
+
' --option-id ID Select an option ID, up to 200 characters.',
|
|
1070
|
+
' --option-label LABEL Rename it, up to 60 characters; requires --option-id.',
|
|
1071
|
+
' --account-ref REF Account for contribution changes.',
|
|
1072
|
+
' --recurring-amount AMOUNT Set a monthly contribution; cannot be used',
|
|
1073
|
+
' with --clear-recurring.',
|
|
1074
|
+
' --clear-recurring Inherit the earlier recurring amount.',
|
|
1075
|
+
' --one-off-amount AMOUNT Set this month\'s one-off contribution.',
|
|
1076
|
+
' Amounts accept zero or a positive decimal with at most two decimal places.',
|
|
1077
|
+
' Contribution fields require --account-ref, and --account-ref requires one',
|
|
1078
|
+
' of those fields. Provide at least one field that changes the scenario.',
|
|
1079
|
+
'',
|
|
1080
|
+
'Options:',
|
|
1081
|
+
' --apply Optional. Save the change; otherwise preview it.',
|
|
1082
|
+
' --base-url URL Optional. Override the API origin.',
|
|
1083
|
+
' -h, --help Show this help.',
|
|
1084
|
+
'',
|
|
1085
|
+
'Contribution changes use the active option when --option-id is omitted.',
|
|
1086
|
+
'For recurring contributions, zero explicitly stops the earlier recurring amount.',
|
|
1087
|
+
'--clear-recurring removes this override so the earlier recurring amount continues.',
|
|
1088
|
+
'',
|
|
1089
|
+
'Write behavior:',
|
|
1090
|
+
' Without --apply, Sloth authenticates, calculates the result, and performs zero writes.',
|
|
1091
|
+
' With --apply, Sloth saves the change using a write-enabled token with Allow changes.',
|
|
1092
|
+
...API_ORIGIN_HELP_LINES,
|
|
1093
|
+
'',
|
|
1094
|
+
'Output:',
|
|
1095
|
+
' JSON with changed, forecastBasis, the proposed or saved scenario, and',
|
|
1096
|
+
' recalculated Goals. Preview and apply use the same output contract.',
|
|
1097
|
+
].join('\n');
|
|
1098
|
+
}
|
|
1099
|
+
export function scenariosActivateHelpText() {
|
|
1100
|
+
return [
|
|
1101
|
+
'Sloth Agent CLI - scenarios activate',
|
|
1102
|
+
'',
|
|
1103
|
+
'Select the option that controls the forecast and recalculates Goals.',
|
|
1104
|
+
'',
|
|
1105
|
+
'Usage:',
|
|
1106
|
+
' sloth-agent scenarios activate --month YYYY-MM --option-id ID [--apply]',
|
|
1107
|
+
' [--base-url URL] [-h]',
|
|
1108
|
+
'',
|
|
1109
|
+
'Required:',
|
|
1110
|
+
' --month YYYY-MM Scenario month.',
|
|
1111
|
+
' --option-id ID Exact option ID from scenarios list, up to 200 characters.',
|
|
1112
|
+
'',
|
|
1113
|
+
'Options:',
|
|
1114
|
+
' --apply Optional. Save the active option; otherwise preview it.',
|
|
1115
|
+
' --base-url URL Optional. Override the API origin.',
|
|
1116
|
+
' -h, --help Show this help.',
|
|
1117
|
+
'',
|
|
1118
|
+
'Write behavior:',
|
|
1119
|
+
'Without --apply, Sloth calculates the result and performs zero writes.',
|
|
1120
|
+
'With --apply, Sloth saves the active option using a write-enabled token with Allow changes.',
|
|
1121
|
+
...API_ORIGIN_HELP_LINES,
|
|
1122
|
+
'',
|
|
1123
|
+
'Output:',
|
|
1124
|
+
' JSON with changed, forecastBasis, the selected scenario, and recalculated Goals.',
|
|
1125
|
+
].join('\n');
|
|
1126
|
+
}
|
|
1127
|
+
export function scenariosDeleteHelpText() {
|
|
1128
|
+
return [
|
|
1129
|
+
'Sloth Agent CLI - scenarios delete',
|
|
1130
|
+
'',
|
|
1131
|
+
'Remove one scenario. Sloth recalculates Goals without it.',
|
|
1132
|
+
'',
|
|
1133
|
+
'Usage:',
|
|
1134
|
+
' sloth-agent scenarios delete --month YYYY-MM [--apply] [--base-url URL] [-h]',
|
|
1135
|
+
'',
|
|
1136
|
+
'Required:',
|
|
1137
|
+
' --month YYYY-MM Scenario month.',
|
|
1138
|
+
'',
|
|
1139
|
+
'Options:',
|
|
1140
|
+
' --apply Optional. Remove the scenario; otherwise preview deletion.',
|
|
1141
|
+
' --base-url URL Optional. Override the API origin.',
|
|
1142
|
+
' -h, --help Show this help.',
|
|
1143
|
+
'',
|
|
1144
|
+
'Write behavior:',
|
|
1145
|
+
'Without --apply, Sloth calculates the result and performs zero writes.',
|
|
1146
|
+
'With --apply, Sloth removes the scenario using a write-enabled token with Allow changes.',
|
|
1147
|
+
...API_ORIGIN_HELP_LINES,
|
|
1148
|
+
'',
|
|
1149
|
+
'Output:',
|
|
1150
|
+
' JSON with changed, forecastBasis, deletedMonthKey, and recalculated Goals.',
|
|
1151
|
+
].join('\n');
|
|
1152
|
+
}
|
|
930
1153
|
export function askPartnerHelpText() {
|
|
931
1154
|
return [
|
|
932
1155
|
'Sloth Agent CLI — ask-partner',
|
|
@@ -1173,6 +1396,7 @@ export function commandHelpText(topic) {
|
|
|
1173
1396
|
'accounts-update': accountsUpdateHelpText,
|
|
1174
1397
|
'accounts-remove': accountsRemoveHelpText,
|
|
1175
1398
|
investments: investmentsHelpText,
|
|
1399
|
+
portfolio: portfolioHelpText,
|
|
1176
1400
|
budget: budgetHelpText,
|
|
1177
1401
|
'budget-status': budgetStatusHelpText,
|
|
1178
1402
|
'budget-move': budgetMoveHelpText,
|
|
@@ -1202,6 +1426,12 @@ export function commandHelpText(topic) {
|
|
|
1202
1426
|
'goals-mark-spent': goalsMarkSpentHelpText,
|
|
1203
1427
|
'goals-restore': goalsRestoreHelpText,
|
|
1204
1428
|
'goals-delete': goalsDeleteHelpText,
|
|
1429
|
+
scenarios: scenariosHelpText,
|
|
1430
|
+
'scenarios-list': scenariosListHelpText,
|
|
1431
|
+
'scenarios-create': scenariosCreateHelpText,
|
|
1432
|
+
'scenarios-update': scenariosUpdateHelpText,
|
|
1433
|
+
'scenarios-activate': scenariosActivateHelpText,
|
|
1434
|
+
'scenarios-delete': scenariosDeleteHelpText,
|
|
1205
1435
|
'ask-partner': askPartnerHelpText,
|
|
1206
1436
|
};
|
|
1207
1437
|
return helpByTopic[topic]();
|
|
@@ -1524,6 +1754,77 @@ function hasFailures(value) {
|
|
|
1524
1754
|
return false;
|
|
1525
1755
|
return Array.isArray(value.failed) && value.failed.length > 0;
|
|
1526
1756
|
}
|
|
1757
|
+
function scenarioRequestDescriptor(parsed, baseUrl) {
|
|
1758
|
+
const previewEndpoint = `${baseUrl}/api/agent/v1/scenarios/preview`;
|
|
1759
|
+
switch (parsed.command) {
|
|
1760
|
+
case 'scenarios-create': {
|
|
1761
|
+
const scenario = {
|
|
1762
|
+
monthKey: parsed.monthKey,
|
|
1763
|
+
name: parsed.name,
|
|
1764
|
+
accountRef: parsed.accountRef,
|
|
1765
|
+
...(parsed.recurringAmount === undefined
|
|
1766
|
+
? {}
|
|
1767
|
+
: { recurringAmount: parsed.recurringAmount }),
|
|
1768
|
+
...(parsed.oneOffAmount === undefined
|
|
1769
|
+
? {}
|
|
1770
|
+
: { oneOffAmount: parsed.oneOffAmount }),
|
|
1771
|
+
};
|
|
1772
|
+
return parsed.apply
|
|
1773
|
+
? { endpoint: `${baseUrl}/api/agent/v1/scenarios`, method: 'POST', body: scenario }
|
|
1774
|
+
: {
|
|
1775
|
+
endpoint: previewEndpoint,
|
|
1776
|
+
method: 'POST',
|
|
1777
|
+
body: { action: 'create', scenario },
|
|
1778
|
+
};
|
|
1779
|
+
}
|
|
1780
|
+
case 'scenarios-update': {
|
|
1781
|
+
const updates = {
|
|
1782
|
+
...(parsed.name === undefined ? {} : { name: parsed.name }),
|
|
1783
|
+
...(parsed.optionId === undefined ? {} : { optionId: parsed.optionId }),
|
|
1784
|
+
...(parsed.optionLabel === undefined ? {} : { optionLabel: parsed.optionLabel }),
|
|
1785
|
+
...(parsed.accountRef === undefined ? {} : { accountRef: parsed.accountRef }),
|
|
1786
|
+
...(parsed.recurringAmount === undefined
|
|
1787
|
+
? {}
|
|
1788
|
+
: { recurringAmount: parsed.recurringAmount }),
|
|
1789
|
+
...(parsed.oneOffAmount === undefined ? {} : { oneOffAmount: parsed.oneOffAmount }),
|
|
1790
|
+
};
|
|
1791
|
+
return parsed.apply
|
|
1792
|
+
? {
|
|
1793
|
+
endpoint: `${baseUrl}/api/agent/v1/scenarios/${encodeURIComponent(parsed.monthKey)}`,
|
|
1794
|
+
method: 'PATCH',
|
|
1795
|
+
body: updates,
|
|
1796
|
+
}
|
|
1797
|
+
: {
|
|
1798
|
+
endpoint: previewEndpoint,
|
|
1799
|
+
method: 'POST',
|
|
1800
|
+
body: { action: 'update', monthKey: parsed.monthKey, updates },
|
|
1801
|
+
};
|
|
1802
|
+
}
|
|
1803
|
+
case 'scenarios-activate':
|
|
1804
|
+
return parsed.apply
|
|
1805
|
+
? {
|
|
1806
|
+
endpoint: `${baseUrl}/api/agent/v1/scenarios/${encodeURIComponent(parsed.monthKey)}/activate`,
|
|
1807
|
+
method: 'POST',
|
|
1808
|
+
body: { optionId: parsed.optionId },
|
|
1809
|
+
}
|
|
1810
|
+
: {
|
|
1811
|
+
endpoint: previewEndpoint,
|
|
1812
|
+
method: 'POST',
|
|
1813
|
+
body: { action: 'activate', monthKey: parsed.monthKey, optionId: parsed.optionId },
|
|
1814
|
+
};
|
|
1815
|
+
case 'scenarios-delete':
|
|
1816
|
+
return parsed.apply
|
|
1817
|
+
? {
|
|
1818
|
+
endpoint: `${baseUrl}/api/agent/v1/scenarios/${encodeURIComponent(parsed.monthKey)}`,
|
|
1819
|
+
method: 'DELETE',
|
|
1820
|
+
}
|
|
1821
|
+
: {
|
|
1822
|
+
endpoint: previewEndpoint,
|
|
1823
|
+
method: 'POST',
|
|
1824
|
+
body: { action: 'delete', monthKey: parsed.monthKey },
|
|
1825
|
+
};
|
|
1826
|
+
}
|
|
1827
|
+
}
|
|
1527
1828
|
export async function runCli(argv = process.argv.slice(2), options = {}) {
|
|
1528
1829
|
const environment = options.env ?? process.env;
|
|
1529
1830
|
const fetchImplementation = options.fetch ?? globalThis.fetch;
|
|
@@ -1969,6 +2270,31 @@ export async function runCli(argv = process.argv.slice(2), options = {}) {
|
|
|
1969
2270
|
writeJson(writeStdout, data);
|
|
1970
2271
|
return 0;
|
|
1971
2272
|
}
|
|
2273
|
+
if (parsed.command === 'scenarios-list') {
|
|
2274
|
+
const response = await fetchImplementation(`${baseUrl}/api/agent/v1/scenarios`, {
|
|
2275
|
+
method: 'GET',
|
|
2276
|
+
headers,
|
|
2277
|
+
signal: AbortSignal.timeout(REQUEST_TIMEOUT_MS),
|
|
2278
|
+
});
|
|
2279
|
+
writeJson(writeStdout, parseApiResponse('scenarios-list', await parseHttpResponse(response, token)));
|
|
2280
|
+
return 0;
|
|
2281
|
+
}
|
|
2282
|
+
if (parsed.command === 'scenarios-create'
|
|
2283
|
+
|| parsed.command === 'scenarios-update'
|
|
2284
|
+
|| parsed.command === 'scenarios-activate'
|
|
2285
|
+
|| parsed.command === 'scenarios-delete') {
|
|
2286
|
+
const request = scenarioRequestDescriptor(parsed, baseUrl);
|
|
2287
|
+
const response = await fetchImplementation(request.endpoint, {
|
|
2288
|
+
method: request.method,
|
|
2289
|
+
headers: request.body === undefined
|
|
2290
|
+
? headers
|
|
2291
|
+
: { ...headers, 'Content-Type': 'application/json' },
|
|
2292
|
+
...(request.body === undefined ? {} : { body: JSON.stringify(request.body) }),
|
|
2293
|
+
signal: AbortSignal.timeout(REQUEST_TIMEOUT_MS),
|
|
2294
|
+
});
|
|
2295
|
+
writeJson(writeStdout, parseApiResponse('scenarios-mutation', await parseHttpResponse(response, token)));
|
|
2296
|
+
return 0;
|
|
2297
|
+
}
|
|
1972
2298
|
if (parsed.command === 'assign') {
|
|
1973
2299
|
const payload = assignmentPayload;
|
|
1974
2300
|
const data = await applyAssignments(fetchImplementation, sleep, baseUrl, token, payload);
|
|
@@ -2063,19 +2389,21 @@ export async function runCli(argv = process.argv.slice(2), options = {}) {
|
|
|
2063
2389
|
}
|
|
2064
2390
|
const path = parsed.command === 'accounts'
|
|
2065
2391
|
? '/api/agent/v1/accounts'
|
|
2066
|
-
: parsed.command === '
|
|
2067
|
-
? `/api/agent/v1/
|
|
2068
|
-
|
|
2069
|
-
|
|
2070
|
-
|
|
2071
|
-
|
|
2072
|
-
:
|
|
2073
|
-
|
|
2074
|
-
|
|
2075
|
-
|
|
2392
|
+
: parsed.command === 'portfolio'
|
|
2393
|
+
? `/api/agent/v1/portfolio?${new URLSearchParams({ view: parsed.view }).toString()}`
|
|
2394
|
+
: parsed.command === 'investments'
|
|
2395
|
+
? `/api/agent/v1/investments${parsed.accountRef
|
|
2396
|
+
? `?${new URLSearchParams({ accountRef: parsed.accountRef }).toString()}`
|
|
2397
|
+
: ''}`
|
|
2398
|
+
: parsed.command === 'categories'
|
|
2399
|
+
? '/api/agent/v1/categories'
|
|
2400
|
+
: `/api/agent/v1/transactions${(() => {
|
|
2401
|
+
const query = buildTransactionsQuery(parsed.filters);
|
|
2402
|
+
return query ? `?${query}` : '';
|
|
2403
|
+
})()}`;
|
|
2076
2404
|
const response = await fetchImplementation(`${baseUrl}${path}`, {
|
|
2077
2405
|
method: 'GET',
|
|
2078
|
-
headers: parsed.command === 'transactions'
|
|
2406
|
+
headers: parsed.command === 'transactions' || parsed.command === 'portfolio'
|
|
2079
2407
|
? { ...headers, Prefer: 'wait=45' }
|
|
2080
2408
|
: headers,
|
|
2081
2409
|
signal: AbortSignal.timeout(REQUEST_TIMEOUT_MS),
|
package/dist/contracts.js
CHANGED
|
@@ -861,6 +861,7 @@ function isAccount(value) {
|
|
|
861
861
|
'lastBalanceUpdatedAt',
|
|
862
862
|
'connectionState',
|
|
863
863
|
'isGoalFundingAccount',
|
|
864
|
+
'partnerVisibility',
|
|
864
865
|
])
|
|
865
866
|
&& isAccountRef(value.accountRef)
|
|
866
867
|
&& isNullableNonEmptyString(value.accountName)
|
|
@@ -879,7 +880,10 @@ function isAccount(value) {
|
|
|
879
880
|
|| value.connectionState === 'expired'
|
|
880
881
|
|| value.connectionState === 'manual'
|
|
881
882
|
|| value.connectionState === 'unknown')
|
|
882
|
-
&& typeof value.isGoalFundingAccount === 'boolean'
|
|
883
|
+
&& typeof value.isGoalFundingAccount === 'boolean'
|
|
884
|
+
&& (value.partnerVisibility === 'private'
|
|
885
|
+
|| value.partnerVisibility === 'balance'
|
|
886
|
+
|| value.partnerVisibility === 'holdings'));
|
|
883
887
|
}
|
|
884
888
|
function isAccountsResponse(value) {
|
|
885
889
|
return (isObject(value)
|
|
@@ -945,6 +949,55 @@ function isInvestmentsResponse(value) {
|
|
|
945
949
|
&& holdings.every(isInvestmentHolding));
|
|
946
950
|
}));
|
|
947
951
|
}
|
|
952
|
+
function isPortfolioAccount(value) {
|
|
953
|
+
return isObject(value)
|
|
954
|
+
&& hasOnlyFields(value, [
|
|
955
|
+
'accountRef', 'ownerRole', 'accountName', 'institutionName', 'accountType',
|
|
956
|
+
'ownership', 'balanceAmount', 'currency', 'source', 'lastBalanceUpdatedAt',
|
|
957
|
+
'connectionState', 'partnerVisibility', 'isGoalFundingAccount', 'holdings',
|
|
958
|
+
])
|
|
959
|
+
&& isAccountRef(value.accountRef)
|
|
960
|
+
&& (value.ownerRole === 'you' || value.ownerRole === 'partner')
|
|
961
|
+
&& isNullableNonEmptyString(value.accountName)
|
|
962
|
+
&& isNullableNonEmptyString(value.institutionName)
|
|
963
|
+
&& (value.accountType === 'savings' || value.accountType === 'investments')
|
|
964
|
+
&& (value.ownership === 'personal' || value.ownership === 'joint')
|
|
965
|
+
&& (value.balanceAmount === null || (typeof value.balanceAmount === 'number' && Number.isFinite(value.balanceAmount)))
|
|
966
|
+
&& isCurrency(value.currency)
|
|
967
|
+
&& (value.source === 'connected' || value.source === 'manual')
|
|
968
|
+
&& (value.lastBalanceUpdatedAt === null || isIsoDateTime(value.lastBalanceUpdatedAt))
|
|
969
|
+
&& ['active', 'expired', 'manual', 'unknown'].includes(String(value.connectionState))
|
|
970
|
+
&& ['private', 'balance', 'holdings'].includes(String(value.partnerVisibility))
|
|
971
|
+
&& (value.isGoalFundingAccount === null || typeof value.isGoalFundingAccount === 'boolean')
|
|
972
|
+
&& Array.isArray(value.holdings)
|
|
973
|
+
&& value.holdings.every(isInvestmentHolding);
|
|
974
|
+
}
|
|
975
|
+
function isPortfolioResponse(value) {
|
|
976
|
+
if (!isObject(value) || !hasOnlyFields(value, [
|
|
977
|
+
'asOf', 'currency', 'view', 'hasPartner', 'totals',
|
|
978
|
+
'excludedCurrencyAccountCount', 'accounts', 'refresh',
|
|
979
|
+
]))
|
|
980
|
+
return false;
|
|
981
|
+
const totals = value.totals;
|
|
982
|
+
const refresh = value.refresh;
|
|
983
|
+
return isIsoDateTime(value.asOf)
|
|
984
|
+
&& isCurrency(value.currency)
|
|
985
|
+
&& ['mine', 'partner', 'household'].includes(String(value.view))
|
|
986
|
+
&& typeof value.hasPartner === 'boolean'
|
|
987
|
+
&& isObject(totals)
|
|
988
|
+
&& hasOnlyFields(totals, ['savingsAmount', 'investmentsAmount', 'trackedAmount'])
|
|
989
|
+
&& ['savingsAmount', 'investmentsAmount', 'trackedAmount'].every(field => (typeof totals[field] === 'number' && Number.isFinite(totals[field])))
|
|
990
|
+
&& Number.isSafeInteger(value.excludedCurrencyAccountCount)
|
|
991
|
+
&& Number(value.excludedCurrencyAccountCount) >= 0
|
|
992
|
+
&& Array.isArray(value.accounts)
|
|
993
|
+
&& value.accounts.every(isPortfolioAccount)
|
|
994
|
+
&& isObject(refresh)
|
|
995
|
+
&& hasOnlyFields(refresh, ['status', 'reason', 'utcDate'])
|
|
996
|
+
&& ['skipped', 'completed', 'in_progress', 'partial', 'failed'].includes(String(refresh.status))
|
|
997
|
+
&& typeof refresh.reason === 'string'
|
|
998
|
+
&& refresh.reason.length > 0
|
|
999
|
+
&& isIsoDate(refresh.utcDate);
|
|
1000
|
+
}
|
|
948
1001
|
function isForecastBasis(value) {
|
|
949
1002
|
return isObject(value)
|
|
950
1003
|
&& hasOnlyFields(value, [
|
|
@@ -1001,6 +1054,101 @@ function isGoalDeleteResponse(value) {
|
|
|
1001
1054
|
&& typeof value.deletedGoalId === 'string'
|
|
1002
1055
|
&& value.deletedGoalId.trim().length > 0);
|
|
1003
1056
|
}
|
|
1057
|
+
function isScenarioAmount(value) {
|
|
1058
|
+
return typeof value === 'number'
|
|
1059
|
+
&& Number.isFinite(value)
|
|
1060
|
+
&& value >= 0
|
|
1061
|
+
&& Math.abs(value * 100 - Math.round(value * 100)) < Number.EPSILON * 100;
|
|
1062
|
+
}
|
|
1063
|
+
function isScenarioContribution(value) {
|
|
1064
|
+
return isObject(value)
|
|
1065
|
+
&& hasOnlyFields(value, [
|
|
1066
|
+
'accountRef',
|
|
1067
|
+
'accountLabel',
|
|
1068
|
+
'accountType',
|
|
1069
|
+
'recurringAmount',
|
|
1070
|
+
'oneOffAmount',
|
|
1071
|
+
])
|
|
1072
|
+
&& isAccountRef(value.accountRef)
|
|
1073
|
+
&& typeof value.accountLabel === 'string'
|
|
1074
|
+
&& value.accountLabel.trim().length > 0
|
|
1075
|
+
&& (value.accountType === 'current'
|
|
1076
|
+
|| value.accountType === 'savings'
|
|
1077
|
+
|| value.accountType === 'investments')
|
|
1078
|
+
&& (value.recurringAmount === null
|
|
1079
|
+
|| isScenarioAmount(value.recurringAmount))
|
|
1080
|
+
&& isScenarioAmount(value.oneOffAmount);
|
|
1081
|
+
}
|
|
1082
|
+
function isScenarioOption(value) {
|
|
1083
|
+
return isObject(value)
|
|
1084
|
+
&& hasOnlyFields(value, ['id', 'label', 'isActive', 'contributions'])
|
|
1085
|
+
&& typeof value.id === 'string'
|
|
1086
|
+
&& value.id.trim().length > 0
|
|
1087
|
+
&& value.id === value.id.trim()
|
|
1088
|
+
&& value.id.length <= 200
|
|
1089
|
+
&& typeof value.label === 'string'
|
|
1090
|
+
&& value.label.trim().length > 0
|
|
1091
|
+
&& value.label === value.label.trim()
|
|
1092
|
+
&& value.label.length <= 60
|
|
1093
|
+
&& typeof value.isActive === 'boolean'
|
|
1094
|
+
&& Array.isArray(value.contributions)
|
|
1095
|
+
&& value.contributions.every(isScenarioContribution);
|
|
1096
|
+
}
|
|
1097
|
+
function hasConsistentScenarioOptions(options, activeOptionId) {
|
|
1098
|
+
const parsedOptions = options.filter(isObject);
|
|
1099
|
+
if (parsedOptions.length !== options.length)
|
|
1100
|
+
return false;
|
|
1101
|
+
const optionIds = parsedOptions.map(option => String(option.id));
|
|
1102
|
+
const activeOptions = parsedOptions.filter(option => option.isActive === true);
|
|
1103
|
+
return new Set(optionIds).size === optionIds.length
|
|
1104
|
+
&& activeOptions.length === 1
|
|
1105
|
+
&& activeOptions[0]?.id === activeOptionId;
|
|
1106
|
+
}
|
|
1107
|
+
function isScenario(value) {
|
|
1108
|
+
return isObject(value)
|
|
1109
|
+
&& hasOnlyFields(value, ['monthKey', 'name', 'activeOptionId', 'options'])
|
|
1110
|
+
&& isMonthKeyOrNull(value.monthKey)
|
|
1111
|
+
&& value.monthKey !== null
|
|
1112
|
+
&& (value.name === null
|
|
1113
|
+
|| (typeof value.name === 'string'
|
|
1114
|
+
&& value.name.trim().length > 0
|
|
1115
|
+
&& value.name === value.name.trim()
|
|
1116
|
+
&& value.name.length <= 60))
|
|
1117
|
+
&& typeof value.activeOptionId === 'string'
|
|
1118
|
+
&& value.activeOptionId.trim().length > 0
|
|
1119
|
+
&& value.activeOptionId === value.activeOptionId.trim()
|
|
1120
|
+
&& value.activeOptionId.length <= 200
|
|
1121
|
+
&& Array.isArray(value.options)
|
|
1122
|
+
&& value.options.length > 0
|
|
1123
|
+
&& value.options.every(isScenarioOption)
|
|
1124
|
+
&& hasConsistentScenarioOptions(value.options, value.activeOptionId);
|
|
1125
|
+
}
|
|
1126
|
+
function isScenariosResponse(value) {
|
|
1127
|
+
return isObject(value)
|
|
1128
|
+
&& hasOnlyFields(value, ['currency', 'forecastBasis', 'scenarios'])
|
|
1129
|
+
&& isCurrency(value.currency)
|
|
1130
|
+
&& isForecastBasis(value.forecastBasis)
|
|
1131
|
+
&& Array.isArray(value.scenarios)
|
|
1132
|
+
&& value.scenarios.every(isScenario);
|
|
1133
|
+
}
|
|
1134
|
+
function isScenarioMutationResponse(value) {
|
|
1135
|
+
return isObject(value)
|
|
1136
|
+
&& hasOnlyFields(value, [
|
|
1137
|
+
'currency',
|
|
1138
|
+
'changed',
|
|
1139
|
+
'forecastBasis',
|
|
1140
|
+
'scenario',
|
|
1141
|
+
'deletedMonthKey',
|
|
1142
|
+
'goals',
|
|
1143
|
+
])
|
|
1144
|
+
&& isCurrency(value.currency)
|
|
1145
|
+
&& typeof value.changed === 'boolean'
|
|
1146
|
+
&& isForecastBasis(value.forecastBasis)
|
|
1147
|
+
&& (value.scenario === null || isScenario(value.scenario))
|
|
1148
|
+
&& isMonthKeyOrNull(value.deletedMonthKey)
|
|
1149
|
+
&& Array.isArray(value.goals)
|
|
1150
|
+
&& value.goals.every(isGoal);
|
|
1151
|
+
}
|
|
1004
1152
|
function isReceiptConfirmation(value) {
|
|
1005
1153
|
try {
|
|
1006
1154
|
validateReceiptConfirmation(value);
|
|
@@ -1059,47 +1207,53 @@ export function parseApiResponse(command, value) {
|
|
|
1059
1207
|
? isAccountRemovalResponse(value)
|
|
1060
1208
|
: command === 'investments'
|
|
1061
1209
|
? isInvestmentsResponse(value)
|
|
1062
|
-
: command === '
|
|
1063
|
-
?
|
|
1064
|
-
: command === 'budget-
|
|
1065
|
-
?
|
|
1066
|
-
: command === 'budget-
|
|
1067
|
-
?
|
|
1068
|
-
: command === '
|
|
1069
|
-
?
|
|
1070
|
-
: command === 'categories
|
|
1071
|
-
?
|
|
1072
|
-
: command === '
|
|
1073
|
-
?
|
|
1074
|
-
: command === '
|
|
1075
|
-
?
|
|
1076
|
-
: command === '
|
|
1077
|
-
?
|
|
1078
|
-
: command === 'rules-
|
|
1079
|
-
?
|
|
1080
|
-
: command === 'rules-
|
|
1081
|
-
?
|
|
1082
|
-
: command === 'rules-
|
|
1083
|
-
?
|
|
1084
|
-
: command === '
|
|
1085
|
-
?
|
|
1086
|
-
: command === 'receipts-
|
|
1087
|
-
?
|
|
1088
|
-
: command === 'receipts-
|
|
1089
|
-
?
|
|
1090
|
-
: command === 'receipts-
|
|
1091
|
-
?
|
|
1092
|
-
: command === '
|
|
1093
|
-
?
|
|
1094
|
-
: command === '
|
|
1095
|
-
?
|
|
1096
|
-
: command === '
|
|
1097
|
-
?
|
|
1098
|
-
: command === 'goals-
|
|
1099
|
-
?
|
|
1100
|
-
: command === '
|
|
1101
|
-
?
|
|
1102
|
-
:
|
|
1210
|
+
: command === 'portfolio'
|
|
1211
|
+
? isPortfolioResponse(value)
|
|
1212
|
+
: command === 'budget' || command === 'budget-update'
|
|
1213
|
+
? isBudgetResponse(value)
|
|
1214
|
+
: command === 'budget-status'
|
|
1215
|
+
? isBudgetActivityStatusResponse(value)
|
|
1216
|
+
: command === 'budget-move'
|
|
1217
|
+
? isBudgetMovementResponse(value)
|
|
1218
|
+
: command === 'categories'
|
|
1219
|
+
? isCategoryResponse(value)
|
|
1220
|
+
: command === 'categories-create' || command === 'categories-rename'
|
|
1221
|
+
? isCategoryMutationResponse(value)
|
|
1222
|
+
: command === 'line-items-create' || command === 'line-items-rename'
|
|
1223
|
+
? isLineItemMutationResponse(value)
|
|
1224
|
+
: command === 'transactions'
|
|
1225
|
+
? isTransactionsResponse(value)
|
|
1226
|
+
: command === 'rules-list'
|
|
1227
|
+
? isNotificationRuleListResponse(value)
|
|
1228
|
+
: command === 'rules-get' || command === 'rules-set'
|
|
1229
|
+
? isNotificationRuleResponse(value)
|
|
1230
|
+
: command === 'rules-delete'
|
|
1231
|
+
? isNotificationRuleDeleteResponse(value)
|
|
1232
|
+
: command === 'rules-scan-contract'
|
|
1233
|
+
? isRenewalExtractionResponse(value)
|
|
1234
|
+
: command === 'receipts-extract'
|
|
1235
|
+
? isReceiptExtractResponse(value)
|
|
1236
|
+
: command === 'receipts-get'
|
|
1237
|
+
? isReceiptLookupResponse(value)
|
|
1238
|
+
: command === 'receipts-attach'
|
|
1239
|
+
? isReceiptMutationResponse(value)
|
|
1240
|
+
: command === 'receipts-remove'
|
|
1241
|
+
? isReceiptDeleteResponse(value)
|
|
1242
|
+
: command === 'assign'
|
|
1243
|
+
? isAssignmentResponse(value)
|
|
1244
|
+
: command === 'ask-partner'
|
|
1245
|
+
? isPartnerResponse(value)
|
|
1246
|
+
: command === 'goals-list'
|
|
1247
|
+
? isGoalsResponse(value)
|
|
1248
|
+
: command === 'scenarios-list'
|
|
1249
|
+
? isScenariosResponse(value)
|
|
1250
|
+
: command === 'scenarios-mutation'
|
|
1251
|
+
? isScenarioMutationResponse(value)
|
|
1252
|
+
: command === 'goals-preview'
|
|
1253
|
+
? isGoalPreviewResponse(value)
|
|
1254
|
+
: command === 'goals-delete'
|
|
1255
|
+
? isGoalDeleteResponse(value)
|
|
1256
|
+
: isGoalMutationResponse(value);
|
|
1103
1257
|
if (!valid) {
|
|
1104
1258
|
const label = command === 'assign' ? 'assignment' : command;
|
|
1105
1259
|
throw new ApiError(`Invalid ${label} response from the Agent API`);
|