@slothmoney/agent-cli 0.18.1 → 0.20.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 +22 -0
- package/README.md +39 -14
- package/dist/args.js +54 -15
- package/dist/cli.js +89 -71
- package/dist/contracts.js +160 -51
- package/dist/errors.js +3 -1
- package/package.json +1 -1
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,27 @@
|
|
|
1
1
|
# Changelog
|
|
2
2
|
|
|
3
|
+
## 0.20.0 - 2026-08-27
|
|
4
|
+
|
|
5
|
+
- Add `portfolio` views for personal, partner-shared, and combined household
|
|
6
|
+
savings and investments.
|
|
7
|
+
- Add account-level partner visibility updates for private, balance-only, and
|
|
8
|
+
balance-and-holdings sharing.
|
|
9
|
+
- Keep portfolio results strict, machine-readable, and explicit about refresh
|
|
10
|
+
status and excluded currencies.
|
|
11
|
+
|
|
12
|
+
## 0.19.0 - 2026-08-26
|
|
13
|
+
|
|
14
|
+
- Make every Goal use one explicit personal Goal-funding account and expose
|
|
15
|
+
the calculated `forecastMonthKey`, effective priority, account details, and
|
|
16
|
+
`forecastBasis` in Goal results.
|
|
17
|
+
- Make `goals create` preview against Sloth's active scenario on the server;
|
|
18
|
+
preview performs no writes and `--apply` remains the write boundary.
|
|
19
|
+
- Add `--account-ref` and `--priority` to Goal creation, allow account
|
|
20
|
+
reassignment during Goal updates, and rename account eligibility to
|
|
21
|
+
`isGoalFundingAccount` / `--goal-funding-account`.
|
|
22
|
+
- Surface corrective API error codes without adding affordability judgments or
|
|
23
|
+
desired-date comparisons to the CLI.
|
|
24
|
+
|
|
3
25
|
## 0.18.1 - 2026-08-25
|
|
4
26
|
|
|
5
27
|
- Require the deployed historical-activity response for `budget status` and
|
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, 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
|
|
@@ -24,7 +24,7 @@ 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`,
|
|
27
|
+
New tokens are view-only. That is enough for `auth status`, `accounts`, `investments`, `portfolio`,
|
|
28
28
|
`budget`, `categories`, `transactions`, and `goals` list. Enable **Allow changes** when
|
|
29
29
|
creating the token only if the CLI must apply assignments, manage categories
|
|
30
30
|
or line items, move assigned budget money, update planned budgets, manage accounts, ask a partner for an explanation, or manage goals. Token
|
|
@@ -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
|
|
@@ -584,7 +585,8 @@ sloth-agent accounts
|
|
|
584
585
|
The command is read-only and cache-only: it does not refresh linked banks or
|
|
585
586
|
change account data. Each result contains an opaque `accountRef`, personal or
|
|
586
587
|
joint ownership, connected or manual source, native balance/currency when
|
|
587
|
-
known, `lastBalanceUpdatedAt`, `connectionState`, and
|
|
588
|
+
known, `lastBalanceUpdatedAt`, `connectionState`, `isGoalFundingAccount`, and
|
|
589
|
+
`partnerVisibility`.
|
|
588
590
|
Missing values are JSON
|
|
589
591
|
`null`; currencies are never converted or combined. Partner personal accounts
|
|
590
592
|
are excluded, while enabled shared joint accounts follow Sloth's existing
|
|
@@ -604,9 +606,9 @@ Copy the value from `sloth-agent accounts`. Account references are the public
|
|
|
604
606
|
account identifier for transaction filtering.
|
|
605
607
|
|
|
606
608
|
Account changes are previews unless `--apply` is present. Connected accounts
|
|
607
|
-
support
|
|
609
|
+
support Goal-funding eligibility and partner visibility. Manual current accounts support their
|
|
608
610
|
institution, name, currency, and ownership. Manual balance accounts also
|
|
609
|
-
support balance, Savings/Investments type, and
|
|
611
|
+
support balance, Savings/Investments type, and Goal-funding eligibility.
|
|
610
612
|
Partner-owned shared accounts return an explanatory error.
|
|
611
613
|
|
|
612
614
|
```bash
|
|
@@ -618,14 +620,31 @@ sloth-agent accounts update \
|
|
|
618
620
|
--ownership individual \
|
|
619
621
|
--balance-amount 12500.75 \
|
|
620
622
|
--account-type investments \
|
|
621
|
-
--goal-
|
|
623
|
+
--goal-funding-account false \
|
|
624
|
+
--partner-visibility holdings
|
|
622
625
|
|
|
623
626
|
sloth-agent accounts update \
|
|
624
627
|
--account-ref sloth_account_v1_... \
|
|
625
|
-
--
|
|
628
|
+
--partner-visibility balance \
|
|
626
629
|
--apply
|
|
627
630
|
```
|
|
628
631
|
|
|
632
|
+
Read the same current position from your, your partner's, or the combined
|
|
633
|
+
household perspective:
|
|
634
|
+
|
|
635
|
+
```bash
|
|
636
|
+
sloth-agent portfolio
|
|
637
|
+
sloth-agent portfolio --view partner
|
|
638
|
+
sloth-agent portfolio --view household
|
|
639
|
+
```
|
|
640
|
+
|
|
641
|
+
The command waits up to 45 seconds for eligible linked balances to refresh,
|
|
642
|
+
then returns cached data if work continues. Partner accounts appear only when
|
|
643
|
+
their owner has shared the balance or linked holdings. Sharing is for household
|
|
644
|
+
planning only. It does not change account ownership, transaction access, Goal
|
|
645
|
+
funding, or who can move money. Totals use the viewer's budget currency and
|
|
646
|
+
exclude other native currencies without converting them.
|
|
647
|
+
|
|
629
648
|
Archive an owned manual account. The account disappears from active Sloth
|
|
630
649
|
surfaces, but its underlying records are retained. Repeating an applied removal
|
|
631
650
|
is safe and returns `changed: false`.
|
|
@@ -664,16 +683,24 @@ Goal writes are previews unless `--apply` is present:
|
|
|
664
683
|
sloth-agent goals create \
|
|
665
684
|
--name "Emergency fund" \
|
|
666
685
|
--target-amount 12000 \
|
|
667
|
-
--type keep
|
|
686
|
+
--type keep \
|
|
687
|
+
--account-ref sloth_account_v1_...
|
|
668
688
|
|
|
669
689
|
sloth-agent goals create \
|
|
670
690
|
--name "Wedding" \
|
|
671
691
|
--target-amount 22000 \
|
|
672
692
|
--target-month 2027-06 \
|
|
673
693
|
--type spend \
|
|
694
|
+
--account-ref sloth_account_v1_... \
|
|
674
695
|
--apply
|
|
675
696
|
```
|
|
676
697
|
|
|
698
|
+
Without `--apply`, Goal creation authenticates and asks Sloth to calculate the
|
|
699
|
+
Goal without writing it. Preview and apply use the same active-scenario planner
|
|
700
|
+
and return `forecastMonthKey`, the effective priority, the funding account, and
|
|
701
|
+
`forecastBasis`. A null forecast includes the final projected month. The CLI
|
|
702
|
+
does not compare the desired and forecast dates or return affordability advice.
|
|
703
|
+
|
|
677
704
|
Every goal is either Keep or Spend. A Keep goal continues reserving its funded
|
|
678
705
|
money. A Spend goal reserves money until you explicitly mark it spent. Goal
|
|
679
706
|
list, create, and update output includes lowercase `goalType` and nullable
|
|
@@ -687,6 +714,7 @@ move it in the priority order:
|
|
|
687
714
|
sloth-agent goals update \
|
|
688
715
|
--goal-id goal-id \
|
|
689
716
|
--target-month 2027-12 \
|
|
717
|
+
--account-ref sloth_account_v1_... \
|
|
690
718
|
--type spend \
|
|
691
719
|
--apply
|
|
692
720
|
|
|
@@ -715,13 +743,10 @@ required recovery action. Restoring clears `spentAt` and returns the goal to
|
|
|
715
743
|
allocation at its saved priority. Deleting a goal also removes its forecast
|
|
716
744
|
assignments and drift history.
|
|
717
745
|
|
|
718
|
-
Goal sharing remains app-managed.
|
|
719
|
-
target amount in the Sloth Budget app, where account balances can be
|
|
720
|
-
reallocated across goals in priority order. Goal list output includes a
|
|
746
|
+
Goal sharing remains app-managed. Goal list output includes a
|
|
721
747
|
one-based `priority`; `1` is highest. Moving one goal automatically shifts the
|
|
722
|
-
goals between its old and new positions.
|
|
723
|
-
|
|
724
|
-
derived state and refresh when the owner next opens the Forecast screen.
|
|
748
|
+
goals between its old and new positions. Sloth recalculates the active-scenario
|
|
749
|
+
roadmap before every applied Goal mutation and returns the updated forecast.
|
|
725
750
|
|
|
726
751
|
Read uncategorised contributions to the joint budget:
|
|
727
752
|
|
package/dist/args.js
CHANGED
|
@@ -389,7 +389,8 @@ function parseAccounts(args, baseUrl) {
|
|
|
389
389
|
'--ownership',
|
|
390
390
|
'--balance-amount',
|
|
391
391
|
'--account-type',
|
|
392
|
-
'--goal-
|
|
392
|
+
'--goal-funding-account',
|
|
393
|
+
'--partner-visibility',
|
|
393
394
|
]));
|
|
394
395
|
const institutionName = values.get('--institution-name');
|
|
395
396
|
const accountName = values.get('--account-name');
|
|
@@ -397,7 +398,8 @@ function parseAccounts(args, baseUrl) {
|
|
|
397
398
|
const ownershipValue = values.get('--ownership');
|
|
398
399
|
const balanceValue = values.get('--balance-amount');
|
|
399
400
|
const accountTypeValue = values.get('--account-type');
|
|
400
|
-
const sourceValue = values.get('--goal-
|
|
401
|
+
const sourceValue = values.get('--goal-funding-account');
|
|
402
|
+
const partnerVisibility = values.get('--partner-visibility');
|
|
401
403
|
if (currencyValue !== undefined && !/^[A-Za-z]{3}$/.test(currencyValue)) {
|
|
402
404
|
throw new UsageError('--currency must be a three-letter currency code');
|
|
403
405
|
}
|
|
@@ -412,7 +414,13 @@ function parseAccounts(args, baseUrl) {
|
|
|
412
414
|
throw new UsageError('--account-type must be savings or investments');
|
|
413
415
|
}
|
|
414
416
|
if (sourceValue !== undefined && sourceValue !== 'true' && sourceValue !== 'false') {
|
|
415
|
-
throw new UsageError('--goal-
|
|
417
|
+
throw new UsageError('--goal-funding-account must be true or false');
|
|
418
|
+
}
|
|
419
|
+
if (partnerVisibility !== undefined
|
|
420
|
+
&& partnerVisibility !== 'private'
|
|
421
|
+
&& partnerVisibility !== 'balance'
|
|
422
|
+
&& partnerVisibility !== 'holdings') {
|
|
423
|
+
throw new UsageError('--partner-visibility must be private, balance, or holdings');
|
|
416
424
|
}
|
|
417
425
|
const update = {
|
|
418
426
|
...(institutionName === undefined
|
|
@@ -429,7 +437,10 @@ function parseAccounts(args, baseUrl) {
|
|
|
429
437
|
...(accountTypeValue === undefined
|
|
430
438
|
? {}
|
|
431
439
|
: { accountType: accountTypeValue }),
|
|
432
|
-
...(sourceValue === undefined ? {} : {
|
|
440
|
+
...(sourceValue === undefined ? {} : { isGoalFundingAccount: sourceValue === 'true' }),
|
|
441
|
+
...(partnerVisibility === undefined ? {} : {
|
|
442
|
+
partnerVisibility: partnerVisibility,
|
|
443
|
+
}),
|
|
433
444
|
};
|
|
434
445
|
if (Object.keys(update).length === 0) {
|
|
435
446
|
throw new UsageError('accounts update requires at least one field to update');
|
|
@@ -471,6 +482,16 @@ function parseInvestments(args, baseUrl) {
|
|
|
471
482
|
}
|
|
472
483
|
return withBaseUrl({ command: 'investments', ...(accountRef ? { accountRef } : {}) }, baseUrl);
|
|
473
484
|
}
|
|
485
|
+
function parsePortfolio(args, baseUrl) {
|
|
486
|
+
const { values, apply } = parseNamedOptions(args, 'portfolio', new Set(['--view']));
|
|
487
|
+
if (apply)
|
|
488
|
+
throw new UsageError('portfolio does not accept --apply');
|
|
489
|
+
const view = values.get('--view') ?? 'mine';
|
|
490
|
+
if (view !== 'mine' && view !== 'partner' && view !== 'household') {
|
|
491
|
+
throw new UsageError('--view must be mine, partner, or household');
|
|
492
|
+
}
|
|
493
|
+
return withBaseUrl({ command: 'portfolio', view }, baseUrl);
|
|
494
|
+
}
|
|
474
495
|
function parseBudget(args, baseUrl) {
|
|
475
496
|
const subcommand = args[0] === 'status' || args[0] === 'update' || args[0] === 'move'
|
|
476
497
|
? args.shift()
|
|
@@ -652,6 +673,8 @@ function parseGoals(args, baseUrl) {
|
|
|
652
673
|
let targetAmount;
|
|
653
674
|
let targetMonthKey;
|
|
654
675
|
let goalType;
|
|
676
|
+
let fundingAccountRef;
|
|
677
|
+
let priority;
|
|
655
678
|
let apply = false;
|
|
656
679
|
for (let index = 0; index < args.length; index += 1) {
|
|
657
680
|
const argument = args[index];
|
|
@@ -667,7 +690,9 @@ function parseGoals(args, baseUrl) {
|
|
|
667
690
|
if (option !== '--name'
|
|
668
691
|
&& option !== '--target-amount'
|
|
669
692
|
&& option !== '--target-month'
|
|
670
|
-
&& option !== '--type'
|
|
693
|
+
&& option !== '--type'
|
|
694
|
+
&& option !== '--account-ref'
|
|
695
|
+
&& option !== '--priority') {
|
|
671
696
|
throw new UsageError(`Unknown goals create option: ${argument}`);
|
|
672
697
|
}
|
|
673
698
|
const value = requireNonEmpty(inlineValue ?? readOptionValue(args, index, option), option);
|
|
@@ -682,9 +707,15 @@ function parseGoals(args, baseUrl) {
|
|
|
682
707
|
else if (option === '--target-month') {
|
|
683
708
|
targetMonthKey = setOnce(targetMonthKey, parseGoalMonthKey(value, option), option);
|
|
684
709
|
}
|
|
685
|
-
else {
|
|
710
|
+
else if (option === '--type') {
|
|
686
711
|
goalType = setOnce(goalType, parseGoalType(value), option);
|
|
687
712
|
}
|
|
713
|
+
else if (option === '--account-ref') {
|
|
714
|
+
fundingAccountRef = setOnce(fundingAccountRef, parseAccountRef(value), option);
|
|
715
|
+
}
|
|
716
|
+
else {
|
|
717
|
+
priority = setOnce(priority, parseGoalPriority(value), option);
|
|
718
|
+
}
|
|
688
719
|
}
|
|
689
720
|
if (!name)
|
|
690
721
|
throw new UsageError('goals create requires --name <name>');
|
|
@@ -694,12 +725,17 @@ function parseGoals(args, baseUrl) {
|
|
|
694
725
|
if (goalType === undefined) {
|
|
695
726
|
throw new UsageError('goals create requires --type <keep|spend>');
|
|
696
727
|
}
|
|
728
|
+
if (fundingAccountRef === undefined) {
|
|
729
|
+
throw new UsageError('goals create requires --account-ref <accountRef>');
|
|
730
|
+
}
|
|
697
731
|
return withBaseUrl({
|
|
698
732
|
command: 'goals-create',
|
|
699
733
|
name,
|
|
700
734
|
targetAmount,
|
|
701
735
|
...(targetMonthKey === undefined ? {} : { targetMonthKey }),
|
|
702
736
|
goalType,
|
|
737
|
+
fundingAccountRef,
|
|
738
|
+
...(priority === undefined ? {} : { priority }),
|
|
703
739
|
apply,
|
|
704
740
|
}, baseUrl);
|
|
705
741
|
}
|
|
@@ -709,6 +745,7 @@ function parseGoals(args, baseUrl) {
|
|
|
709
745
|
let targetAmount;
|
|
710
746
|
let targetMonthKey;
|
|
711
747
|
let goalType;
|
|
748
|
+
let fundingAccountRef;
|
|
712
749
|
let priority;
|
|
713
750
|
let apply = false;
|
|
714
751
|
for (let index = 0; index < args.length; index += 1) {
|
|
@@ -734,6 +771,7 @@ function parseGoals(args, baseUrl) {
|
|
|
734
771
|
&& option !== '--target-amount'
|
|
735
772
|
&& option !== '--target-month'
|
|
736
773
|
&& option !== '--type'
|
|
774
|
+
&& option !== '--account-ref'
|
|
737
775
|
&& option !== '--priority') {
|
|
738
776
|
throw new UsageError(`Unknown goals update option: ${argument}`);
|
|
739
777
|
}
|
|
@@ -758,6 +796,9 @@ function parseGoals(args, baseUrl) {
|
|
|
758
796
|
else if (option === '--priority') {
|
|
759
797
|
priority = setOnce(priority, parseGoalPriority(value), option);
|
|
760
798
|
}
|
|
799
|
+
else if (option === '--account-ref') {
|
|
800
|
+
fundingAccountRef = setOnce(fundingAccountRef, parseAccountRef(value), option);
|
|
801
|
+
}
|
|
761
802
|
else {
|
|
762
803
|
goalType = setOnce(goalType, parseGoalType(value), option);
|
|
763
804
|
}
|
|
@@ -768,16 +809,10 @@ function parseGoals(args, baseUrl) {
|
|
|
768
809
|
&& targetAmount === undefined
|
|
769
810
|
&& targetMonthKey === undefined
|
|
770
811
|
&& goalType === undefined
|
|
812
|
+
&& fundingAccountRef === undefined
|
|
771
813
|
&& priority === undefined) {
|
|
772
814
|
throw new UsageError('goals update requires at least one field to update');
|
|
773
815
|
}
|
|
774
|
-
if (priority !== undefined
|
|
775
|
-
&& (name !== undefined
|
|
776
|
-
|| targetAmount !== undefined
|
|
777
|
-
|| targetMonthKey !== undefined
|
|
778
|
-
|| goalType !== undefined)) {
|
|
779
|
-
throw new UsageError('--priority must be used on its own');
|
|
780
|
-
}
|
|
781
816
|
return withBaseUrl({
|
|
782
817
|
command: 'goals-update',
|
|
783
818
|
goalId,
|
|
@@ -785,6 +820,7 @@ function parseGoals(args, baseUrl) {
|
|
|
785
820
|
...(targetAmount === undefined ? {} : { targetAmount }),
|
|
786
821
|
...(targetMonthKey === undefined ? {} : { targetMonthKey }),
|
|
787
822
|
...(goalType === undefined ? {} : { goalType }),
|
|
823
|
+
...(fundingAccountRef === undefined ? {} : { fundingAccountRef }),
|
|
788
824
|
...(priority === undefined ? {} : { priority }),
|
|
789
825
|
apply,
|
|
790
826
|
}, baseUrl);
|
|
@@ -964,8 +1000,8 @@ function helpTopic(argv) {
|
|
|
964
1000
|
return 'accounts-remove';
|
|
965
1001
|
return command;
|
|
966
1002
|
}
|
|
967
|
-
if (command === 'investments')
|
|
968
|
-
return
|
|
1003
|
+
if (command === 'investments' || command === 'portfolio')
|
|
1004
|
+
return command;
|
|
969
1005
|
return undefined;
|
|
970
1006
|
}
|
|
971
1007
|
export function parseArgs(argv) {
|
|
@@ -1000,6 +1036,9 @@ export function parseArgs(argv) {
|
|
|
1000
1036
|
if (command === 'investments') {
|
|
1001
1037
|
return parseInvestments(args, baseUrl);
|
|
1002
1038
|
}
|
|
1039
|
+
if (command === 'portfolio') {
|
|
1040
|
+
return parsePortfolio(args, baseUrl);
|
|
1041
|
+
}
|
|
1003
1042
|
if (command === 'budget') {
|
|
1004
1043
|
return parseBudget(args, baseUrl);
|
|
1005
1044
|
}
|
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.20.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]',
|
|
@@ -61,7 +62,8 @@ export function usageText() {
|
|
|
61
62
|
' sloth-agent receipts remove --transaction-ref REF --revision N [--apply]',
|
|
62
63
|
' sloth-agent goals [list] [--base-url URL]',
|
|
63
64
|
' sloth-agent goals create --name NAME --target-amount AMOUNT',
|
|
64
|
-
' --type keep|spend [--target-month YYYY-MM]
|
|
65
|
+
' --type keep|spend --account-ref REF [--target-month YYYY-MM]',
|
|
66
|
+
' [--priority POSITION] [--apply] [--base-url URL]',
|
|
65
67
|
' sloth-agent goals update --goal-id ID [fields] [--apply] [--base-url URL]',
|
|
66
68
|
' sloth-agent goals mark-spent --goal-id ID [--apply] [--base-url URL]',
|
|
67
69
|
' sloth-agent goals restore --goal-id ID [--apply] [--base-url URL]',
|
|
@@ -354,7 +356,7 @@ export function accountsHelpText() {
|
|
|
354
356
|
' accounts[].ownership personal or joint',
|
|
355
357
|
' accounts[].balanceAmount and currency in the native currency when known',
|
|
356
358
|
' accounts[].connectionState and lastBalanceUpdatedAt for freshness',
|
|
357
|
-
' accounts[].
|
|
359
|
+
' accounts[].isGoalFundingAccount whether Goals may use the account',
|
|
358
360
|
].join('\n');
|
|
359
361
|
}
|
|
360
362
|
export function accountsUpdateHelpText() {
|
|
@@ -376,13 +378,16 @@ export function accountsUpdateHelpText() {
|
|
|
376
378
|
' --ownership individual|joint Manual account ownership.',
|
|
377
379
|
' --balance-amount AMOUNT Balance-only account balance.',
|
|
378
380
|
' --account-type savings|investments Balance-only account type.',
|
|
379
|
-
' --goal-
|
|
381
|
+
' --goal-funding-account true|false Whether Goals may use this account.',
|
|
382
|
+
' --partner-visibility private|balance|holdings',
|
|
383
|
+
' What this account shares with your partner.',
|
|
380
384
|
'',
|
|
381
385
|
'Write behavior:',
|
|
382
386
|
' Without --apply, returns a JSON preview without credentials or a network request.',
|
|
383
387
|
' With --apply, requires agent:write on a write-enabled token and updates saved Sloth metadata.',
|
|
384
|
-
' Connected accounts support
|
|
385
|
-
'
|
|
388
|
+
' Connected accounts support --goal-funding-account and --partner-visibility.',
|
|
389
|
+
' Sharing exposes planning data only. It does not change ownership or assign the account to Goals.',
|
|
390
|
+
' Manual current accounts cannot change type, balance, or Goal-funding membership.',
|
|
386
391
|
' Partner-owned shared accounts cannot be changed.',
|
|
387
392
|
' Unknown, disconnected, or inaccessible references return Account not found.',
|
|
388
393
|
...API_ORIGIN_HELP_LINES,
|
|
@@ -441,6 +446,33 @@ export function investmentsHelpText() {
|
|
|
441
446
|
' provider-native and are not converted or guaranteed to reconcile to totals.',
|
|
442
447
|
].join('\n');
|
|
443
448
|
}
|
|
449
|
+
export function portfolioHelpText() {
|
|
450
|
+
return [
|
|
451
|
+
'Sloth Agent CLI — portfolio',
|
|
452
|
+
'',
|
|
453
|
+
'Read your savings and investments from one household planning perspective.',
|
|
454
|
+
'',
|
|
455
|
+
'Usage:',
|
|
456
|
+
' sloth-agent portfolio [--view mine|partner|household] [--base-url URL]',
|
|
457
|
+
'',
|
|
458
|
+
'Options:',
|
|
459
|
+
' --view mine|partner|household Optional. Defaults to mine.',
|
|
460
|
+
' --base-url URL Optional. Override the API origin.',
|
|
461
|
+
' -h, --help Show this help.',
|
|
462
|
+
...API_ORIGIN_HELP_LINES,
|
|
463
|
+
'',
|
|
464
|
+
'Access:',
|
|
465
|
+
' This read-only command waits up to 45 seconds for eligible linked balances to refresh.',
|
|
466
|
+
' Partner shows only balances or holdings your partner explicitly shared.',
|
|
467
|
+
' Household combines your accounts with those shared balances and deduplicates joint accounts.',
|
|
468
|
+
' Shared data supports planning only. It does not assign partner accounts to Goals or change ownership.',
|
|
469
|
+
'',
|
|
470
|
+
'Output:',
|
|
471
|
+
' totals gives savings, investments, and tracked amounts in the viewer currency.',
|
|
472
|
+
' accounts includes ownerRole, freshness, sharing level, and permitted holdings.',
|
|
473
|
+
' refresh reports whether eligible linked balances refreshed or cached data was returned.',
|
|
474
|
+
].join('\n');
|
|
475
|
+
}
|
|
444
476
|
export function budgetHelpText() {
|
|
445
477
|
return [
|
|
446
478
|
'Sloth Agent CLI — budget',
|
|
@@ -720,7 +752,7 @@ export function goalsHelpText() {
|
|
|
720
752
|
return [
|
|
721
753
|
'Sloth Agent CLI — goals',
|
|
722
754
|
'',
|
|
723
|
-
'List, create, update, mark spent, restore, or delete
|
|
755
|
+
'List, preview, create, update, mark spent, restore, or delete account-funded Goals.',
|
|
724
756
|
'',
|
|
725
757
|
'Commands:',
|
|
726
758
|
' sloth-agent goals list List goals; "sloth-agent goals" is equivalent.',
|
|
@@ -754,9 +786,9 @@ export function goalsListHelpText() {
|
|
|
754
786
|
' This command is read-only.',
|
|
755
787
|
'',
|
|
756
788
|
'Output:',
|
|
757
|
-
' JSON containing currency and goals. Each goal
|
|
758
|
-
'
|
|
759
|
-
'
|
|
789
|
+
' JSON containing currency, forecastBasis, and goals. Each goal includes its',
|
|
790
|
+
' effectivePriority, funding account, desired targetMonthKey, and calculated',
|
|
791
|
+
' forecastMonthKey.',
|
|
760
792
|
].join('\n');
|
|
761
793
|
}
|
|
762
794
|
export function goalsCreateHelpText() {
|
|
@@ -766,30 +798,33 @@ export function goalsCreateHelpText() {
|
|
|
766
798
|
'Preview or create a goal.',
|
|
767
799
|
'',
|
|
768
800
|
'Usage:',
|
|
769
|
-
' sloth-agent goals create --name NAME --target-amount AMOUNT --type keep|spend [options]',
|
|
801
|
+
' sloth-agent goals create --name NAME --target-amount AMOUNT --type keep|spend --account-ref REF [options]',
|
|
770
802
|
'',
|
|
771
803
|
'Options:',
|
|
772
804
|
' --name NAME Required. Goal name, 1 to 200 characters.',
|
|
773
805
|
' --target-amount AMOUNT Required. Positive major-unit amount with up to 2 decimals.',
|
|
774
806
|
' --type keep|spend Required. Keep reserves funded money; Spend is spent later.',
|
|
775
|
-
' --
|
|
807
|
+
' --account-ref REF Required. Personal Goal-funding account from accounts list.',
|
|
808
|
+
' --target-month YYYY-MM Optional. Desired calendar month; it does not change the forecast.',
|
|
809
|
+
' --priority POSITION Optional. One-based priority; defaults to append.',
|
|
776
810
|
' --apply Optional. Create the goal in Sloth Money.',
|
|
777
811
|
' --base-url URL Optional. Override the API origin.',
|
|
778
812
|
' -h, --help Show this help.',
|
|
779
813
|
...API_ORIGIN_HELP_LINES,
|
|
780
814
|
'',
|
|
781
815
|
'Safety:',
|
|
782
|
-
' Without --apply, the command
|
|
816
|
+
' Without --apply, the command authenticates and asks Sloth to calculate the Goal.',
|
|
817
|
+
' The server performs no writes for a preview.',
|
|
783
818
|
' Applying requires a write-enabled token created with Allow changes.',
|
|
784
|
-
' New goals are private to the owner and
|
|
819
|
+
' New goals are private to the owner and append unless --priority is supplied.',
|
|
785
820
|
'',
|
|
786
821
|
'Example:',
|
|
787
|
-
' sloth-agent goals create --name "Emergency fund" --target-amount 12000 --type keep',
|
|
788
|
-
' sloth-agent goals create --name "Wedding" --target-amount 22000 --type spend --target-month 2027-06 --apply',
|
|
822
|
+
' sloth-agent goals create --name "Emergency fund" --target-amount 12000 --type keep --account-ref REF',
|
|
823
|
+
' sloth-agent goals create --name "Wedding" --target-amount 22000 --type spend --account-ref REF --target-month 2027-06 --apply',
|
|
789
824
|
'',
|
|
790
825
|
'Output:',
|
|
791
|
-
' Preview
|
|
792
|
-
'
|
|
826
|
+
' Preview and apply return the Goal, currency, calculated forecastMonthKey,',
|
|
827
|
+
' effective priority, funding-account details, and forecastBasis.',
|
|
793
828
|
].join('\n');
|
|
794
829
|
}
|
|
795
830
|
export function goalsUpdateHelpText() {
|
|
@@ -808,6 +843,7 @@ export function goalsUpdateHelpText() {
|
|
|
808
843
|
' --target-month YYYY-MM Optional. Replace the target month.',
|
|
809
844
|
' --clear-target-month Optional. Remove the target month.',
|
|
810
845
|
' --type keep|spend Optional. Change how funded money is treated.',
|
|
846
|
+
' --account-ref REF Optional. Reassign to another personal Goal-funding account.',
|
|
811
847
|
' --priority POSITION Optional. Positive whole-number position; 1 is highest.',
|
|
812
848
|
' --apply Optional. Write the partial update.',
|
|
813
849
|
' --base-url URL Optional. Override the API origin.',
|
|
@@ -816,17 +852,13 @@ export function goalsUpdateHelpText() {
|
|
|
816
852
|
'',
|
|
817
853
|
'Constraints:',
|
|
818
854
|
' Provide at least one field to update.',
|
|
819
|
-
' Priority must be updated on its own.',
|
|
820
855
|
' Priority 1 is highest. The position cannot exceed the current goal count.',
|
|
821
856
|
' Moving a goal shifts the intervening goals automatically.',
|
|
822
|
-
'
|
|
823
|
-
' the Forecast screen.',
|
|
857
|
+
' Sloth recalculates the Goal roadmap before saving the update.',
|
|
824
858
|
' Set and clear target-month options are mutually exclusive.',
|
|
825
859
|
' Restore a spent goal before changing its type.',
|
|
826
|
-
'
|
|
827
|
-
'
|
|
828
|
-
' Sharing remains app-managed. Updates to an already shared goal remain visible',
|
|
829
|
-
' to the connected partner.',
|
|
860
|
+
' Sharing remains app-managed. Updates to a shared Goal remain visible to the',
|
|
861
|
+
' connected partner, but its assigned account remains private to the owner.',
|
|
830
862
|
'',
|
|
831
863
|
'Safety:',
|
|
832
864
|
' Without --apply, the command returns a dry-run preview and does not write.',
|
|
@@ -838,7 +870,8 @@ export function goalsUpdateHelpText() {
|
|
|
838
870
|
'',
|
|
839
871
|
'Output:',
|
|
840
872
|
' Preview mode returns dryRun, method, endpoint, and payload.',
|
|
841
|
-
' Apply mode returns the complete persisted
|
|
873
|
+
' Apply mode returns the complete persisted Goal, calculated forecastMonthKey,',
|
|
874
|
+
' effective priority, funding-account details, currency, and forecastBasis.',
|
|
842
875
|
].join('\n');
|
|
843
876
|
}
|
|
844
877
|
export function goalsMarkSpentHelpText() {
|
|
@@ -1171,6 +1204,7 @@ export function commandHelpText(topic) {
|
|
|
1171
1204
|
'accounts-update': accountsUpdateHelpText,
|
|
1172
1205
|
'accounts-remove': accountsRemoveHelpText,
|
|
1173
1206
|
investments: investmentsHelpText,
|
|
1207
|
+
portfolio: portfolioHelpText,
|
|
1174
1208
|
budget: budgetHelpText,
|
|
1175
1209
|
'budget-status': budgetStatusHelpText,
|
|
1176
1210
|
'budget-move': budgetMoveHelpText,
|
|
@@ -1207,23 +1241,6 @@ export function commandHelpText(topic) {
|
|
|
1207
1241
|
function writeJson(write, data) {
|
|
1208
1242
|
write(`${JSON.stringify(data, null, 2)}\n`);
|
|
1209
1243
|
}
|
|
1210
|
-
function withListedGoalPriorities(value) {
|
|
1211
|
-
const response = value;
|
|
1212
|
-
return {
|
|
1213
|
-
...response,
|
|
1214
|
-
goals: response.goals.map((goal, index) => ({
|
|
1215
|
-
...goal,
|
|
1216
|
-
priority: index + 1,
|
|
1217
|
-
})),
|
|
1218
|
-
};
|
|
1219
|
-
}
|
|
1220
|
-
function withUpdatedGoalPriority(value, priority) {
|
|
1221
|
-
const response = value;
|
|
1222
|
-
return {
|
|
1223
|
-
...response,
|
|
1224
|
-
goal: { ...response.goal, priority },
|
|
1225
|
-
};
|
|
1226
|
-
}
|
|
1227
1244
|
function redact(value, token) {
|
|
1228
1245
|
return token ? value.split(token).join('[REDACTED]') : value;
|
|
1229
1246
|
}
|
|
@@ -1404,7 +1421,11 @@ async function parseHttpResponse(response, token) {
|
|
|
1404
1421
|
&& typeof data.error === 'string')
|
|
1405
1422
|
? data.error
|
|
1406
1423
|
: `Agent API request failed with status ${response.status}`;
|
|
1407
|
-
|
|
1424
|
+
const code = (data
|
|
1425
|
+
&& typeof data === 'object'
|
|
1426
|
+
&& 'code' in data
|
|
1427
|
+
&& typeof data.code === 'string') ? data.code : undefined;
|
|
1428
|
+
throw new ApiError(redact(message, token), response.status, code);
|
|
1408
1429
|
}
|
|
1409
1430
|
return data;
|
|
1410
1431
|
}
|
|
@@ -1891,7 +1912,7 @@ export async function runCli(argv = process.argv.slice(2), options = {}) {
|
|
|
1891
1912
|
return 0;
|
|
1892
1913
|
}
|
|
1893
1914
|
if (parsed.command === 'goals-create') {
|
|
1894
|
-
const endpoint = `${baseUrl}/api/agent/v1/goals`;
|
|
1915
|
+
const endpoint = `${baseUrl}/api/agent/v1/goals${parsed.apply ? '' : '/preview'}`;
|
|
1895
1916
|
const payload = {
|
|
1896
1917
|
name: parsed.name,
|
|
1897
1918
|
targetAmount: parsed.targetAmount,
|
|
@@ -1899,16 +1920,9 @@ export async function runCli(argv = process.argv.slice(2), options = {}) {
|
|
|
1899
1920
|
? {}
|
|
1900
1921
|
: { targetMonthKey: parsed.targetMonthKey }),
|
|
1901
1922
|
goalType: parsed.goalType,
|
|
1923
|
+
fundingAccountRef: parsed.fundingAccountRef,
|
|
1924
|
+
...(parsed.priority === undefined ? {} : { priority: parsed.priority }),
|
|
1902
1925
|
};
|
|
1903
|
-
if (!parsed.apply) {
|
|
1904
|
-
writeJson(writeStdout, {
|
|
1905
|
-
dryRun: true,
|
|
1906
|
-
endpoint,
|
|
1907
|
-
method: 'POST',
|
|
1908
|
-
payload,
|
|
1909
|
-
});
|
|
1910
|
-
return 0;
|
|
1911
|
-
}
|
|
1912
1926
|
const response = await fetchImplementation(endpoint, {
|
|
1913
1927
|
method: 'POST',
|
|
1914
1928
|
headers: {
|
|
@@ -1918,7 +1932,7 @@ export async function runCli(argv = process.argv.slice(2), options = {}) {
|
|
|
1918
1932
|
body: JSON.stringify(payload),
|
|
1919
1933
|
signal: AbortSignal.timeout(REQUEST_TIMEOUT_MS),
|
|
1920
1934
|
});
|
|
1921
|
-
const data = parseApiResponse('goals-create', await parseHttpResponse(response, token));
|
|
1935
|
+
const data = parseApiResponse(parsed.apply ? 'goals-create' : 'goals-preview', await parseHttpResponse(response, token));
|
|
1922
1936
|
writeJson(writeStdout, data);
|
|
1923
1937
|
return 0;
|
|
1924
1938
|
}
|
|
@@ -1938,6 +1952,9 @@ export async function runCli(argv = process.argv.slice(2), options = {}) {
|
|
|
1938
1952
|
...(parsed.goalType === undefined
|
|
1939
1953
|
? {}
|
|
1940
1954
|
: { goalType: parsed.goalType }),
|
|
1955
|
+
...(parsed.fundingAccountRef === undefined
|
|
1956
|
+
? {}
|
|
1957
|
+
: { fundingAccountRef: parsed.fundingAccountRef }),
|
|
1941
1958
|
...(parsed.priority === undefined
|
|
1942
1959
|
? {}
|
|
1943
1960
|
: { priority: parsed.priority }),
|
|
@@ -1962,9 +1979,7 @@ export async function runCli(argv = process.argv.slice(2), options = {}) {
|
|
|
1962
1979
|
signal: AbortSignal.timeout(REQUEST_TIMEOUT_MS),
|
|
1963
1980
|
});
|
|
1964
1981
|
const data = parseApiResponse(parsed.command, await parseHttpResponse(response, token));
|
|
1965
|
-
writeJson(writeStdout,
|
|
1966
|
-
? data
|
|
1967
|
-
: withUpdatedGoalPriority(data, parsed.priority));
|
|
1982
|
+
writeJson(writeStdout, data);
|
|
1968
1983
|
return 0;
|
|
1969
1984
|
}
|
|
1970
1985
|
if (parsed.command === 'goals-delete') {
|
|
@@ -2059,7 +2074,7 @@ export async function runCli(argv = process.argv.slice(2), options = {}) {
|
|
|
2059
2074
|
signal: AbortSignal.timeout(REQUEST_TIMEOUT_MS),
|
|
2060
2075
|
});
|
|
2061
2076
|
const data = parseApiResponse('goals-list', await parseHttpResponse(response, token));
|
|
2062
|
-
writeJson(writeStdout,
|
|
2077
|
+
writeJson(writeStdout, data);
|
|
2063
2078
|
return 0;
|
|
2064
2079
|
}
|
|
2065
2080
|
if (parsed.command === 'budget' || parsed.command === 'budget-status') {
|
|
@@ -2080,19 +2095,21 @@ export async function runCli(argv = process.argv.slice(2), options = {}) {
|
|
|
2080
2095
|
}
|
|
2081
2096
|
const path = parsed.command === 'accounts'
|
|
2082
2097
|
? '/api/agent/v1/accounts'
|
|
2083
|
-
: parsed.command === '
|
|
2084
|
-
? `/api/agent/v1/
|
|
2085
|
-
|
|
2086
|
-
|
|
2087
|
-
|
|
2088
|
-
|
|
2089
|
-
:
|
|
2090
|
-
|
|
2091
|
-
|
|
2092
|
-
|
|
2098
|
+
: parsed.command === 'portfolio'
|
|
2099
|
+
? `/api/agent/v1/portfolio?${new URLSearchParams({ view: parsed.view }).toString()}`
|
|
2100
|
+
: parsed.command === 'investments'
|
|
2101
|
+
? `/api/agent/v1/investments${parsed.accountRef
|
|
2102
|
+
? `?${new URLSearchParams({ accountRef: parsed.accountRef }).toString()}`
|
|
2103
|
+
: ''}`
|
|
2104
|
+
: parsed.command === 'categories'
|
|
2105
|
+
? '/api/agent/v1/categories'
|
|
2106
|
+
: `/api/agent/v1/transactions${(() => {
|
|
2107
|
+
const query = buildTransactionsQuery(parsed.filters);
|
|
2108
|
+
return query ? `?${query}` : '';
|
|
2109
|
+
})()}`;
|
|
2093
2110
|
const response = await fetchImplementation(`${baseUrl}${path}`, {
|
|
2094
2111
|
method: 'GET',
|
|
2095
|
-
headers: parsed.command === 'transactions'
|
|
2112
|
+
headers: parsed.command === 'transactions' || parsed.command === 'portfolio'
|
|
2096
2113
|
? { ...headers, Prefer: 'wait=45' }
|
|
2097
2114
|
: headers,
|
|
2098
2115
|
signal: AbortSignal.timeout(REQUEST_TIMEOUT_MS),
|
|
@@ -2104,7 +2121,8 @@ export async function runCli(argv = process.argv.slice(2), options = {}) {
|
|
|
2104
2121
|
catch (error) {
|
|
2105
2122
|
const exitCode = error instanceof CliError ? error.exitCode : 1;
|
|
2106
2123
|
const message = error instanceof Error ? error.message : String(error);
|
|
2107
|
-
|
|
2124
|
+
const prefix = error instanceof ApiError && error.code ? `${error.code}: ` : '';
|
|
2125
|
+
writeStderr(`${prefix}${redact(message, token)}\n`);
|
|
2108
2126
|
return exitCode;
|
|
2109
2127
|
}
|
|
2110
2128
|
}
|
package/dist/contracts.js
CHANGED
|
@@ -610,6 +610,25 @@ function hasOnlyFields(value, fields) {
|
|
|
610
610
|
const allowed = new Set(fields);
|
|
611
611
|
return Object.keys(value).every((key) => allowed.has(key));
|
|
612
612
|
}
|
|
613
|
+
function isMonthKeyOrNull(value) {
|
|
614
|
+
return value === null || (typeof value === 'string'
|
|
615
|
+
&& /^\d{4}-(0[1-9]|1[0-2])$/.test(value));
|
|
616
|
+
}
|
|
617
|
+
function isPlannedGoalFields(value) {
|
|
618
|
+
return (typeof value.name === 'string'
|
|
619
|
+
&& value.name.trim().length > 0
|
|
620
|
+
&& typeof value.targetAmount === 'number'
|
|
621
|
+
&& Number.isFinite(value.targetAmount)
|
|
622
|
+
&& value.targetAmount > 0
|
|
623
|
+
&& isMonthKeyOrNull(value.targetMonthKey)
|
|
624
|
+
&& isMonthKeyOrNull(value.forecastMonthKey)
|
|
625
|
+
&& isGoalType(value.goalType)
|
|
626
|
+
&& Number.isSafeInteger(value.effectivePriority)
|
|
627
|
+
&& Number(value.effectivePriority) > 0
|
|
628
|
+
&& isAccountRef(value.fundingAccountRef)
|
|
629
|
+
&& typeof value.fundingAccountLabel === 'string'
|
|
630
|
+
&& value.fundingAccountLabel.trim().length > 0);
|
|
631
|
+
}
|
|
613
632
|
function isGoal(value) {
|
|
614
633
|
return (isObject(value)
|
|
615
634
|
&& hasOnlyFields(value, [
|
|
@@ -617,20 +636,17 @@ function isGoal(value) {
|
|
|
617
636
|
'name',
|
|
618
637
|
'targetAmount',
|
|
619
638
|
'targetMonthKey',
|
|
639
|
+
'forecastMonthKey',
|
|
620
640
|
'goalType',
|
|
621
641
|
'spentAt',
|
|
622
642
|
'sharedWithPartner',
|
|
643
|
+
'effectivePriority',
|
|
644
|
+
'fundingAccountRef',
|
|
645
|
+
'fundingAccountLabel',
|
|
623
646
|
])
|
|
624
647
|
&& typeof value.id === 'string'
|
|
625
648
|
&& value.id.trim().length > 0
|
|
626
|
-
&&
|
|
627
|
-
&& value.name.trim().length > 0
|
|
628
|
-
&& (value.targetAmount === null
|
|
629
|
-
|| (typeof value.targetAmount === 'number' && Number.isFinite(value.targetAmount)))
|
|
630
|
-
&& (value.targetMonthKey === null
|
|
631
|
-
|| (typeof value.targetMonthKey === 'string'
|
|
632
|
-
&& /^\d{4}-(0[1-9]|1[0-2])$/.test(value.targetMonthKey)))
|
|
633
|
-
&& isGoalType(value.goalType)
|
|
649
|
+
&& isPlannedGoalFields(value)
|
|
634
650
|
&& (value.goalType === 'spend'
|
|
635
651
|
? value.spentAt === null || isIsoDateTime(value.spentAt)
|
|
636
652
|
: value.spentAt === null)
|
|
@@ -844,7 +860,8 @@ function isAccount(value) {
|
|
|
844
860
|
'source',
|
|
845
861
|
'lastBalanceUpdatedAt',
|
|
846
862
|
'connectionState',
|
|
847
|
-
'
|
|
863
|
+
'isGoalFundingAccount',
|
|
864
|
+
'partnerVisibility',
|
|
848
865
|
])
|
|
849
866
|
&& isAccountRef(value.accountRef)
|
|
850
867
|
&& isNullableNonEmptyString(value.accountName)
|
|
@@ -863,7 +880,10 @@ function isAccount(value) {
|
|
|
863
880
|
|| value.connectionState === 'expired'
|
|
864
881
|
|| value.connectionState === 'manual'
|
|
865
882
|
|| value.connectionState === 'unknown')
|
|
866
|
-
&& typeof value.
|
|
883
|
+
&& typeof value.isGoalFundingAccount === 'boolean'
|
|
884
|
+
&& (value.partnerVisibility === 'private'
|
|
885
|
+
|| value.partnerVisibility === 'balance'
|
|
886
|
+
|| value.partnerVisibility === 'holdings'));
|
|
867
887
|
}
|
|
868
888
|
function isAccountsResponse(value) {
|
|
869
889
|
return (isObject(value)
|
|
@@ -929,19 +949,104 @@ function isInvestmentsResponse(value) {
|
|
|
929
949
|
&& holdings.every(isInvestmentHolding));
|
|
930
950
|
}));
|
|
931
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
|
+
}
|
|
1001
|
+
function isForecastBasis(value) {
|
|
1002
|
+
return isObject(value)
|
|
1003
|
+
&& hasOnlyFields(value, [
|
|
1004
|
+
'calculatedAt',
|
|
1005
|
+
'activeScenarioRevision',
|
|
1006
|
+
'projectionThroughMonthKey',
|
|
1007
|
+
])
|
|
1008
|
+
&& isIsoDateTime(value.calculatedAt)
|
|
1009
|
+
&& Number.isSafeInteger(value.activeScenarioRevision)
|
|
1010
|
+
&& Number(value.activeScenarioRevision) >= 0
|
|
1011
|
+
&& isMonthKeyOrNull(value.projectionThroughMonthKey) === true
|
|
1012
|
+
&& value.projectionThroughMonthKey !== null;
|
|
1013
|
+
}
|
|
1014
|
+
function isGoalPreview(value) {
|
|
1015
|
+
return isObject(value)
|
|
1016
|
+
&& hasOnlyFields(value, [
|
|
1017
|
+
'name',
|
|
1018
|
+
'targetAmount',
|
|
1019
|
+
'targetMonthKey',
|
|
1020
|
+
'forecastMonthKey',
|
|
1021
|
+
'goalType',
|
|
1022
|
+
'effectivePriority',
|
|
1023
|
+
'fundingAccountRef',
|
|
1024
|
+
'fundingAccountLabel',
|
|
1025
|
+
])
|
|
1026
|
+
&& isPlannedGoalFields(value);
|
|
1027
|
+
}
|
|
932
1028
|
function isGoalsResponse(value) {
|
|
933
1029
|
return (isObject(value)
|
|
934
|
-
&& hasOnlyFields(value, ['currency', 'goals'])
|
|
1030
|
+
&& hasOnlyFields(value, ['currency', 'forecastBasis', 'goals'])
|
|
935
1031
|
&& isCurrency(value.currency)
|
|
1032
|
+
&& isForecastBasis(value.forecastBasis)
|
|
936
1033
|
&& Array.isArray(value.goals)
|
|
937
1034
|
&& value.goals.every(isGoal));
|
|
938
1035
|
}
|
|
939
1036
|
function isGoalMutationResponse(value) {
|
|
940
1037
|
return (isObject(value)
|
|
941
|
-
&& hasOnlyFields(value, ['currency', 'goal'])
|
|
1038
|
+
&& hasOnlyFields(value, ['currency', 'forecastBasis', 'goal'])
|
|
942
1039
|
&& isCurrency(value.currency)
|
|
1040
|
+
&& isForecastBasis(value.forecastBasis)
|
|
943
1041
|
&& isGoal(value.goal));
|
|
944
1042
|
}
|
|
1043
|
+
function isGoalPreviewResponse(value) {
|
|
1044
|
+
return isObject(value)
|
|
1045
|
+
&& hasOnlyFields(value, ['currency', 'forecastBasis', 'goal'])
|
|
1046
|
+
&& isCurrency(value.currency)
|
|
1047
|
+
&& isForecastBasis(value.forecastBasis)
|
|
1048
|
+
&& isGoalPreview(value.goal);
|
|
1049
|
+
}
|
|
945
1050
|
function isGoalDeleteResponse(value) {
|
|
946
1051
|
return (isObject(value)
|
|
947
1052
|
&& hasOnlyFields(value, ['deleted', 'deletedGoalId'])
|
|
@@ -1007,45 +1112,49 @@ export function parseApiResponse(command, value) {
|
|
|
1007
1112
|
? isAccountRemovalResponse(value)
|
|
1008
1113
|
: command === 'investments'
|
|
1009
1114
|
? isInvestmentsResponse(value)
|
|
1010
|
-
: command === '
|
|
1011
|
-
?
|
|
1012
|
-
: command === 'budget-
|
|
1013
|
-
?
|
|
1014
|
-
: command === 'budget-
|
|
1015
|
-
?
|
|
1016
|
-
: command === '
|
|
1017
|
-
?
|
|
1018
|
-
: command === 'categories
|
|
1019
|
-
?
|
|
1020
|
-
: command === '
|
|
1021
|
-
?
|
|
1022
|
-
: command === '
|
|
1023
|
-
?
|
|
1024
|
-
: command === '
|
|
1025
|
-
?
|
|
1026
|
-
: command === 'rules-
|
|
1027
|
-
?
|
|
1028
|
-
: command === 'rules-
|
|
1029
|
-
?
|
|
1030
|
-
: command === 'rules-
|
|
1031
|
-
?
|
|
1032
|
-
: command === '
|
|
1033
|
-
?
|
|
1034
|
-
: command === 'receipts-
|
|
1035
|
-
?
|
|
1036
|
-
: command === 'receipts-
|
|
1037
|
-
?
|
|
1038
|
-
: command === 'receipts-
|
|
1039
|
-
?
|
|
1040
|
-
: command === '
|
|
1041
|
-
?
|
|
1042
|
-
: command === '
|
|
1043
|
-
?
|
|
1044
|
-
: command === '
|
|
1045
|
-
?
|
|
1046
|
-
: command === 'goals-
|
|
1047
|
-
?
|
|
1048
|
-
:
|
|
1115
|
+
: command === 'portfolio'
|
|
1116
|
+
? isPortfolioResponse(value)
|
|
1117
|
+
: command === 'budget' || command === 'budget-update'
|
|
1118
|
+
? isBudgetResponse(value)
|
|
1119
|
+
: command === 'budget-status'
|
|
1120
|
+
? isBudgetActivityStatusResponse(value)
|
|
1121
|
+
: command === 'budget-move'
|
|
1122
|
+
? isBudgetMovementResponse(value)
|
|
1123
|
+
: command === 'categories'
|
|
1124
|
+
? isCategoryResponse(value)
|
|
1125
|
+
: command === 'categories-create' || command === 'categories-rename'
|
|
1126
|
+
? isCategoryMutationResponse(value)
|
|
1127
|
+
: command === 'line-items-create' || command === 'line-items-rename'
|
|
1128
|
+
? isLineItemMutationResponse(value)
|
|
1129
|
+
: command === 'transactions'
|
|
1130
|
+
? isTransactionsResponse(value)
|
|
1131
|
+
: command === 'rules-list'
|
|
1132
|
+
? isNotificationRuleListResponse(value)
|
|
1133
|
+
: command === 'rules-get' || command === 'rules-set'
|
|
1134
|
+
? isNotificationRuleResponse(value)
|
|
1135
|
+
: command === 'rules-delete'
|
|
1136
|
+
? isNotificationRuleDeleteResponse(value)
|
|
1137
|
+
: command === 'rules-scan-contract'
|
|
1138
|
+
? isRenewalExtractionResponse(value)
|
|
1139
|
+
: command === 'receipts-extract'
|
|
1140
|
+
? isReceiptExtractResponse(value)
|
|
1141
|
+
: command === 'receipts-get'
|
|
1142
|
+
? isReceiptLookupResponse(value)
|
|
1143
|
+
: command === 'receipts-attach'
|
|
1144
|
+
? isReceiptMutationResponse(value)
|
|
1145
|
+
: command === 'receipts-remove'
|
|
1146
|
+
? isReceiptDeleteResponse(value)
|
|
1147
|
+
: command === 'assign'
|
|
1148
|
+
? isAssignmentResponse(value)
|
|
1149
|
+
: command === 'ask-partner'
|
|
1150
|
+
? isPartnerResponse(value)
|
|
1151
|
+
: command === 'goals-list'
|
|
1152
|
+
? isGoalsResponse(value)
|
|
1153
|
+
: command === 'goals-preview'
|
|
1154
|
+
? isGoalPreviewResponse(value)
|
|
1155
|
+
: command === 'goals-delete'
|
|
1156
|
+
? isGoalDeleteResponse(value)
|
|
1157
|
+
: isGoalMutationResponse(value);
|
|
1049
1158
|
if (!valid) {
|
|
1050
1159
|
const label = command === 'assign' ? 'assignment' : command;
|
|
1051
1160
|
throw new ApiError(`Invalid ${label} response from the Agent API`);
|
package/dist/errors.js
CHANGED
|
@@ -17,8 +17,10 @@ export class ConfigError extends CliError {
|
|
|
17
17
|
}
|
|
18
18
|
export class ApiError extends CliError {
|
|
19
19
|
status;
|
|
20
|
-
|
|
20
|
+
code;
|
|
21
|
+
constructor(message, status, code) {
|
|
21
22
|
super(message, 1);
|
|
22
23
|
this.status = status;
|
|
24
|
+
this.code = code;
|
|
23
25
|
}
|
|
24
26
|
}
|