@slothmoney/agent-cli 0.20.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 +9 -0
- package/README.md +53 -4
- package/dist/args.js +246 -0
- package/dist/cli.js +295 -1
- package/dist/contracts.js +104 -5
- package/package.json +1 -1
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,14 @@
|
|
|
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
|
+
|
|
3
12
|
## 0.20.0 - 2026-08-27
|
|
4
13
|
|
|
5
14
|
- Add `portfolio` views for personal, partner-shared, and combined household
|
package/README.md
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
# Sloth Agent CLI
|
|
2
2
|
|
|
3
|
-
Use your own agent to inspect personal and household 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
|
|
@@ -25,9 +25,9 @@ Create a personal access token in Sloth Money under
|
|
|
25
25
|
where the CLI runs.
|
|
26
26
|
|
|
27
27
|
New tokens are view-only. That is enough for `auth status`, `accounts`, `investments`, `portfolio`,
|
|
28
|
-
`budget`, `categories`, `transactions`, and `
|
|
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
|
|
@@ -122,6 +122,9 @@ sloth-agent goals create --help
|
|
|
122
122
|
sloth-agent goals update --help
|
|
123
123
|
sloth-agent goals mark-spent --help
|
|
124
124
|
sloth-agent goals restore --help
|
|
125
|
+
sloth-agent scenarios create --help
|
|
126
|
+
sloth-agent scenarios update --help
|
|
127
|
+
sloth-agent scenarios activate --help
|
|
125
128
|
sloth-agent ask-partner --help
|
|
126
129
|
```
|
|
127
130
|
|
|
@@ -748,6 +751,52 @@ one-based `priority`; `1` is highest. Moving one goal automatically shifts the
|
|
|
748
751
|
goals between its old and new positions. Sloth recalculates the active-scenario
|
|
749
752
|
roadmap before every applied Goal mutation and returns the updated forecast.
|
|
750
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
|
+
|
|
751
800
|
Read uncategorised contributions to the joint budget:
|
|
752
801
|
|
|
753
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('.');
|
|
@@ -864,6 +874,226 @@ function parseGoals(args, baseUrl) {
|
|
|
864
874
|
}
|
|
865
875
|
throw new UsageError(`Unknown goals command: ${subcommand}`);
|
|
866
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
|
+
}
|
|
867
1097
|
function parseRules(args, baseUrl) {
|
|
868
1098
|
const subcommand = args.shift();
|
|
869
1099
|
if (subcommand === undefined || subcommand === 'list') {
|
|
@@ -945,6 +1175,19 @@ function helpTopic(argv) {
|
|
|
945
1175
|
return 'goals-delete';
|
|
946
1176
|
return 'goals';
|
|
947
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
|
+
}
|
|
948
1191
|
if (command === 'rules') {
|
|
949
1192
|
if (subcommand === 'get')
|
|
950
1193
|
return 'rules-get';
|
|
@@ -1021,6 +1264,9 @@ export function parseArgs(argv) {
|
|
|
1021
1264
|
if (command === 'goals') {
|
|
1022
1265
|
return parseGoals(args, baseUrl);
|
|
1023
1266
|
}
|
|
1267
|
+
if (command === 'scenarios') {
|
|
1268
|
+
return parseScenarios(args, baseUrl);
|
|
1269
|
+
}
|
|
1024
1270
|
if (command === 'rules') {
|
|
1025
1271
|
return parseRules(args, baseUrl);
|
|
1026
1272
|
}
|
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 = [
|
|
@@ -68,6 +68,12 @@ export function usageText() {
|
|
|
68
68
|
' sloth-agent goals mark-spent --goal-id ID [--apply] [--base-url URL]',
|
|
69
69
|
' sloth-agent goals restore --goal-id ID [--apply] [--base-url URL]',
|
|
70
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]',
|
|
71
77
|
' sloth-agent ask-partner --transaction-ref REF [--base-url URL]',
|
|
72
78
|
'',
|
|
73
79
|
'Help:',
|
|
@@ -958,6 +964,192 @@ export function goalsDeleteHelpText() {
|
|
|
958
964
|
' Apply mode returns deleted and deletedGoalId.',
|
|
959
965
|
].join('\n');
|
|
960
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
|
+
}
|
|
961
1153
|
export function askPartnerHelpText() {
|
|
962
1154
|
return [
|
|
963
1155
|
'Sloth Agent CLI — ask-partner',
|
|
@@ -1234,6 +1426,12 @@ export function commandHelpText(topic) {
|
|
|
1234
1426
|
'goals-mark-spent': goalsMarkSpentHelpText,
|
|
1235
1427
|
'goals-restore': goalsRestoreHelpText,
|
|
1236
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,
|
|
1237
1435
|
'ask-partner': askPartnerHelpText,
|
|
1238
1436
|
};
|
|
1239
1437
|
return helpByTopic[topic]();
|
|
@@ -1556,6 +1754,77 @@ function hasFailures(value) {
|
|
|
1556
1754
|
return false;
|
|
1557
1755
|
return Array.isArray(value.failed) && value.failed.length > 0;
|
|
1558
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
|
+
}
|
|
1559
1828
|
export async function runCli(argv = process.argv.slice(2), options = {}) {
|
|
1560
1829
|
const environment = options.env ?? process.env;
|
|
1561
1830
|
const fetchImplementation = options.fetch ?? globalThis.fetch;
|
|
@@ -2001,6 +2270,31 @@ export async function runCli(argv = process.argv.slice(2), options = {}) {
|
|
|
2001
2270
|
writeJson(writeStdout, data);
|
|
2002
2271
|
return 0;
|
|
2003
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
|
+
}
|
|
2004
2298
|
if (parsed.command === 'assign') {
|
|
2005
2299
|
const payload = assignmentPayload;
|
|
2006
2300
|
const data = await applyAssignments(fetchImplementation, sleep, baseUrl, token, payload);
|
package/dist/contracts.js
CHANGED
|
@@ -1054,6 +1054,101 @@ function isGoalDeleteResponse(value) {
|
|
|
1054
1054
|
&& typeof value.deletedGoalId === 'string'
|
|
1055
1055
|
&& value.deletedGoalId.trim().length > 0);
|
|
1056
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
|
+
}
|
|
1057
1152
|
function isReceiptConfirmation(value) {
|
|
1058
1153
|
try {
|
|
1059
1154
|
validateReceiptConfirmation(value);
|
|
@@ -1150,11 +1245,15 @@ export function parseApiResponse(command, value) {
|
|
|
1150
1245
|
? isPartnerResponse(value)
|
|
1151
1246
|
: command === 'goals-list'
|
|
1152
1247
|
? isGoalsResponse(value)
|
|
1153
|
-
: command === '
|
|
1154
|
-
?
|
|
1155
|
-
: command === '
|
|
1156
|
-
?
|
|
1157
|
-
:
|
|
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);
|
|
1158
1257
|
if (!valid) {
|
|
1159
1258
|
const label = command === 'assign' ? 'assignment' : command;
|
|
1160
1259
|
throw new ApiError(`Invalid ${label} response from the Agent API`);
|